@netless/app-slide 0.0.24 → 0.0.28
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 +10 -3
- package/dist/SlideDocsViewer/index.d.ts +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/main.cjs.js +283 -147
- package/dist/main.cjs.js.map +1 -1
- package/dist/main.es.js +2464 -1801
- package/dist/main.es.js.map +1 -1
- package/dist/main.iife.js +277 -141
- 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/helpers.ts +8 -1
- package/src/SlideController/index.ts +81 -12
- package/src/SlideDocsViewer/index.ts +15 -8
- package/src/SlideDocsViewer/style.scss +1 -1
- 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.28",
|
|
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.33",
|
|
15
|
+
"@types/color-string": "^1.5.0",
|
|
16
|
+
"color-string": "^1.6.0",
|
|
15
17
|
"side-effect-manager": "^0.1.5",
|
|
16
18
|
"vanilla-lazyload": "^17.5.0"
|
|
17
19
|
},
|
|
@@ -35,7 +35,14 @@ export function syncSceneWithSlide(
|
|
|
35
35
|
room.putScenes(baseScenePath, scenes);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
let currentScenePath: string;
|
|
39
|
+
if (context.getBox().focus) {
|
|
40
|
+
currentScenePath = room.state.sceneState.scenePath;
|
|
41
|
+
} else {
|
|
42
|
+
currentScenePath = context.getView()?.focusScenePath || "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (currentScenePath !== scenePath) {
|
|
39
46
|
context.setScenePath(scenePath);
|
|
40
47
|
}
|
|
41
48
|
}
|
|
@@ -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
|
|
@@ -148,6 +160,8 @@ export class SlideController {
|
|
|
148
160
|
slide.on(SLIDE_EVENTS.renderError, this.onError);
|
|
149
161
|
slide.on(SLIDE_EVENTS.stateChange, this.onStateChange);
|
|
150
162
|
slide.on(SLIDE_EVENTS.syncDispatch, this.onSyncDispatch);
|
|
163
|
+
|
|
164
|
+
slide.on(SLIDE_EVENTS.renderEnd, this.resolveReady);
|
|
151
165
|
}
|
|
152
166
|
|
|
153
167
|
private onSyncDispatch = (event: SyncEvent) => {
|
|
@@ -195,19 +209,21 @@ export class SlideController {
|
|
|
195
209
|
|
|
196
210
|
private onStateChange = (state: SlideState) => {
|
|
197
211
|
if (this.context.getIsWritable()) {
|
|
212
|
+
if (import.meta.env.DEV) {
|
|
213
|
+
console.log("[Slide] state change", JSON.stringify(state, null, 2));
|
|
214
|
+
}
|
|
198
215
|
this.context.updateAttributes(["state"], state);
|
|
199
216
|
}
|
|
200
217
|
};
|
|
201
218
|
|
|
202
|
-
private resolveReady!: () => void;
|
|
203
|
-
public readonly readyPromise = new Promise<void>(resolve => {
|
|
204
|
-
this.resolveReady = resolve;
|
|
205
|
-
});
|
|
206
|
-
|
|
207
219
|
private pollCount = 0;
|
|
208
220
|
private pollReadyState = () => {
|
|
209
221
|
if (this.ready) {
|
|
210
|
-
this.
|
|
222
|
+
if (this._toFreeze === 1) {
|
|
223
|
+
this.freeze();
|
|
224
|
+
} else if (this._toFreeze === -1) {
|
|
225
|
+
this.unfreeze();
|
|
226
|
+
}
|
|
211
227
|
} else if (this.pollCount < MaxPollCount) {
|
|
212
228
|
this.pollCount++;
|
|
213
229
|
setTimeout(this.pollReadyState, 500);
|
|
@@ -220,12 +236,13 @@ export class SlideController {
|
|
|
220
236
|
}
|
|
221
237
|
};
|
|
222
238
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
239
|
+
// cache `slideCount`, because once the slide is frozen,
|
|
240
|
+
// the `slideCount` will be 0
|
|
241
|
+
private _pageCount = 0;
|
|
227
242
|
public get pageCount() {
|
|
228
|
-
return this.
|
|
243
|
+
if (this._pageCount > 0) return this._pageCount;
|
|
244
|
+
this._pageCount = this.slide.slideCount;
|
|
245
|
+
return this._pageCount;
|
|
229
246
|
}
|
|
230
247
|
|
|
231
248
|
public get page() {
|
|
@@ -245,7 +262,7 @@ export class SlideController {
|
|
|
245
262
|
autoFPS: true,
|
|
246
263
|
autoResolution: true,
|
|
247
264
|
resolution: this.context.getAppOptions()?.resolution,
|
|
248
|
-
transactionBgColor:
|
|
265
|
+
transactionBgColor: this.context.getAppOptions()?.bgColor || cachedGetBgColor(anchor),
|
|
249
266
|
},
|
|
250
267
|
timestamp: this.timestamp,
|
|
251
268
|
});
|
|
@@ -278,4 +295,56 @@ export class SlideController {
|
|
|
278
295
|
return Date.now();
|
|
279
296
|
}
|
|
280
297
|
};
|
|
298
|
+
|
|
299
|
+
public isFrozen = false;
|
|
300
|
+
private _toFreeze: -1 | 0 | 1 = 0; // -1: unfreeze, 0: no change, 1: freeze
|
|
301
|
+
private freezePromise: Promise<void> | null = null;
|
|
302
|
+
|
|
303
|
+
private afterFreeze(from: -1 | 1) {
|
|
304
|
+
if (from === 1) {
|
|
305
|
+
this.isFrozen = true;
|
|
306
|
+
this.freezePromise = null;
|
|
307
|
+
if (this._toFreeze === -1) {
|
|
308
|
+
this.unfreeze();
|
|
309
|
+
}
|
|
310
|
+
} else if (from === -1) {
|
|
311
|
+
this.isFrozen = false;
|
|
312
|
+
this.freezePromise = null;
|
|
313
|
+
if (this._toFreeze === 1) {
|
|
314
|
+
this.freeze();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
public freeze = () => {
|
|
320
|
+
if (this.ready) {
|
|
321
|
+
if (this.debug) {
|
|
322
|
+
console.log("[Slide] freeze", this.context.appId);
|
|
323
|
+
}
|
|
324
|
+
if (this.freezePromise) {
|
|
325
|
+
this._toFreeze = 1;
|
|
326
|
+
} else if (!this.isFrozen) {
|
|
327
|
+
this._toFreeze = 0;
|
|
328
|
+
this.freezePromise = this.slide.frozen().then(this.afterFreeze.bind(this, 1));
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
this._toFreeze = 1;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
public unfreeze = async () => {
|
|
336
|
+
if (this.ready) {
|
|
337
|
+
if (this.debug) {
|
|
338
|
+
console.log("[Slide] unfreeze", this.context.appId);
|
|
339
|
+
}
|
|
340
|
+
if (this.freezePromise) {
|
|
341
|
+
this._toFreeze = -1;
|
|
342
|
+
} else if (this.isFrozen) {
|
|
343
|
+
this._toFreeze = 0;
|
|
344
|
+
this.freezePromise = this.slide.release().then(this.afterFreeze.bind(this, -1));
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
this._toFreeze = -1;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
281
350
|
}
|
|
@@ -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
|
+
};
|