@netless/app-slide 0.0.27 → 0.0.32

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,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;
@@ -0,0 +1,20 @@
1
+ import type { SlideController } from "../SlideController";
2
+ declare class Logger {
3
+ enable: boolean;
4
+ apps: Record<string, {
5
+ context?: any;
6
+ controller?: SlideController;
7
+ }>;
8
+ level: "debug" | "verbose";
9
+ constructor(enable: boolean);
10
+ log(...args: Parameters<Console["log"]>): void;
11
+ verbose(...args: Parameters<Console["log"]>): void;
12
+ dispose(appId: string): void;
13
+ _onMessage: (ev: MessageEvent<{
14
+ slide: boolean | "__instance";
15
+ }>) => void;
16
+ }
17
+ export declare const logger: Logger;
18
+ export declare const log: (...args: Parameters<Console["log"]>) => void;
19
+ export declare const verbose: (...args: Parameters<Console["log"]>) => void;
20
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netless/app-slide",
3
- "version": "0.0.27",
3
+ "version": "0.0.32",
4
4
  "main": "dist/main.cjs.js",
5
5
  "module": "dist/main.es.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,9 +11,9 @@
11
11
  ],
12
12
  "devDependencies": {
13
13
  "@netless/app-shared": "^0.1.1",
14
- "@netless/slide": "^0.1.32",
15
- "@types/color-string": "^1.5.0",
16
- "color-string": "^1.6.0",
14
+ "@netless/slide": "^0.1.37",
15
+ "@types/color-string": "^1.5.1",
16
+ "color-string": "^1.7.4",
17
17
  "side-effect-manager": "^0.1.5",
18
18
  "vanilla-lazyload": "^17.5.0"
19
19
  },
@@ -17,6 +17,7 @@ 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
19
  import { cachedGetBgColor } from "../utils/bgcolor";
20
+ import { logger, log, verbose } from "../utils/logger";
20
21
  export { syncSceneWithSlide, createDocsViewerPages } from "./helpers";
21
22
 
22
23
  export const DefaultUrl = "https://convertcdn.netless.link/dynamicConvert";
@@ -39,7 +40,6 @@ export interface SlideControllerOptions {
39
40
  export class SlideController {
40
41
  public readonly context: SlideControllerOptions["context"];
41
42
  public readonly slide: Slide;
42
- public readonly debug: boolean;
43
43
 
44
44
  private readonly channel: string;
45
45
  private readonly room?: Room;
@@ -69,13 +69,22 @@ export class SlideController {
69
69
  this.channel = `channel-${context.appId}`;
70
70
  this.room = context.getRoom();
71
71
  this.player = this.room ? undefined : (context.getDisplayer() as Player);
72
- this.debug = import.meta.env.DEV || !!context.getAppOptions()?.debug;
73
72
  this.slide = this.createSlide(anchor);
74
73
  // the adder does not need to sync state
75
74
  this.syncStateOnceFlag = !this.context.isAddApp;
76
75
  this.initialize();
77
76
  }
78
77
 
78
+ public ready = false;
79
+ private resolveReady!: () => void;
80
+ public readonly readyPromise = new Promise<void>(resolve => {
81
+ this.resolveReady = () =>
82
+ setTimeout(() => {
83
+ this.ready = true;
84
+ resolve();
85
+ }, 1000);
86
+ });
87
+
79
88
  public jumpToPage(page: number) {
80
89
  if (this.ready) {
81
90
  page = clamp(page, 1, this.pageCount);
@@ -94,16 +103,12 @@ export class SlideController {
94
103
  slide.setResource(taskId, url || DefaultUrl);
95
104
  if (state) {
96
105
  // if we already have state, try restore from it
97
- if (this.debug) {
98
- console.log("[Slide] init with state", deepClone(state));
99
- }
106
+ log("[Slide] init with state", deepClone(state));
100
107
  this.syncStateOnceFlag = false;
101
108
  slide.setSlideState(deepClone(state));
102
109
  } else if (context.isAddApp) {
103
110
  // otherwise, maybe this slide is just added, let the adder kick start first render
104
- if (this.debug) {
105
- console.log("[Slide] init by renderSlide", 1);
106
- }
111
+ log("[Slide] init by renderSlide", 1);
107
112
  slide.renderSlide(1);
108
113
  }
109
114
  // there's still some risk that the adder is left and no first render
@@ -132,6 +137,11 @@ export class SlideController {
132
137
  return disposer;
133
138
  });
134
139
 
140
+ this.sideEffect.add(() => {
141
+ context.emitter.on("seek", this.onSeeked);
142
+ return () => context.emitter.off("seek", this.onSeeked);
143
+ });
144
+
135
145
  this.sideEffect.add(() => {
136
146
  const displayer = context.getDisplayer();
137
147
  displayer.addMagixEventListener(this.channel, this.magixEventListener, {
@@ -150,6 +160,8 @@ export class SlideController {
150
160
  slide.on(SLIDE_EVENTS.renderError, this.onError);
151
161
  slide.on(SLIDE_EVENTS.stateChange, this.onStateChange);
152
162
  slide.on(SLIDE_EVENTS.syncDispatch, this.onSyncDispatch);
163
+
164
+ slide.on(SLIDE_EVENTS.renderEnd, this.resolveReady);
153
165
  }
154
166
 
155
167
  private onSyncDispatch = (event: SyncEvent) => {
@@ -158,9 +170,7 @@ export class SlideController {
158
170
  type: SLIDE_EVENTS.syncDispatch,
159
171
  payload: event,
160
172
  };
161
- if (this.debug) {
162
- console.log("[Slide] dispatch", event);
163
- }
173
+ log("[Slide] dispatch", event);
164
174
  this.room.dispatchMagixEvent(this.channel, payload);
165
175
  }
166
176
  };
@@ -170,9 +180,7 @@ export class SlideController {
170
180
  const { type, payload } = ev.payload as MagixPayload;
171
181
  if (type === SLIDE_EVENTS.syncDispatch) {
172
182
  this.syncStateOnce();
173
- if (this.debug) {
174
- console.log("[Slide] receive", payload);
175
- }
183
+ log("[Slide] receive", payload);
176
184
  // only emit the event if the slide state is sync-ed
177
185
  if (!this.syncStateOnceFlag) {
178
186
  this.slide.emit(SLIDE_EVENTS.syncReceive, payload);
@@ -186,9 +194,7 @@ export class SlideController {
186
194
  if (this.syncStateOnceFlag) {
187
195
  const { state } = { ...EmptyAttributes, ...this.context.getAttributes() };
188
196
  if (state) {
189
- if (this.debug) {
190
- console.log("[Slide] sync with state (once)", deepClone(state));
191
- }
197
+ log("[Slide] sync with state (once)", deepClone(state));
192
198
  this.slide.setSlideState(deepClone(state));
193
199
  this.syncStateOnceFlag = false;
194
200
  }
@@ -197,40 +203,44 @@ export class SlideController {
197
203
 
198
204
  private onStateChange = (state: SlideState) => {
199
205
  if (this.context.getIsWritable()) {
200
- if (import.meta.env.DEV) {
201
- console.log("[Slide] state change", JSON.stringify(state, null, 2));
202
- }
206
+ verbose("[Slide] state change", JSON.stringify(state, null, 2));
203
207
  this.context.updateAttributes(["state"], state);
204
208
  }
205
209
  };
206
210
 
207
- private resolveReady!: () => void;
208
- public readonly readyPromise = new Promise<void>(resolve => {
209
- this.resolveReady = resolve;
210
- });
211
+ private onSeeked = () => {
212
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
213
+ const state = this.context.getAttributes()!.state;
214
+ if (state) {
215
+ this.slide.setSlideState(deepClone(state));
216
+ }
217
+ };
211
218
 
212
219
  private pollCount = 0;
213
220
  private pollReadyState = () => {
214
221
  if (this.ready) {
215
- this.resolveReady();
222
+ if (this._toFreeze === 1) {
223
+ this.freeze();
224
+ } else if (this._toFreeze === -1) {
225
+ this.unfreeze();
226
+ }
216
227
  } else if (this.pollCount < MaxPollCount) {
217
228
  this.pollCount++;
218
229
  setTimeout(this.pollReadyState, 500);
219
230
  } else {
220
231
  this.pollCount = 0;
221
- if (this.debug) {
222
- console.log("[Slide] renderSlide (retry after timeout)", 1);
223
- }
232
+ log("[Slide] renderSlide (retry after timeout)", 1);
224
233
  this.slide.renderSlide(1);
225
234
  }
226
235
  };
227
236
 
228
- public get ready() {
229
- return this.slide.slideCount > 0;
230
- }
231
-
237
+ // cache `slideCount`, because once the slide is frozen,
238
+ // the `slideCount` will be 0
239
+ private _pageCount = 0;
232
240
  public get pageCount() {
233
- return this.slide.slideCount;
241
+ if (this._pageCount > 0) return this._pageCount;
242
+ this._pageCount = this.slide.slideCount;
243
+ return this._pageCount;
234
244
  }
235
245
 
236
246
  public get page() {
@@ -243,7 +253,7 @@ export class SlideController {
243
253
  interactive: true,
244
254
  mode: "interactive",
245
255
  resize: true,
246
- controller: this.debug,
256
+ controller: logger.enable,
247
257
  renderOptions: {
248
258
  minFPS: 25,
249
259
  maxFPS: 30,
@@ -254,7 +264,7 @@ export class SlideController {
254
264
  },
255
265
  timestamp: this.timestamp,
256
266
  });
257
- if (this.debug) {
267
+ if (import.meta.env.DEV) {
258
268
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
259
269
  (window as any).slide = slide;
260
270
  }
@@ -266,9 +276,7 @@ export class SlideController {
266
276
  public destroy() {
267
277
  this.sideEffect.flushAll();
268
278
  if (!this.destroyed) {
269
- if (this.debug) {
270
- console.log("[Slide] destroy slide (once)");
271
- }
279
+ log("[Slide] destroy slide (once)");
272
280
  this.slide.destroy();
273
281
  this.destroyed = true;
274
282
  }
@@ -283,4 +291,52 @@ export class SlideController {
283
291
  return Date.now();
284
292
  }
285
293
  };
294
+
295
+ public isFrozen = false;
296
+ private _toFreeze: -1 | 0 | 1 = 0; // -1: unfreeze, 0: no change, 1: freeze
297
+ private freezePromise: Promise<void> | null = null;
298
+
299
+ private afterFreeze(from: -1 | 1) {
300
+ if (from === 1) {
301
+ this.isFrozen = true;
302
+ this.freezePromise = null;
303
+ if (this._toFreeze === -1) {
304
+ this.unfreeze();
305
+ }
306
+ } else if (from === -1) {
307
+ this.isFrozen = false;
308
+ this.freezePromise = null;
309
+ if (this._toFreeze === 1) {
310
+ this.freeze();
311
+ }
312
+ }
313
+ }
314
+
315
+ public freeze = () => {
316
+ if (this.ready) {
317
+ log("[Slide] freeze", this.context.appId);
318
+ if (this.freezePromise) {
319
+ this._toFreeze = 1;
320
+ } else if (!this.isFrozen) {
321
+ this._toFreeze = 0;
322
+ this.freezePromise = this.slide.frozen().then(this.afterFreeze.bind(this, 1));
323
+ }
324
+ } else {
325
+ this._toFreeze = 1;
326
+ }
327
+ };
328
+
329
+ public unfreeze = async () => {
330
+ if (this.ready) {
331
+ log("[Slide] unfreeze", this.context.appId);
332
+ if (this.freezePromise) {
333
+ this._toFreeze = -1;
334
+ } else if (this.isFrozen) {
335
+ this._toFreeze = 0;
336
+ this.freezePromise = this.slide.release().then(this.afterFreeze.bind(this, -1));
337
+ }
338
+ } else {
339
+ this._toFreeze = -1;
340
+ }
341
+ };
286
342
  }
@@ -5,7 +5,7 @@ import { SideEffectManager } from "side-effect-manager";
5
5
  import { createDocsViewerPages } from "../SlideController";
6
6
  import { DocsViewer } from "../DocsViewer";
7
7
 
8
- export const ClickThroughAppliances = new Set(["clicker", "selector"]);
8
+ export const ClickThroughAppliances = new Set(["clicker"]);
9
9
 
10
10
  export type MountSlideOptions = Omit<SlideControllerOptions, "context" | "onPageChanged"> & {
11
11
  onReady: () => void;
@@ -161,17 +161,19 @@ 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
- });
171
- if (!this.isViewMounted && width && height) {
172
- this.isViewMounted = true;
173
- console.log("[Slide] mount whiteboard view");
174
- this.mountWhiteboard(this.$whiteboardView);
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
+ }
175
177
  }
176
178
  }
177
179
  };
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,13 @@ import {
12
13
  SlideController,
13
14
  } from "./SlideController";
14
15
  import { SlideDocsViewer } from "./SlideDocsViewer";
16
+ import { apps, FreezerLength, addHooks } from "./utils/freezer";
17
+ import { log, logger } from "./utils/logger";
15
18
  import styles from "./style.scss?inline";
16
19
 
17
- export type { Attributes };
20
+ export type { Attributes, AddHooks, FreezableSlide };
18
21
 
19
- export { DefaultUrl };
22
+ export { DefaultUrl, apps, FreezerLength, addHooks };
20
23
 
21
24
  const SlideApp: NetlessApp<Attributes> = {
22
25
  kind: "Slide",
@@ -46,8 +49,13 @@ const SlideApp: NetlessApp<Attributes> = {
46
49
 
47
50
  const onPageChanged = (page: number) => {
48
51
  const room = context.getRoom();
49
- if (docsViewer && docsViewer.slideController && room && context.getIsWritable()) {
50
- syncSceneWithSlide(room, context, docsViewer.slideController.slide, baseScenePath);
52
+ if (docsViewer && docsViewer.slideController) {
53
+ let synced = false;
54
+ if (room && context.getIsWritable()) {
55
+ syncSceneWithSlide(room, context, docsViewer.slideController.slide, baseScenePath);
56
+ synced = true;
57
+ }
58
+ log("[Slide] page to", page, synced);
51
59
  docsViewer.viewer.setPageIndex(page - 1);
52
60
  }
53
61
  };
@@ -58,6 +66,8 @@ const SlideApp: NetlessApp<Attributes> = {
58
66
  ...options,
59
67
  onPageChanged,
60
68
  });
69
+ apps.set(context.appId, slideController);
70
+ (logger.apps[context.appId] ||= {}).controller = slideController;
61
71
  if (import.meta.env.DEV) {
62
72
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
73
  (window as any).slideController = slideController;
@@ -80,6 +90,14 @@ const SlideApp: NetlessApp<Attributes> = {
80
90
 
81
91
  const room = context.getRoom();
82
92
  const sideEffect = new SideEffectManager();
93
+
94
+ sideEffect.add(() => {
95
+ (logger.apps[context.appId] ||= {}).context = context;
96
+ logger.enable = context.getAppOptions()?.debug || import.meta.env.DEV;
97
+ logger.level = import.meta.env.DEV ? "verbose" : "debug";
98
+ return () => logger.dispose(context.appId);
99
+ });
100
+
83
101
  if (room) {
84
102
  docsViewer.toggleClickThrough(room.state.memberState.currentApplianceName);
85
103
  sideEffect.add(() => {
@@ -94,7 +112,8 @@ const SlideApp: NetlessApp<Attributes> = {
94
112
  }
95
113
 
96
114
  context.emitter.on("destroy", () => {
97
- console.log("[Slide]: destroy");
115
+ log("[Slide]: destroy");
116
+ apps.delete(context.appId);
98
117
  sideEffect.flushAll();
99
118
  if (docsViewer) {
100
119
  docsViewer.destroy();
@@ -1,4 +1,5 @@
1
1
  import ColorString from "color-string";
2
+ import { log } from "./logger";
2
3
 
3
4
  export function guessBgColor(el: HTMLElement): string {
4
5
  try {
@@ -38,9 +39,7 @@ let cachedBgColor = "";
38
39
  export function cachedGetBgColor(el: HTMLElement): string {
39
40
  if (!cachedBgColor) {
40
41
  cachedBgColor = toHex(guessBgColor(el));
41
- if (import.meta.env.DEV) {
42
- console.log("[Slide] guess bg color", cachedBgColor);
43
- }
42
+ log("[Slide] guess bg color", cachedBgColor);
44
43
  }
45
44
  return cachedBgColor;
46
45
  }
@@ -0,0 +1,69 @@
1
+ import type { RegisterParams } from "@netless/window-manager";
2
+ import { log } from "./logger";
3
+
4
+ export interface FreezableSlide {
5
+ freeze: () => void;
6
+ unfreeze: () => void;
7
+ }
8
+
9
+ export const FreezerLength = 2;
10
+
11
+ export const apps = {
12
+ map: new Map<string, FreezableSlide>(),
13
+ queue: [] as string[],
14
+ validateQueue() {
15
+ log("[Slide] freezer: validate", this.queue);
16
+ while (this.queue.length > FreezerLength) {
17
+ const appId = this.queue.pop() as string;
18
+ const slide = this.map.get(appId);
19
+ if (slide) {
20
+ log("[Slide] freezer: validate-freeze", appId, this.queue);
21
+ slide.freeze();
22
+ }
23
+ }
24
+ },
25
+ set(appId: string, slide: FreezableSlide) {
26
+ log("[Slide] freezer: add", appId, this.queue);
27
+ this.map.set(appId, slide);
28
+ if (!this.queue.includes(appId)) {
29
+ this.queue.unshift(appId);
30
+ }
31
+ this.validateQueue();
32
+ },
33
+ delete(appId: string) {
34
+ log("[Slide] freezer: delete", appId, this.queue);
35
+ this.map.delete(appId);
36
+ this.queue = this.queue.filter(id => id !== appId);
37
+ },
38
+ focus(appId: string) {
39
+ const slide = this.map.get(appId);
40
+ const index = this.queue.indexOf(appId);
41
+ if (index > -1) {
42
+ this.queue.splice(index, 1);
43
+ }
44
+ this.queue.unshift(appId);
45
+ this.validateQueue();
46
+ log("[Slide] freezer: focus", appId, this.queue);
47
+ if (slide) {
48
+ slide.unfreeze();
49
+ }
50
+ },
51
+ };
52
+
53
+ if (import.meta.env.DEV) {
54
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
+ (window as any).freezer = apps;
56
+ }
57
+
58
+ export type AddHooks = NonNullable<RegisterParams["addHooks"]>;
59
+
60
+ /**
61
+ * Put this function in your register code:
62
+ * `WindowManager.register({ addHooks })`
63
+ * So that it will automatically freeze the app when it is not in focus.
64
+ */
65
+ export const addHooks: AddHooks = emitter => {
66
+ emitter.on("focus", ({ appId }) => {
67
+ apps.focus(appId);
68
+ });
69
+ };
@@ -0,0 +1,39 @@
1
+ import type { SlideController } from "../SlideController";
2
+ import { isObj } from "./helpers";
3
+
4
+ class Logger {
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ public apps: Record<string, { context?: any; controller?: SlideController }> = {};
7
+ public level: "debug" | "verbose" = import.meta.env.DEV ? "verbose" : "debug";
8
+ constructor(public enable: boolean) {
9
+ window.addEventListener("message", this._onMessage);
10
+ }
11
+ log(...args: Parameters<Console["log"]>) {
12
+ if (this.enable) {
13
+ console.log(...args);
14
+ }
15
+ }
16
+ verbose(...args: Parameters<Console["log"]>) {
17
+ if (this.enable && this.level === "verbose") {
18
+ console.log(...args);
19
+ }
20
+ }
21
+ dispose(appId: string) {
22
+ this.enable = false;
23
+ delete this.apps[appId];
24
+ window.removeEventListener("message", this._onMessage);
25
+ }
26
+ _onMessage = (ev: MessageEvent<{ slide: boolean | "__instance" }>) => {
27
+ if (isObj(ev.data)) {
28
+ if (typeof ev.data.slide === "boolean") {
29
+ this.enable = ev.data.slide;
30
+ } else if (ev.data.slide === "__instance") {
31
+ console.log(this);
32
+ }
33
+ }
34
+ };
35
+ }
36
+
37
+ export const logger = new Logger(import.meta.env.DEV);
38
+ export const log = logger.log.bind(logger);
39
+ export const verbose = logger.verbose.bind(logger);