@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.
@@ -0,0 +1,3 @@
1
+ export declare function guessBgColor(el: HTMLElement): string;
2
+ export declare function toHex(color: string): string;
3
+ export declare function cachedGetBgColor(el: HTMLElement): string;
@@ -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.24",
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.29",
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
- if (context.getView()?.focusScenePath !== scenePath) {
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.resolveReady();
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
- public get ready() {
224
- return this.slide.slideCount > 0;
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.slide.slideCount;
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: "#f9f9fc",
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
- this.whiteboardView.moveCameraToContain({
165
- originX: -width / 2,
166
- originY: -height / 2,
167
- width,
168
- height,
169
- animationMode: "immediately" as AnimationMode.Immediately,
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
 
@@ -6,6 +6,6 @@ $namespace: "netless-app-slide";
6
6
  left: 0;
7
7
  width: 100%;
8
8
  height: 100%;
9
- z-index: 100;
9
+ z-index: 1001;
10
10
  overflow: hidden;
11
11
  }
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
+ };