@netless/app-slide 0.2.2 → 0.3.0-canary.0

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.
Files changed (58) hide show
  1. package/README-zh.md +1 -4
  2. package/README.md +1 -4
  3. package/dist/Refrigerator.d.ts +13 -0
  4. package/dist/SlideViewer.d.ts +63 -0
  5. package/dist/constants.d.ts +2 -0
  6. package/dist/{DocsViewer/icons → icons}/arrow-left.d.ts +0 -0
  7. package/dist/{DocsViewer/icons → icons}/arrow-right.d.ts +0 -0
  8. package/dist/{DocsViewer/icons → icons}/pause.d.ts +0 -0
  9. package/dist/{DocsViewer/icons → icons}/play.d.ts +0 -0
  10. package/dist/{DocsViewer/icons → icons}/sidebar.d.ts +0 -0
  11. package/dist/index.d.ts +10 -35
  12. package/dist/main.cjs.js +177 -177
  13. package/dist/main.cjs.js.map +1 -1
  14. package/dist/main.es.js +1901 -2479
  15. package/dist/main.es.js.map +1 -1
  16. package/dist/main.iife.js +254 -254
  17. package/dist/main.iife.js.map +1 -1
  18. package/dist/typings.d.ts +21 -9
  19. package/dist/utils.d.ts +1 -0
  20. package/package.json +2 -2
  21. package/src/Refrigerator.ts +66 -0
  22. package/src/SlideViewer.ts +543 -0
  23. package/src/constants.ts +3 -0
  24. package/src/{DocsViewer/icons → icons}/arrow-left.ts +0 -0
  25. package/src/{DocsViewer/icons → icons}/arrow-right.ts +0 -0
  26. package/src/{DocsViewer/icons → icons}/pause.ts +0 -0
  27. package/src/{DocsViewer/icons → icons}/play.ts +0 -0
  28. package/src/{DocsViewer/icons → icons}/sidebar.ts +0 -0
  29. package/src/index.ts +210 -205
  30. package/src/style.scss +245 -5
  31. package/src/typings.ts +23 -10
  32. package/src/utils.ts +3 -0
  33. package/dist/DocsViewer/index.d.ts +0 -53
  34. package/dist/SlideController/helpers.d.ts +0 -6
  35. package/dist/SlideController/index.d.ts +0 -60
  36. package/dist/SlideDocsViewer/index.d.ts +0 -45
  37. package/dist/SlidePreviewer/index.d.ts +0 -52
  38. package/dist/utils/bgcolor.d.ts +0 -3
  39. package/dist/utils/freezer.d.ts +0 -22
  40. package/dist/utils/helpers.d.ts +0 -6
  41. package/dist/utils/logger.d.ts +0 -39
  42. package/src/DocsViewer/icons/arrow-left.svg +0 -4
  43. package/src/DocsViewer/icons/arrow-right.svg +0 -4
  44. package/src/DocsViewer/icons/pause.svg +0 -4
  45. package/src/DocsViewer/icons/play.svg +0 -4
  46. package/src/DocsViewer/icons/sidebar.svg +0 -4
  47. package/src/DocsViewer/index.ts +0 -368
  48. package/src/DocsViewer/style.scss +0 -228
  49. package/src/DocsViewer/variables.scss +0 -1
  50. package/src/SlideController/helpers.ts +0 -48
  51. package/src/SlideController/index.ts +0 -356
  52. package/src/SlideDocsViewer/index.ts +0 -210
  53. package/src/SlideDocsViewer/style.scss +0 -11
  54. package/src/SlidePreviewer/index.ts +0 -226
  55. package/src/utils/bgcolor.ts +0 -45
  56. package/src/utils/freezer.ts +0 -75
  57. package/src/utils/helpers.ts +0 -26
  58. package/src/utils/logger.ts +0 -106
@@ -1,356 +0,0 @@
1
- // this controller does these things:
2
- // 1. map slide events to ui
3
- // 2. make sure to init correctly
4
- // - the one with (context.isAddApp === true) should call renderSlide(1)
5
- // - others wait for first sync event and restore from sync state
6
- // - if none of above happen, force doing renderSlide(1) after timeout
7
- // 3. send/receive slide sync events
8
- // 4. automatically re-create scenes to sync strokes, a view must be existing
9
- // 5. pages information are loaded dynamically by the slide package
10
-
11
- import type { AppContext, Player, Room, Displayer } from "@netless/window-manager";
12
- import type { SyncEvent } from "@netless/slide";
13
- import type { Attributes, MagixEvents, MagixPayload, SlideState } from "../typings";
14
- import type { AppOptions } from "..";
15
-
16
- import { SideEffectManager } from "side-effect-manager";
17
- import { Slide, SLIDE_EVENTS } from "@netless/slide";
18
- import { clamp } from "../utils/helpers";
19
- import { cachedGetBgColor } from "../utils/bgcolor";
20
- import { logger, log, verbose, setRoomLogger } from "../utils/logger";
21
- export { syncSceneWithSlide, createDocsViewerPages } from "./helpers";
22
-
23
- export const DefaultUrl = "https://convertcdn.netless.link/dynamicConvert";
24
- export const MaxPollCount = 40; // 500ms * 40 times = 20s
25
- export const EmptyAttributes: Attributes = {
26
- taskId: "",
27
- url: "",
28
- state: null,
29
- };
30
-
31
- export interface SlideControllerOptions {
32
- context: AppContext<Attributes, MagixEvents, AppOptions>;
33
- anchor: HTMLDivElement;
34
- onPageChanged: (page: number) => void;
35
- onTransitionStart: () => void;
36
- onTransitionEnd: () => void;
37
- onError: (args: { error: Error }) => void;
38
- }
39
-
40
- type MagixEventListener = Parameters<
41
- AppContext<Attributes, MagixEvents>["addMagixEventListener"]
42
- >[1];
43
-
44
- export class SlideController {
45
- public readonly context: SlideControllerOptions["context"];
46
- public readonly slide: Slide;
47
-
48
- private readonly room?: Room;
49
- private readonly player?: Player;
50
- private readonly sideEffect = new SideEffectManager();
51
-
52
- private readonly onPageChanged: SlideControllerOptions["onPageChanged"];
53
- private readonly onTransitionStart: SlideControllerOptions["onTransitionStart"];
54
- private readonly onTransitionEnd: SlideControllerOptions["onTransitionEnd"];
55
- private readonly onError: SlideControllerOptions["onError"];
56
-
57
- private syncStateOnceFlag: boolean;
58
-
59
- private visible: boolean;
60
- private savedIsFrozen: boolean;
61
-
62
- public constructor({
63
- context,
64
- anchor,
65
- onPageChanged,
66
- onTransitionStart,
67
- onTransitionEnd,
68
- onError,
69
- }: SlideControllerOptions) {
70
- this.onPageChanged = onPageChanged;
71
- this.onTransitionStart = onTransitionStart;
72
- this.onTransitionEnd = onTransitionEnd;
73
- this.onError = onError;
74
-
75
- this.context = context;
76
- this.room = context.getRoom();
77
- this.player = this.room ? undefined : (context.getDisplayer() as Player);
78
- setRoomLogger((this.room || this.player) as Displayer);
79
- this.slide = this.createSlide(anchor);
80
-
81
- // the adder does not need to sync state
82
- this.syncStateOnceFlag = !this.context.isAddApp;
83
- this.visible = document.visibilityState === "visible";
84
- this.savedIsFrozen = false;
85
- this.initialize();
86
- }
87
-
88
- public ready = false;
89
- private resolveReady!: () => void;
90
- public readonly readyPromise = new Promise<void>(resolve => {
91
- this.resolveReady = () => {
92
- if (this.ready) {
93
- log("[Slide] render end");
94
- } else {
95
- setTimeout(() => {
96
- this.ready = true;
97
- resolve();
98
- }, 1000);
99
- }
100
- };
101
- });
102
-
103
- public jumpToPage(page: number) {
104
- if (this.ready) {
105
- page = clamp(page, 1, this.pageCount);
106
- this.slide.renderSlide(page);
107
- }
108
- }
109
-
110
- private initialize() {
111
- this.registerEventListeners();
112
- this.kickStart();
113
- }
114
-
115
- private kickStart() {
116
- const { context, slide } = this;
117
- if (context.getIsWritable()) {
118
- context.storage.ensureState(EmptyAttributes);
119
- }
120
- const { taskId, url, state } = context.storage.state;
121
- slide.setResource(taskId, url || DefaultUrl);
122
- if (state) {
123
- // if we already have state, try restore from it
124
- log("[Slide] init with state", JSON.stringify(state));
125
- this.syncStateOnceFlag = false;
126
- slide.setSlideState(state);
127
- } else if (context.isAddApp) {
128
- // otherwise, maybe this slide is just added, let the adder kick start first render
129
- log("[Slide] init by renderSlide", 1);
130
- slide.renderSlide(1);
131
- }
132
- // there's still some risk that the adder is left and no first render
133
- // so anyway, we start polling the slide's "ready state"
134
- // if in the next 20 seconds the slide is not ready, start render first page
135
- this.pollReadyState();
136
- }
137
-
138
- private registerEventListeners() {
139
- const { context, slide } = this;
140
-
141
- // it is possible that we miss the first `renderSlide(1)` event
142
- // and the attributes has no value yet, so we need to sync state
143
- // when there's state change. we only have to do it once
144
- const disposerId = this.sideEffect.addDisposer(
145
- context.storage.addStateChangedListener(() => {
146
- if (context.storage.state.state) {
147
- this.syncStateOnce();
148
- this.sideEffect.flush(disposerId);
149
- }
150
- })
151
- );
152
-
153
- this.sideEffect.add(() =>
154
- context.addMagixEventListener(SLIDE_EVENTS.syncDispatch, this.magixEventListener, {
155
- fireSelfEventAfterCommit: true,
156
- })
157
- );
158
-
159
- slide.on(SLIDE_EVENTS.slideChange, this.onPageChanged);
160
- slide.on(SLIDE_EVENTS.renderStart, this.onTransitionStart);
161
- slide.on(SLIDE_EVENTS.renderEnd, this.onTransitionEnd);
162
- slide.on(SLIDE_EVENTS.mainSeqStepStart, this.onTransitionStart);
163
- slide.on(SLIDE_EVENTS.mainSeqStepEnd, this.onTransitionEnd);
164
- slide.on(SLIDE_EVENTS.renderError, this.onError);
165
- slide.on(SLIDE_EVENTS.stateChange, this.onStateChange);
166
- slide.on(SLIDE_EVENTS.syncDispatch, this.onSyncDispatch);
167
-
168
- slide.on(SLIDE_EVENTS.renderEnd, this.resolveReady);
169
-
170
- this.sideEffect.add(() => {
171
- document.addEventListener("visibilitychange", this.onVisibilityChange);
172
- return () => document.removeEventListener("visibilitychange", this.onVisibilityChange);
173
- });
174
- }
175
-
176
- private onSyncDispatch = (event: SyncEvent) => {
177
- if (this.context.getIsWritable() && this.room) {
178
- const payload: MagixPayload = {
179
- type: SLIDE_EVENTS.syncDispatch,
180
- payload: event,
181
- };
182
- verbose("[Slide] dispatch", JSON.stringify(event));
183
- this.context.dispatchMagixEvent(SLIDE_EVENTS.syncDispatch, payload);
184
- }
185
- };
186
-
187
- private magixEventListener: MagixEventListener = ev => {
188
- const { type, payload } = ev.payload;
189
- if (type === SLIDE_EVENTS.syncDispatch) {
190
- this.syncStateOnce();
191
- verbose("[Slide] receive", JSON.stringify(payload));
192
- this.slide.emit(SLIDE_EVENTS.syncReceive, payload);
193
- }
194
- };
195
-
196
- private syncStateOnce() {
197
- // sync state before the first event, so that they can be in the correct order
198
- if (this.syncStateOnceFlag) {
199
- if (this.context.getIsWritable()) {
200
- this.context.storage.ensureState(EmptyAttributes);
201
- }
202
- const { state } = this.context.storage.state;
203
- if (state) {
204
- log("[Slide] sync with state (once)", JSON.stringify(state));
205
- this.slide.setSlideState(state);
206
- this.syncStateOnceFlag = false;
207
- }
208
- }
209
- }
210
-
211
- private onStateChange = (state: SlideState) => {
212
- if (this.context.getIsWritable()) {
213
- verbose("[Slide] state change", JSON.stringify(state, null, 2));
214
- this.context.storage.setState({ state });
215
- }
216
- };
217
-
218
- private pollCount = 0;
219
- private pollReadyState = () => {
220
- if (this.ready) {
221
- if (this._toFreeze === 1) {
222
- this.freeze();
223
- } else if (this._toFreeze === -1) {
224
- this.unfreeze();
225
- }
226
- } else if (this.pollCount < MaxPollCount) {
227
- this.pollCount++;
228
- setTimeout(this.pollReadyState, 500);
229
- } else {
230
- this.pollCount = 0;
231
- log("[Slide] renderSlide 1 (retry after timeout)");
232
- this.slide.renderSlide(1);
233
- }
234
- };
235
-
236
- // cache `slideCount`, because once the slide is frozen,
237
- // the `slideCount` will be 0
238
- private _pageCount = 0;
239
- public get pageCount() {
240
- if (this._pageCount > 0) return this._pageCount;
241
- this._pageCount = this.slide.slideCount;
242
- return this._pageCount;
243
- }
244
-
245
- public get page() {
246
- return this.slide.slideState.currentSlideIndex;
247
- }
248
-
249
- private createSlide(anchor: HTMLDivElement) {
250
- const slide = new Slide({
251
- anchor,
252
- interactive: true,
253
- mode: "interactive",
254
- resize: true,
255
- controller: logger.enable,
256
- enableGlobalClick: true,
257
- renderOptions: {
258
- minFPS: this.context.getAppOptions()?.minFPS || 25,
259
- maxFPS: this.context.getAppOptions()?.maxFPS || 30,
260
- autoFPS: this.context.getAppOptions()?.autoFPS ?? true,
261
- autoResolution: this.context.getAppOptions()?.autoResolution ?? true,
262
- resolution: this.context.getAppOptions()?.resolution,
263
- transactionBgColor: this.context.getAppOptions()?.bgColor || cachedGetBgColor(anchor),
264
- },
265
- timestamp: this.timestamp,
266
- });
267
- if (import.meta.env.DEV) {
268
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
269
- (window as any).slide = slide;
270
- }
271
- return slide;
272
- }
273
-
274
- private destroyed = false;
275
-
276
- public destroy() {
277
- this.sideEffect.flushAll();
278
- if (!this.destroyed) {
279
- log("[Slide] destroy slide (once)");
280
- this.slide.destroy();
281
- this.destroyed = true;
282
- }
283
- }
284
-
285
- public timestamp = () => {
286
- if (this.room && this.room.calibrationTimestamp) {
287
- return this.room.calibrationTimestamp;
288
- } else if (this.player) {
289
- return this.player.beginTimestamp + this.player.progressTime;
290
- } else {
291
- return Date.now();
292
- }
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.visible) return;
331
- if (this.ready) {
332
- log("[Slide] unfreeze", this.context.appId);
333
- if (this.freezePromise) {
334
- this._toFreeze = -1;
335
- } else if (this.isFrozen) {
336
- this._toFreeze = 0;
337
- this.freezePromise = this.slide.release().then(this.afterFreeze.bind(this, -1));
338
- }
339
- } else {
340
- this._toFreeze = -1;
341
- }
342
- };
343
-
344
- private onVisibilityChange = async () => {
345
- if (!(this.visible = document.visibilityState === "visible")) {
346
- this.savedIsFrozen = this.freezePromise ? this._toFreeze === 1 : this.isFrozen;
347
- log("[Slide] freeze because tab becomes invisible");
348
- this.freeze();
349
- } else {
350
- log("[Slide] unfreeze because tab becomes visible", { savedIsFrozen: this.savedIsFrozen });
351
- if (!this.savedIsFrozen) {
352
- this.unfreeze();
353
- }
354
- }
355
- };
356
- }
@@ -1,210 +0,0 @@
1
- import type { ReadonlyTeleBox, AnimationMode, View } from "@netless/window-manager";
2
- import type { SlideController, SlideControllerOptions } from "../SlideController";
3
-
4
- import { SideEffectManager } from "side-effect-manager";
5
- import { createDocsViewerPages } from "../SlideController";
6
- import { DocsViewer } from "../DocsViewer";
7
- import { logger } from "../utils/logger";
8
-
9
- export const ClickThroughAppliances = new Set(["clicker"]);
10
-
11
- export type MountSlideOptions = Omit<SlideControllerOptions, "context" | "onPageChanged"> & {
12
- onReady: () => void;
13
- };
14
-
15
- export interface SlideDocsViewerConfig {
16
- box: ReadonlyTeleBox;
17
- view: View;
18
- mountSlideController: (options: MountSlideOptions) => SlideController;
19
- mountWhiteboard: (dom: HTMLDivElement) => void;
20
- }
21
-
22
- export class SlideDocsViewer {
23
- public viewer: DocsViewer;
24
- public slideController: SlideController | null = null;
25
-
26
- protected readonly box: ReadonlyTeleBox;
27
- protected readonly whiteboardView: SlideDocsViewerConfig["view"];
28
- protected readonly mountSlideController: SlideDocsViewerConfig["mountSlideController"];
29
- protected readonly mountWhiteboard: SlideDocsViewerConfig["mountWhiteboard"];
30
- private isViewMounted = false;
31
-
32
- public constructor({ box, view, mountSlideController, mountWhiteboard }: SlideDocsViewerConfig) {
33
- this.box = box;
34
- this.whiteboardView = view;
35
- this.mountSlideController = mountSlideController;
36
- this.mountWhiteboard = mountWhiteboard;
37
-
38
- this.viewer = new DocsViewer({
39
- readonly: box.readonly,
40
- onNewPageIndex: this.onNewPageIndex,
41
- onPlay: this.onPlay,
42
- });
43
-
44
- this.sideEffect.add(() => {
45
- const handler = (readonly: boolean): void => {
46
- this.setReadonly(readonly);
47
- };
48
- box.events.on("readonly", handler);
49
- return () => box.events.off("readonly", handler);
50
- });
51
-
52
- this.render();
53
- }
54
-
55
- public $slide!: HTMLDivElement;
56
- public $whiteboardView!: HTMLDivElement;
57
-
58
- public render() {
59
- this.viewer.$content.appendChild(this.renderSlideContainer());
60
- this.viewer.$content.appendChild(this.renderWhiteboardView());
61
- this.sideEffect.addEventListener(window, "keydown", ev => {
62
- if (this.box.focus && this.slideController) {
63
- switch (ev.key) {
64
- case "ArrowUp":
65
- case "ArrowLeft": {
66
- this.slideController.slide.prevStep();
67
- break;
68
- }
69
- case "ArrowRight":
70
- case "ArrowDown": {
71
- this.slideController.slide.nextStep();
72
- break;
73
- }
74
- default: {
75
- break;
76
- }
77
- }
78
- }
79
- });
80
- }
81
-
82
- protected renderSlideContainer(): HTMLDivElement {
83
- if (!this.$slide) {
84
- const $slide = document.createElement("div");
85
- $slide.className = this.wrapClassName("slide");
86
- $slide.dataset.appKind = "Slide";
87
- this.$slide = $slide;
88
- }
89
- return this.$slide;
90
- }
91
-
92
- protected renderWhiteboardView(): HTMLDivElement {
93
- if (!this.$whiteboardView) {
94
- this.$whiteboardView = document.createElement("div");
95
- this.$whiteboardView.className = this.wrapClassName("wb-view");
96
- }
97
- return this.$whiteboardView;
98
- }
99
-
100
- public mount() {
101
- this.box.mountContent(this.viewer.$content);
102
- this.box.mountFooter(this.viewer.$footer);
103
-
104
- this.slideController = this.mountSlideController({
105
- anchor: this.$slide,
106
- onTransitionStart: this.viewer.setPlaying,
107
- onTransitionEnd: this.viewer.setPaused,
108
- onReady: this.refreshPages,
109
- onError: this.onError,
110
- });
111
-
112
- this.scaleDocsToFit();
113
- this.sideEffect.add(() => {
114
- this.whiteboardView.callbacks.on("onSizeUpdated", this.scaleDocsToFit);
115
- return () => this.whiteboardView.callbacks.off("onSizeUpdated", this.scaleDocsToFit);
116
- });
117
-
118
- return this;
119
- }
120
-
121
- protected onError = ({ error }: { error: Error }) => {
122
- this.viewer.setPaused();
123
- logger.warn("[Slide] render error", error);
124
- };
125
-
126
- protected refreshPages = () => {
127
- if (this.slideController) {
128
- this.viewer.pages = createDocsViewerPages(this.slideController.slide);
129
- this.viewer.setPageIndex(this.getPageIndex(this.slideController.page));
130
- this.scaleDocsToFit();
131
- }
132
- };
133
-
134
- protected getPageIndex(page: number) {
135
- return (page > 0 ? page : 1) - 1;
136
- }
137
-
138
- public unmount() {
139
- if (this.slideController) {
140
- this.slideController.destroy();
141
- this.slideController = null;
142
- }
143
- this.viewer.unmount();
144
- return this;
145
- }
146
-
147
- public setReadonly(readonly: boolean) {
148
- this.viewer.setReadonly(readonly);
149
- }
150
-
151
- public destroy() {
152
- this.sideEffect.flushAll();
153
- this.unmount();
154
- this.viewer.destroy();
155
- }
156
-
157
- public toggleClickThrough(tool?: string) {
158
- this.$whiteboardView.style.pointerEvents =
159
- !tool || ClickThroughAppliances.has(tool) ? "none" : "auto";
160
- }
161
-
162
- protected scaleDocsToFit = () => {
163
- if (this.slideController) {
164
- const { width, height } = this.slideController.slide;
165
- if (width && height) {
166
- this.whiteboardView.moveCameraToContain({
167
- originX: -width / 2,
168
- originY: -height / 2,
169
- width,
170
- height,
171
- animationMode: "immediately" as AnimationMode.Immediately,
172
- });
173
- this.whiteboardView.setCameraBound({
174
- damping: 1,
175
- maxContentMode: () => this.whiteboardView.camera.scale,
176
- minContentMode: () => this.whiteboardView.camera.scale,
177
- centerX: 0,
178
- centerY: 0,
179
- width,
180
- height,
181
- });
182
- if (!this.isViewMounted) {
183
- this.isViewMounted = true;
184
- console.log("[Slide] mount whiteboard view");
185
- this.mountWhiteboard(this.$whiteboardView);
186
- }
187
- }
188
- }
189
- };
190
-
191
- protected onPlay = () => {
192
- if (this.slideController) {
193
- this.slideController.slide.nextStep();
194
- }
195
- };
196
-
197
- protected onNewPageIndex = (index: number) => {
198
- if (this.slideController) {
199
- this.slideController.jumpToPage(index + 1);
200
- }
201
- };
202
-
203
- protected sideEffect = new SideEffectManager();
204
-
205
- protected wrapClassName(className: string) {
206
- return `${this.namespace}-${className}`;
207
- }
208
-
209
- protected namespace = "netless-app-slide";
210
- }
@@ -1,11 +0,0 @@
1
- $namespace: "netless-app-slide";
2
-
3
- .#{$namespace}-wb-view {
4
- position: absolute;
5
- top: 0;
6
- left: 0;
7
- width: 100%;
8
- height: 100%;
9
- z-index: 100;
10
- overflow: hidden;
11
- }