@netless/app-slide 0.0.25 → 0.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/SlideController/index.d.ts +11 -3
- package/dist/SlideDocsViewer/index.d.ts +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/main.cjs.js +344 -208
- package/dist/main.cjs.js.map +1 -1
- package/dist/main.es.js +2476 -1806
- package/dist/main.es.js.map +1 -1
- package/dist/main.iife.js +274 -138
- package/dist/main.iife.js.map +1 -1
- package/dist/utils/bgcolor.d.ts +3 -0
- package/dist/utils/freezer.d.ts +21 -0
- package/package.json +4 -2
- package/src/SlideController/index.ts +93 -11
- package/src/SlideDocsViewer/index.ts +15 -8
- package/src/index.ts +9 -2
- package/src/utils/bgcolor.ts +46 -0
- package/src/utils/freezer.ts +78 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RegisterParams } from "@netless/window-manager";
|
|
2
|
+
export interface FreezableSlide {
|
|
3
|
+
freeze: () => void;
|
|
4
|
+
unfreeze: () => void;
|
|
5
|
+
}
|
|
6
|
+
export declare const FreezerLength = 2;
|
|
7
|
+
export declare const apps: {
|
|
8
|
+
map: Map<string, FreezableSlide>;
|
|
9
|
+
queue: string[];
|
|
10
|
+
validateQueue(): void;
|
|
11
|
+
set(appId: string, slide: FreezableSlide): void;
|
|
12
|
+
delete(appId: string): void;
|
|
13
|
+
focus(appId: string): void;
|
|
14
|
+
};
|
|
15
|
+
export declare type AddHooks = NonNullable<RegisterParams["addHooks"]>;
|
|
16
|
+
/**
|
|
17
|
+
* Put this function in your register code:
|
|
18
|
+
* `WindowManager.register({ addHooks })`
|
|
19
|
+
* So that it will automatically freeze the app when it is not in focus.
|
|
20
|
+
*/
|
|
21
|
+
export declare const addHooks: AddHooks;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netless/app-slide",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.29",
|
|
4
4
|
"main": "dist/main.cjs.js",
|
|
5
5
|
"module": "dist/main.es.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
],
|
|
12
12
|
"devDependencies": {
|
|
13
13
|
"@netless/app-shared": "^0.1.1",
|
|
14
|
-
"@netless/slide": "^0.1.
|
|
14
|
+
"@netless/slide": "^0.1.34",
|
|
15
|
+
"@types/color-string": "^1.5.0",
|
|
16
|
+
"color-string": "^1.7.1",
|
|
15
17
|
"side-effect-manager": "^0.1.5",
|
|
16
18
|
"vanilla-lazyload": "^17.5.0"
|
|
17
19
|
},
|
|
@@ -16,6 +16,7 @@ import type { Attributes, MagixPayload, SlideState } from "../typings";
|
|
|
16
16
|
import { SideEffectManager } from "side-effect-manager";
|
|
17
17
|
import { Slide, SLIDE_EVENTS } from "@netless/slide";
|
|
18
18
|
import { clamp, deepClone, isObj } from "../utils/helpers";
|
|
19
|
+
import { cachedGetBgColor } from "../utils/bgcolor";
|
|
19
20
|
export { syncSceneWithSlide, createDocsViewerPages } from "./helpers";
|
|
20
21
|
|
|
21
22
|
export const DefaultUrl = "https://convertcdn.netless.link/dynamicConvert";
|
|
@@ -75,6 +76,16 @@ export class SlideController {
|
|
|
75
76
|
this.initialize();
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
public ready = false;
|
|
80
|
+
private resolveReady!: () => void;
|
|
81
|
+
public readonly readyPromise = new Promise<void>(resolve => {
|
|
82
|
+
this.resolveReady = () =>
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
this.ready = true;
|
|
85
|
+
resolve();
|
|
86
|
+
}, 1000);
|
|
87
|
+
});
|
|
88
|
+
|
|
78
89
|
public jumpToPage(page: number) {
|
|
79
90
|
if (this.ready) {
|
|
80
91
|
page = clamp(page, 1, this.pageCount);
|
|
@@ -96,6 +107,7 @@ export class SlideController {
|
|
|
96
107
|
if (this.debug) {
|
|
97
108
|
console.log("[Slide] init with state", deepClone(state));
|
|
98
109
|
}
|
|
110
|
+
this.syncStateOnceFlag = false;
|
|
99
111
|
slide.setSlideState(deepClone(state));
|
|
100
112
|
} else if (context.isAddApp) {
|
|
101
113
|
// otherwise, maybe this slide is just added, let the adder kick start first render
|
|
@@ -130,6 +142,11 @@ export class SlideController {
|
|
|
130
142
|
return disposer;
|
|
131
143
|
});
|
|
132
144
|
|
|
145
|
+
this.sideEffect.add(() => {
|
|
146
|
+
context.emitter.on("seek", this.onSeeked);
|
|
147
|
+
return () => context.emitter.off("seek", this.onSeeked);
|
|
148
|
+
});
|
|
149
|
+
|
|
133
150
|
this.sideEffect.add(() => {
|
|
134
151
|
const displayer = context.getDisplayer();
|
|
135
152
|
displayer.addMagixEventListener(this.channel, this.magixEventListener, {
|
|
@@ -148,6 +165,8 @@ export class SlideController {
|
|
|
148
165
|
slide.on(SLIDE_EVENTS.renderError, this.onError);
|
|
149
166
|
slide.on(SLIDE_EVENTS.stateChange, this.onStateChange);
|
|
150
167
|
slide.on(SLIDE_EVENTS.syncDispatch, this.onSyncDispatch);
|
|
168
|
+
|
|
169
|
+
slide.on(SLIDE_EVENTS.renderEnd, this.resolveReady);
|
|
151
170
|
}
|
|
152
171
|
|
|
153
172
|
private onSyncDispatch = (event: SyncEvent) => {
|
|
@@ -195,19 +214,29 @@ export class SlideController {
|
|
|
195
214
|
|
|
196
215
|
private onStateChange = (state: SlideState) => {
|
|
197
216
|
if (this.context.getIsWritable()) {
|
|
217
|
+
if (import.meta.env.DEV) {
|
|
218
|
+
console.log("[Slide] state change", JSON.stringify(state, null, 2));
|
|
219
|
+
}
|
|
198
220
|
this.context.updateAttributes(["state"], state);
|
|
199
221
|
}
|
|
200
222
|
};
|
|
201
223
|
|
|
202
|
-
private
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
224
|
+
private onSeeked = () => {
|
|
225
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
226
|
+
const state = this.context.getAttributes()!.state
|
|
227
|
+
if (state) {
|
|
228
|
+
this.slide.setSlideState(deepClone(state));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
206
231
|
|
|
207
232
|
private pollCount = 0;
|
|
208
233
|
private pollReadyState = () => {
|
|
209
234
|
if (this.ready) {
|
|
210
|
-
this.
|
|
235
|
+
if (this._toFreeze === 1) {
|
|
236
|
+
this.freeze();
|
|
237
|
+
} else if (this._toFreeze === -1) {
|
|
238
|
+
this.unfreeze();
|
|
239
|
+
}
|
|
211
240
|
} else if (this.pollCount < MaxPollCount) {
|
|
212
241
|
this.pollCount++;
|
|
213
242
|
setTimeout(this.pollReadyState, 500);
|
|
@@ -220,12 +249,13 @@ export class SlideController {
|
|
|
220
249
|
}
|
|
221
250
|
};
|
|
222
251
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
252
|
+
// cache `slideCount`, because once the slide is frozen,
|
|
253
|
+
// the `slideCount` will be 0
|
|
254
|
+
private _pageCount = 0;
|
|
227
255
|
public get pageCount() {
|
|
228
|
-
return this.
|
|
256
|
+
if (this._pageCount > 0) return this._pageCount;
|
|
257
|
+
this._pageCount = this.slide.slideCount;
|
|
258
|
+
return this._pageCount;
|
|
229
259
|
}
|
|
230
260
|
|
|
231
261
|
public get page() {
|
|
@@ -245,7 +275,7 @@ export class SlideController {
|
|
|
245
275
|
autoFPS: true,
|
|
246
276
|
autoResolution: true,
|
|
247
277
|
resolution: this.context.getAppOptions()?.resolution,
|
|
248
|
-
transactionBgColor:
|
|
278
|
+
transactionBgColor: this.context.getAppOptions()?.bgColor || cachedGetBgColor(anchor),
|
|
249
279
|
},
|
|
250
280
|
timestamp: this.timestamp,
|
|
251
281
|
});
|
|
@@ -278,4 +308,56 @@ export class SlideController {
|
|
|
278
308
|
return Date.now();
|
|
279
309
|
}
|
|
280
310
|
};
|
|
311
|
+
|
|
312
|
+
public isFrozen = false;
|
|
313
|
+
private _toFreeze: -1 | 0 | 1 = 0; // -1: unfreeze, 0: no change, 1: freeze
|
|
314
|
+
private freezePromise: Promise<void> | null = null;
|
|
315
|
+
|
|
316
|
+
private afterFreeze(from: -1 | 1) {
|
|
317
|
+
if (from === 1) {
|
|
318
|
+
this.isFrozen = true;
|
|
319
|
+
this.freezePromise = null;
|
|
320
|
+
if (this._toFreeze === -1) {
|
|
321
|
+
this.unfreeze();
|
|
322
|
+
}
|
|
323
|
+
} else if (from === -1) {
|
|
324
|
+
this.isFrozen = false;
|
|
325
|
+
this.freezePromise = null;
|
|
326
|
+
if (this._toFreeze === 1) {
|
|
327
|
+
this.freeze();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
public freeze = () => {
|
|
333
|
+
if (this.ready) {
|
|
334
|
+
if (this.debug) {
|
|
335
|
+
console.log("[Slide] freeze", this.context.appId);
|
|
336
|
+
}
|
|
337
|
+
if (this.freezePromise) {
|
|
338
|
+
this._toFreeze = 1;
|
|
339
|
+
} else if (!this.isFrozen) {
|
|
340
|
+
this._toFreeze = 0;
|
|
341
|
+
this.freezePromise = this.slide.frozen().then(this.afterFreeze.bind(this, 1));
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
this._toFreeze = 1;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
public unfreeze = async () => {
|
|
349
|
+
if (this.ready) {
|
|
350
|
+
if (this.debug) {
|
|
351
|
+
console.log("[Slide] unfreeze", this.context.appId);
|
|
352
|
+
}
|
|
353
|
+
if (this.freezePromise) {
|
|
354
|
+
this._toFreeze = -1;
|
|
355
|
+
} else if (this.isFrozen) {
|
|
356
|
+
this._toFreeze = 0;
|
|
357
|
+
this.freezePromise = this.slide.release().then(this.afterFreeze.bind(this, -1));
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
this._toFreeze = -1;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
281
363
|
}
|
|
@@ -26,6 +26,7 @@ export class SlideDocsViewer {
|
|
|
26
26
|
protected readonly whiteboardView: SlideDocsViewerConfig["view"];
|
|
27
27
|
protected readonly mountSlideController: SlideDocsViewerConfig["mountSlideController"];
|
|
28
28
|
protected readonly mountWhiteboard: SlideDocsViewerConfig["mountWhiteboard"];
|
|
29
|
+
private isViewMounted = false;
|
|
29
30
|
|
|
30
31
|
public constructor({ box, view, mountSlideController, mountWhiteboard }: SlideDocsViewerConfig) {
|
|
31
32
|
this.box = box;
|
|
@@ -92,7 +93,6 @@ export class SlideDocsViewer {
|
|
|
92
93
|
if (!this.$whiteboardView) {
|
|
93
94
|
this.$whiteboardView = document.createElement("div");
|
|
94
95
|
this.$whiteboardView.className = this.wrapClassName("wb-view");
|
|
95
|
-
this.mountWhiteboard(this.$whiteboardView);
|
|
96
96
|
}
|
|
97
97
|
return this.$whiteboardView;
|
|
98
98
|
}
|
|
@@ -161,13 +161,20 @@ export class SlideDocsViewer {
|
|
|
161
161
|
protected scaleDocsToFit = () => {
|
|
162
162
|
if (this.slideController) {
|
|
163
163
|
const { width, height } = this.slideController.slide;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
164
|
+
if (width && height) {
|
|
165
|
+
this.whiteboardView.moveCameraToContain({
|
|
166
|
+
originX: -width / 2,
|
|
167
|
+
originY: -height / 2,
|
|
168
|
+
width,
|
|
169
|
+
height,
|
|
170
|
+
animationMode: "immediately" as AnimationMode.Immediately,
|
|
171
|
+
});
|
|
172
|
+
if (!this.isViewMounted) {
|
|
173
|
+
this.isViewMounted = true;
|
|
174
|
+
console.log("[Slide] mount whiteboard view");
|
|
175
|
+
this.mountWhiteboard(this.$whiteboardView);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
171
178
|
}
|
|
172
179
|
};
|
|
173
180
|
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { NetlessApp } from "@netless/window-manager";
|
|
|
2
2
|
import type { RoomState } from "white-web-sdk";
|
|
3
3
|
import type { MountSlideOptions } from "./SlideDocsViewer";
|
|
4
4
|
import type { Attributes } from "./typings";
|
|
5
|
+
import type { AddHooks, FreezableSlide } from "./utils/freezer";
|
|
5
6
|
|
|
6
7
|
import { SideEffectManager } from "side-effect-manager";
|
|
7
8
|
import { ensureAttributes } from "@netless/app-shared";
|
|
@@ -12,11 +13,12 @@ import {
|
|
|
12
13
|
SlideController,
|
|
13
14
|
} from "./SlideController";
|
|
14
15
|
import { SlideDocsViewer } from "./SlideDocsViewer";
|
|
16
|
+
import { apps, FreezerLength, addHooks } from "./utils/freezer";
|
|
15
17
|
import styles from "./style.scss?inline";
|
|
16
18
|
|
|
17
|
-
export type { Attributes };
|
|
19
|
+
export type { Attributes, AddHooks, FreezableSlide };
|
|
18
20
|
|
|
19
|
-
export { DefaultUrl };
|
|
21
|
+
export { DefaultUrl, apps, FreezerLength, addHooks };
|
|
20
22
|
|
|
21
23
|
const SlideApp: NetlessApp<Attributes> = {
|
|
22
24
|
kind: "Slide",
|
|
@@ -48,6 +50,9 @@ const SlideApp: NetlessApp<Attributes> = {
|
|
|
48
50
|
const room = context.getRoom();
|
|
49
51
|
if (docsViewer && docsViewer.slideController && room && context.getIsWritable()) {
|
|
50
52
|
syncSceneWithSlide(room, context, docsViewer.slideController.slide, baseScenePath);
|
|
53
|
+
if (context.getAppOptions()?.debug || import.meta.env.DEV) {
|
|
54
|
+
console.log("[Slide] page to", page);
|
|
55
|
+
}
|
|
51
56
|
docsViewer.viewer.setPageIndex(page - 1);
|
|
52
57
|
}
|
|
53
58
|
};
|
|
@@ -58,6 +63,7 @@ const SlideApp: NetlessApp<Attributes> = {
|
|
|
58
63
|
...options,
|
|
59
64
|
onPageChanged,
|
|
60
65
|
});
|
|
66
|
+
apps.set(context.appId, slideController);
|
|
61
67
|
if (import.meta.env.DEV) {
|
|
62
68
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
69
|
(window as any).slideController = slideController;
|
|
@@ -95,6 +101,7 @@ const SlideApp: NetlessApp<Attributes> = {
|
|
|
95
101
|
|
|
96
102
|
context.emitter.on("destroy", () => {
|
|
97
103
|
console.log("[Slide]: destroy");
|
|
104
|
+
apps.delete(context.appId);
|
|
98
105
|
sideEffect.flushAll();
|
|
99
106
|
if (docsViewer) {
|
|
100
107
|
docsViewer.destroy();
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import ColorString from "color-string";
|
|
2
|
+
|
|
3
|
+
export function guessBgColor(el: HTMLElement): string {
|
|
4
|
+
try {
|
|
5
|
+
const bg = window.getComputedStyle(el).backgroundColor;
|
|
6
|
+
if (bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
|
|
7
|
+
return bg;
|
|
8
|
+
}
|
|
9
|
+
if (el.parentElement) {
|
|
10
|
+
return guessBgColor(el.parentElement);
|
|
11
|
+
}
|
|
12
|
+
} catch {
|
|
13
|
+
// ignore any error
|
|
14
|
+
}
|
|
15
|
+
return "#ffffff";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function toHex(color: string): string {
|
|
19
|
+
const result = ColorString.get(color);
|
|
20
|
+
if (result && result.model === "rgb") {
|
|
21
|
+
const args = result.value;
|
|
22
|
+
|
|
23
|
+
// https://github.com/Qix-/color-convert/blob/8dfdbbc6b46fa6a305bf394d942cc1b08e23fca5/conversions.js#L616
|
|
24
|
+
const integer =
|
|
25
|
+
((Math.round(args[0]) & 0xff) << 16) +
|
|
26
|
+
((Math.round(args[1]) & 0xff) << 8) +
|
|
27
|
+
(Math.round(args[2]) & 0xff);
|
|
28
|
+
|
|
29
|
+
const string = integer.toString(16);
|
|
30
|
+
return "#" + "000000".substring(string.length) + string;
|
|
31
|
+
} else {
|
|
32
|
+
return color;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let cachedBgColor = "";
|
|
37
|
+
|
|
38
|
+
export function cachedGetBgColor(el: HTMLElement): string {
|
|
39
|
+
if (!cachedBgColor) {
|
|
40
|
+
cachedBgColor = toHex(guessBgColor(el));
|
|
41
|
+
if (import.meta.env.DEV) {
|
|
42
|
+
console.log("[Slide] guess bg color", cachedBgColor);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return cachedBgColor;
|
|
46
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { RegisterParams } from "@netless/window-manager";
|
|
2
|
+
|
|
3
|
+
export interface FreezableSlide {
|
|
4
|
+
freeze: () => void;
|
|
5
|
+
unfreeze: () => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const FreezerLength = 2;
|
|
9
|
+
|
|
10
|
+
export const apps = {
|
|
11
|
+
map: new Map<string, FreezableSlide>(),
|
|
12
|
+
queue: [] as string[],
|
|
13
|
+
validateQueue() {
|
|
14
|
+
if (import.meta.env.DEV) {
|
|
15
|
+
console.log("[Slide] freezer: validate", this.queue);
|
|
16
|
+
}
|
|
17
|
+
while (this.queue.length > FreezerLength) {
|
|
18
|
+
const appId = this.queue.pop() as string;
|
|
19
|
+
const slide = this.map.get(appId);
|
|
20
|
+
if (slide) {
|
|
21
|
+
if (import.meta.env.DEV) {
|
|
22
|
+
console.log("[Slide] freezer: validate-freeze", appId, this.queue);
|
|
23
|
+
}
|
|
24
|
+
slide.freeze();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
set(appId: string, slide: FreezableSlide) {
|
|
29
|
+
if (import.meta.env.DEV) {
|
|
30
|
+
console.log("[Slide] freezer: add", appId, this.queue);
|
|
31
|
+
}
|
|
32
|
+
this.map.set(appId, slide);
|
|
33
|
+
if (!this.queue.includes(appId)) {
|
|
34
|
+
this.queue.unshift(appId);
|
|
35
|
+
}
|
|
36
|
+
this.validateQueue();
|
|
37
|
+
},
|
|
38
|
+
delete(appId: string) {
|
|
39
|
+
if (import.meta.env.DEV) {
|
|
40
|
+
console.log("[Slide] freezer: delete", appId, this.queue);
|
|
41
|
+
}
|
|
42
|
+
this.map.delete(appId);
|
|
43
|
+
this.queue = this.queue.filter(id => id !== appId);
|
|
44
|
+
},
|
|
45
|
+
focus(appId: string) {
|
|
46
|
+
const slide = this.map.get(appId);
|
|
47
|
+
const index = this.queue.indexOf(appId);
|
|
48
|
+
if (index > -1) {
|
|
49
|
+
this.queue.splice(index, 1);
|
|
50
|
+
}
|
|
51
|
+
this.queue.unshift(appId);
|
|
52
|
+
this.validateQueue();
|
|
53
|
+
if (import.meta.env.DEV) {
|
|
54
|
+
console.log("[Slide] freezer: focus", appId, this.queue);
|
|
55
|
+
}
|
|
56
|
+
if (slide) {
|
|
57
|
+
slide.unfreeze();
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (import.meta.env.DEV) {
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
(window as any).freezer = apps;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type AddHooks = NonNullable<RegisterParams["addHooks"]>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Put this function in your register code:
|
|
71
|
+
* `WindowManager.register({ addHooks })`
|
|
72
|
+
* So that it will automatically freeze the app when it is not in focus.
|
|
73
|
+
*/
|
|
74
|
+
export const addHooks: AddHooks = emitter => {
|
|
75
|
+
emitter.on("focus", ({ appId }) => {
|
|
76
|
+
apps.focus(appId);
|
|
77
|
+
});
|
|
78
|
+
};
|