@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.
@@ -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.25",
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.30",
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 resolveReady!: () => void;
203
- public readonly readyPromise = new Promise<void>(resolve => {
204
- this.resolveReady = resolve;
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.resolveReady();
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
- public get ready() {
224
- return this.slide.slideCount > 0;
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.slide.slideCount;
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: "#f9f9fc",
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
- 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
 
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
+ };