@netless/app-slide 0.2.39 → 0.2.40
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/LICENSE +21 -0
- package/README-zh.md +53 -53
- package/README.md +68 -68
- package/dist/index.d.ts +294 -294
- package/dist/main.cjs.js +4 -4
- package/dist/main.cjs.js.map +1 -1
- package/dist/main.es.js +6 -6
- package/dist/main.es.js.map +1 -1
- package/dist/main.iife.js +4 -4
- package/dist/main.iife.js.map +1 -1
- package/package.json +9 -9
- package/src/DocsViewer/icons/arrow-left.svg +4 -4
- package/src/DocsViewer/icons/arrow-left.ts +18 -18
- package/src/DocsViewer/icons/arrow-right.svg +4 -4
- package/src/DocsViewer/icons/arrow-right.ts +18 -18
- package/src/DocsViewer/icons/pause.svg +4 -4
- package/src/DocsViewer/icons/pause.ts +18 -18
- package/src/DocsViewer/icons/play.svg +4 -4
- package/src/DocsViewer/icons/play.ts +18 -18
- package/src/DocsViewer/icons/sidebar.svg +4 -4
- package/src/DocsViewer/icons/sidebar.ts +18 -18
- package/src/DocsViewer/index.ts +381 -381
- package/src/DocsViewer/style.scss +228 -228
- package/src/DocsViewer/variables.scss +1 -1
- package/src/SlideController/helpers.ts +48 -48
- package/src/SlideController/index.ts +351 -351
- package/src/SlideDocsViewer/style.scss +28 -28
- package/src/SlidePreviewer/index.ts +225 -225
- package/src/index.ts +283 -283
- package/src/style.scss +14 -14
- package/src/typings.ts +21 -21
- package/src/utils/bgcolor.ts +45 -45
- package/src/utils/freezer.ts +116 -116
- package/src/utils/helpers.ts +32 -32
- package/src/utils/logger.ts +106 -106
|
@@ -1,351 +1,351 @@
|
|
|
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
|
-
onRenderEnd: () => void;
|
|
35
|
-
onPageChanged: (page: number) => void;
|
|
36
|
-
onTransitionStart: () => void;
|
|
37
|
-
onTransitionEnd: () => void;
|
|
38
|
-
onError: (args: { error: Error; index: number }) => void;
|
|
39
|
-
onRenderError?: (error: Error, pageIndex: number) => void;
|
|
40
|
-
showRenderError?: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
type MagixEventListener = Parameters<
|
|
44
|
-
AppContext<Attributes, MagixEvents>["addMagixEventListener"]
|
|
45
|
-
>[1];
|
|
46
|
-
|
|
47
|
-
export class SlideController {
|
|
48
|
-
public readonly context: SlideControllerOptions["context"];
|
|
49
|
-
public readonly slide: Slide;
|
|
50
|
-
public readonly showRenderError: boolean;
|
|
51
|
-
public readonly onRenderError?: (error: Error, pageIndex: number) => void;
|
|
52
|
-
|
|
53
|
-
private readonly room?: Room;
|
|
54
|
-
private readonly player?: Player;
|
|
55
|
-
private readonly sideEffect = new SideEffectManager();
|
|
56
|
-
|
|
57
|
-
private readonly onPageChanged: SlideControllerOptions["onPageChanged"];
|
|
58
|
-
private readonly onTransitionStart: SlideControllerOptions["onTransitionStart"];
|
|
59
|
-
private readonly onTransitionEnd: SlideControllerOptions["onTransitionEnd"];
|
|
60
|
-
private readonly onError: SlideControllerOptions["onError"];
|
|
61
|
-
|
|
62
|
-
private syncStateOnceFlag: boolean;
|
|
63
|
-
|
|
64
|
-
private visible: boolean;
|
|
65
|
-
private savedIsFrozen: boolean;
|
|
66
|
-
|
|
67
|
-
public constructor({
|
|
68
|
-
context,
|
|
69
|
-
anchor,
|
|
70
|
-
onPageChanged,
|
|
71
|
-
onTransitionStart,
|
|
72
|
-
onTransitionEnd,
|
|
73
|
-
onError,
|
|
74
|
-
onRenderError,
|
|
75
|
-
showRenderError,
|
|
76
|
-
}: SlideControllerOptions) {
|
|
77
|
-
this.onPageChanged = onPageChanged;
|
|
78
|
-
this.onTransitionStart = onTransitionStart;
|
|
79
|
-
this.onTransitionEnd = onTransitionEnd;
|
|
80
|
-
this.onError = onError;
|
|
81
|
-
this.onRenderError = onRenderError;
|
|
82
|
-
this.showRenderError = showRenderError ?? true;
|
|
83
|
-
|
|
84
|
-
this.context = context;
|
|
85
|
-
this.room = context.getRoom();
|
|
86
|
-
this.player = this.room ? undefined : (context.getDisplayer() as Player);
|
|
87
|
-
setRoomLogger((this.room || this.player) as Displayer);
|
|
88
|
-
this.slide = this.createSlide(anchor);
|
|
89
|
-
|
|
90
|
-
// the adder does not need to sync state
|
|
91
|
-
this.syncStateOnceFlag = !this.context.isAddApp;
|
|
92
|
-
this.visible = document.visibilityState === "visible";
|
|
93
|
-
this.savedIsFrozen = false;
|
|
94
|
-
this.initialize();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
public ready = false;
|
|
98
|
-
private resolveReady!: () => void;
|
|
99
|
-
public readonly readyPromise = new Promise<void>(resolve => {
|
|
100
|
-
this.resolveReady = () => {
|
|
101
|
-
if (this.ready) {
|
|
102
|
-
log("[Slide] render end");
|
|
103
|
-
} else {
|
|
104
|
-
setTimeout(() => {
|
|
105
|
-
this.ready = true;
|
|
106
|
-
resolve();
|
|
107
|
-
}, 1000);
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
public jumpToPage(page: number) {
|
|
113
|
-
if (this.ready) {
|
|
114
|
-
page = clamp(page, 1, this.pageCount);
|
|
115
|
-
this.slide.renderSlide(page);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private initialize() {
|
|
120
|
-
this.registerEventListeners();
|
|
121
|
-
this.kickStart();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
private kickStart() {
|
|
125
|
-
const { context, slide } = this;
|
|
126
|
-
if (context.getIsWritable()) {
|
|
127
|
-
context.storage.ensureState(EmptyAttributes);
|
|
128
|
-
}
|
|
129
|
-
const { taskId, url, state } = context.storage.state;
|
|
130
|
-
slide.setResource(taskId, url || DefaultUrl);
|
|
131
|
-
if (state) {
|
|
132
|
-
// if we already have state, try restore from it
|
|
133
|
-
log("[Slide] init with state", JSON.stringify(state));
|
|
134
|
-
this.syncStateOnceFlag = false;
|
|
135
|
-
slide.setSlideState(state);
|
|
136
|
-
} else if (context.isAddApp) {
|
|
137
|
-
// otherwise, maybe this slide is just added, let the adder kick start first render
|
|
138
|
-
log("[Slide] init by renderSlide", 1);
|
|
139
|
-
slide.renderSlide(1);
|
|
140
|
-
}
|
|
141
|
-
// there's still some risk that the adder is left and no first render
|
|
142
|
-
// so anyway, we start polling the slide's "ready state"
|
|
143
|
-
// if in the next 20 seconds the slide is not ready, start render first page
|
|
144
|
-
this.pollReadyState();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
private registerEventListeners() {
|
|
148
|
-
const { context, slide } = this;
|
|
149
|
-
|
|
150
|
-
// it is possible that we miss the first `renderSlide(1)` event
|
|
151
|
-
// and the attributes has no value yet, so we need to sync state
|
|
152
|
-
// when there's state change. we only have to do it once
|
|
153
|
-
const disposerId = this.sideEffect.addDisposer(
|
|
154
|
-
context.storage.addStateChangedListener(() => {
|
|
155
|
-
if (context.storage.state.state) {
|
|
156
|
-
this.syncStateOnce();
|
|
157
|
-
this.sideEffect.flush(disposerId);
|
|
158
|
-
}
|
|
159
|
-
})
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
this.sideEffect.add(() =>
|
|
163
|
-
context.addMagixEventListener(SLIDE_EVENTS.syncDispatch, this.magixEventListener, {
|
|
164
|
-
fireSelfEventAfterCommit: true,
|
|
165
|
-
})
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
slide.on(SLIDE_EVENTS.slideChange, this.onPageChanged);
|
|
169
|
-
slide.on(SLIDE_EVENTS.renderStart, this.onTransitionStart);
|
|
170
|
-
slide.on(SLIDE_EVENTS.renderEnd, this.onTransitionEnd);
|
|
171
|
-
slide.on(SLIDE_EVENTS.mainSeqStepStart, this.onTransitionStart);
|
|
172
|
-
slide.on(SLIDE_EVENTS.mainSeqStepEnd, this.onTransitionEnd);
|
|
173
|
-
slide.on(SLIDE_EVENTS.renderError, this.onError);
|
|
174
|
-
slide.on(SLIDE_EVENTS.stateChange, this.onStateChange);
|
|
175
|
-
slide.on(SLIDE_EVENTS.syncDispatch, this.onSyncDispatch);
|
|
176
|
-
|
|
177
|
-
slide.on(SLIDE_EVENTS.renderEnd, this.resolveReady);
|
|
178
|
-
|
|
179
|
-
this.sideEffect.add(() => {
|
|
180
|
-
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
181
|
-
return () => document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private onSyncDispatch = (event: SyncEvent) => {
|
|
186
|
-
if (this.context.getIsWritable() && this.room) {
|
|
187
|
-
const payload: MagixPayload = {
|
|
188
|
-
type: SLIDE_EVENTS.syncDispatch,
|
|
189
|
-
payload: event,
|
|
190
|
-
};
|
|
191
|
-
verbose("[Slide] dispatch", JSON.stringify(event));
|
|
192
|
-
this.context.dispatchMagixEvent(SLIDE_EVENTS.syncDispatch, payload);
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
private magixEventListener: MagixEventListener = ev => {
|
|
197
|
-
const { type, payload } = ev.payload;
|
|
198
|
-
if (type === SLIDE_EVENTS.syncDispatch) {
|
|
199
|
-
this.syncStateOnce();
|
|
200
|
-
verbose("[Slide] receive", JSON.stringify(payload));
|
|
201
|
-
this.slide.emit(SLIDE_EVENTS.syncReceive, payload);
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
private syncStateOnce() {
|
|
206
|
-
// sync state before the first event, so that they can be in the correct order
|
|
207
|
-
if (this.syncStateOnceFlag) {
|
|
208
|
-
if (this.context.getIsWritable()) {
|
|
209
|
-
this.context.storage.ensureState(EmptyAttributes);
|
|
210
|
-
}
|
|
211
|
-
const { state } = this.context.storage.state;
|
|
212
|
-
if (state) {
|
|
213
|
-
log("[Slide] sync with state (once)", JSON.stringify(state));
|
|
214
|
-
this.slide.setSlideState(state);
|
|
215
|
-
this.syncStateOnceFlag = false;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
private onStateChange = (state: SlideState) => {
|
|
221
|
-
if (this.context.getIsWritable()) {
|
|
222
|
-
verbose("[Slide] state change", JSON.stringify(state, null, 2));
|
|
223
|
-
this.context.storage.setState({ state });
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
private pollCount = 0;
|
|
228
|
-
private pollReadyState = () => {
|
|
229
|
-
if (this.ready) {
|
|
230
|
-
if (this._toFreeze === 1) {
|
|
231
|
-
this.freeze();
|
|
232
|
-
} else if (this._toFreeze === -1) {
|
|
233
|
-
this.unfreeze();
|
|
234
|
-
}
|
|
235
|
-
} else if (this.pollCount < MaxPollCount) {
|
|
236
|
-
this.pollCount++;
|
|
237
|
-
setTimeout(this.pollReadyState, 500);
|
|
238
|
-
} else {
|
|
239
|
-
this.pollCount = 0;
|
|
240
|
-
log("[Slide] renderSlide 1 (retry after timeout)");
|
|
241
|
-
this.slide.renderSlide(1);
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
// cache `slideCount`, because once the slide is frozen,
|
|
246
|
-
// the `slideCount` will be 0
|
|
247
|
-
private _pageCount = 0;
|
|
248
|
-
public get pageCount() {
|
|
249
|
-
if (this._pageCount > 0) return this._pageCount;
|
|
250
|
-
this._pageCount = this.slide.slideCount;
|
|
251
|
-
return this._pageCount;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
public get page() {
|
|
255
|
-
return this.slide.slideState.currentSlideIndex;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
private createSlide(anchor: HTMLDivElement) {
|
|
259
|
-
const options = this.context.getAppOptions() || {};
|
|
260
|
-
const slide = new Slide({
|
|
261
|
-
anchor,
|
|
262
|
-
interactive: true,
|
|
263
|
-
mode: "interactive",
|
|
264
|
-
controller: logger.enable,
|
|
265
|
-
enableGlobalClick: true,
|
|
266
|
-
renderOptions: {
|
|
267
|
-
minFPS: options.minFPS || 25,
|
|
268
|
-
maxFPS: options.maxFPS || 30,
|
|
269
|
-
autoFPS: options.autoFPS ?? true,
|
|
270
|
-
autoResolution: options.autoResolution ?? true,
|
|
271
|
-
resolution: options.resolution,
|
|
272
|
-
transactionBgColor: options.bgColor || cachedGetBgColor(anchor),
|
|
273
|
-
maxResolutionLevel: options.maxResolutionLevel,
|
|
274
|
-
forceCanvas: options.forceCanvas,
|
|
275
|
-
enableNvidiaDetect: options.enableNvidiaDetect,
|
|
276
|
-
},
|
|
277
|
-
fixedFrameSize: options.fixedFrameSize,
|
|
278
|
-
loaderDelegate: options.loaderDelegate,
|
|
279
|
-
navigatorDelegate: options.navigatorDelegate,
|
|
280
|
-
urlInterrupter: options.urlInterrupter,
|
|
281
|
-
resourceTimeout: options.resourceTimeout,
|
|
282
|
-
rtcAudio: options.rtcAudio,
|
|
283
|
-
useLocalCache: options.useLocalCache,
|
|
284
|
-
logger: options.logger,
|
|
285
|
-
timestamp: this.timestamp,
|
|
286
|
-
});
|
|
287
|
-
if (import.meta.env.DEV) {
|
|
288
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
289
|
-
(window as any).slide = slide;
|
|
290
|
-
}
|
|
291
|
-
return slide;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
private destroyed = false;
|
|
295
|
-
|
|
296
|
-
public destroy() {
|
|
297
|
-
this.sideEffect.flushAll();
|
|
298
|
-
if (!this.destroyed) {
|
|
299
|
-
log("[Slide] destroy slide (once)");
|
|
300
|
-
this.slide.destroy();
|
|
301
|
-
this.destroyed = true;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
public timestamp = () => {
|
|
306
|
-
if (this.room && this.room.calibrationTimestamp) {
|
|
307
|
-
return this.room.calibrationTimestamp;
|
|
308
|
-
} else if (this.player) {
|
|
309
|
-
return this.player.beginTimestamp + this.player.progressTime;
|
|
310
|
-
} else {
|
|
311
|
-
return Date.now();
|
|
312
|
-
}
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
public isFrozen = false;
|
|
316
|
-
private _toFreeze: -1 | 0 | 1 = 0; // -1: unfreeze, 0: no change, 1: freeze
|
|
317
|
-
|
|
318
|
-
public freeze = () => {
|
|
319
|
-
this.isFrozen = true;
|
|
320
|
-
if (this.ready) {
|
|
321
|
-
log("[Slide] freeze", this.context.appId);
|
|
322
|
-
this.slide.frozen();
|
|
323
|
-
} else {
|
|
324
|
-
this._toFreeze = 1;
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
public unfreeze = async () => {
|
|
329
|
-
if (!this.visible) return;
|
|
330
|
-
this.isFrozen = false;
|
|
331
|
-
if (this.ready) {
|
|
332
|
-
log("[Slide] unfreeze", this.context.appId);
|
|
333
|
-
this.slide.release();
|
|
334
|
-
} else {
|
|
335
|
-
this._toFreeze = -1;
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
private onVisibilityChange = async () => {
|
|
340
|
-
if (!(this.visible = document.visibilityState === "visible")) {
|
|
341
|
-
this.savedIsFrozen = this.isFrozen;
|
|
342
|
-
log("[Slide] freeze because tab becomes invisible");
|
|
343
|
-
this.freeze();
|
|
344
|
-
} else {
|
|
345
|
-
log("[Slide] unfreeze because tab becomes visible", { savedIsFrozen: this.savedIsFrozen });
|
|
346
|
-
if (!this.savedIsFrozen) {
|
|
347
|
-
this.unfreeze();
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
};
|
|
351
|
-
}
|
|
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
|
+
onRenderEnd: () => void;
|
|
35
|
+
onPageChanged: (page: number) => void;
|
|
36
|
+
onTransitionStart: () => void;
|
|
37
|
+
onTransitionEnd: () => void;
|
|
38
|
+
onError: (args: { error: Error; index: number }) => void;
|
|
39
|
+
onRenderError?: (error: Error, pageIndex: number) => void;
|
|
40
|
+
showRenderError?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type MagixEventListener = Parameters<
|
|
44
|
+
AppContext<Attributes, MagixEvents>["addMagixEventListener"]
|
|
45
|
+
>[1];
|
|
46
|
+
|
|
47
|
+
export class SlideController {
|
|
48
|
+
public readonly context: SlideControllerOptions["context"];
|
|
49
|
+
public readonly slide: Slide;
|
|
50
|
+
public readonly showRenderError: boolean;
|
|
51
|
+
public readonly onRenderError?: (error: Error, pageIndex: number) => void;
|
|
52
|
+
|
|
53
|
+
private readonly room?: Room;
|
|
54
|
+
private readonly player?: Player;
|
|
55
|
+
private readonly sideEffect = new SideEffectManager();
|
|
56
|
+
|
|
57
|
+
private readonly onPageChanged: SlideControllerOptions["onPageChanged"];
|
|
58
|
+
private readonly onTransitionStart: SlideControllerOptions["onTransitionStart"];
|
|
59
|
+
private readonly onTransitionEnd: SlideControllerOptions["onTransitionEnd"];
|
|
60
|
+
private readonly onError: SlideControllerOptions["onError"];
|
|
61
|
+
|
|
62
|
+
private syncStateOnceFlag: boolean;
|
|
63
|
+
|
|
64
|
+
private visible: boolean;
|
|
65
|
+
private savedIsFrozen: boolean;
|
|
66
|
+
|
|
67
|
+
public constructor({
|
|
68
|
+
context,
|
|
69
|
+
anchor,
|
|
70
|
+
onPageChanged,
|
|
71
|
+
onTransitionStart,
|
|
72
|
+
onTransitionEnd,
|
|
73
|
+
onError,
|
|
74
|
+
onRenderError,
|
|
75
|
+
showRenderError,
|
|
76
|
+
}: SlideControllerOptions) {
|
|
77
|
+
this.onPageChanged = onPageChanged;
|
|
78
|
+
this.onTransitionStart = onTransitionStart;
|
|
79
|
+
this.onTransitionEnd = onTransitionEnd;
|
|
80
|
+
this.onError = onError;
|
|
81
|
+
this.onRenderError = onRenderError;
|
|
82
|
+
this.showRenderError = showRenderError ?? true;
|
|
83
|
+
|
|
84
|
+
this.context = context;
|
|
85
|
+
this.room = context.getRoom();
|
|
86
|
+
this.player = this.room ? undefined : (context.getDisplayer() as Player);
|
|
87
|
+
setRoomLogger((this.room || this.player) as Displayer);
|
|
88
|
+
this.slide = this.createSlide(anchor);
|
|
89
|
+
|
|
90
|
+
// the adder does not need to sync state
|
|
91
|
+
this.syncStateOnceFlag = !this.context.isAddApp;
|
|
92
|
+
this.visible = document.visibilityState === "visible";
|
|
93
|
+
this.savedIsFrozen = false;
|
|
94
|
+
this.initialize();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public ready = false;
|
|
98
|
+
private resolveReady!: () => void;
|
|
99
|
+
public readonly readyPromise = new Promise<void>(resolve => {
|
|
100
|
+
this.resolveReady = () => {
|
|
101
|
+
if (this.ready) {
|
|
102
|
+
log("[Slide] render end");
|
|
103
|
+
} else {
|
|
104
|
+
setTimeout(() => {
|
|
105
|
+
this.ready = true;
|
|
106
|
+
resolve();
|
|
107
|
+
}, 1000);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
public jumpToPage(page: number) {
|
|
113
|
+
if (this.ready) {
|
|
114
|
+
page = clamp(page, 1, this.pageCount);
|
|
115
|
+
this.slide.renderSlide(page);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private initialize() {
|
|
120
|
+
this.registerEventListeners();
|
|
121
|
+
this.kickStart();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private kickStart() {
|
|
125
|
+
const { context, slide } = this;
|
|
126
|
+
if (context.getIsWritable()) {
|
|
127
|
+
context.storage.ensureState(EmptyAttributes);
|
|
128
|
+
}
|
|
129
|
+
const { taskId, url, state } = context.storage.state;
|
|
130
|
+
slide.setResource(taskId, url || DefaultUrl);
|
|
131
|
+
if (state) {
|
|
132
|
+
// if we already have state, try restore from it
|
|
133
|
+
log("[Slide] init with state", JSON.stringify(state));
|
|
134
|
+
this.syncStateOnceFlag = false;
|
|
135
|
+
slide.setSlideState(state);
|
|
136
|
+
} else if (context.isAddApp) {
|
|
137
|
+
// otherwise, maybe this slide is just added, let the adder kick start first render
|
|
138
|
+
log("[Slide] init by renderSlide", 1);
|
|
139
|
+
slide.renderSlide(1);
|
|
140
|
+
}
|
|
141
|
+
// there's still some risk that the adder is left and no first render
|
|
142
|
+
// so anyway, we start polling the slide's "ready state"
|
|
143
|
+
// if in the next 20 seconds the slide is not ready, start render first page
|
|
144
|
+
this.pollReadyState();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private registerEventListeners() {
|
|
148
|
+
const { context, slide } = this;
|
|
149
|
+
|
|
150
|
+
// it is possible that we miss the first `renderSlide(1)` event
|
|
151
|
+
// and the attributes has no value yet, so we need to sync state
|
|
152
|
+
// when there's state change. we only have to do it once
|
|
153
|
+
const disposerId = this.sideEffect.addDisposer(
|
|
154
|
+
context.storage.addStateChangedListener(() => {
|
|
155
|
+
if (context.storage.state.state) {
|
|
156
|
+
this.syncStateOnce();
|
|
157
|
+
this.sideEffect.flush(disposerId);
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
this.sideEffect.add(() =>
|
|
163
|
+
context.addMagixEventListener(SLIDE_EVENTS.syncDispatch, this.magixEventListener, {
|
|
164
|
+
fireSelfEventAfterCommit: true,
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
slide.on(SLIDE_EVENTS.slideChange, this.onPageChanged);
|
|
169
|
+
slide.on(SLIDE_EVENTS.renderStart, this.onTransitionStart);
|
|
170
|
+
slide.on(SLIDE_EVENTS.renderEnd, this.onTransitionEnd);
|
|
171
|
+
slide.on(SLIDE_EVENTS.mainSeqStepStart, this.onTransitionStart);
|
|
172
|
+
slide.on(SLIDE_EVENTS.mainSeqStepEnd, this.onTransitionEnd);
|
|
173
|
+
slide.on(SLIDE_EVENTS.renderError, this.onError);
|
|
174
|
+
slide.on(SLIDE_EVENTS.stateChange, this.onStateChange);
|
|
175
|
+
slide.on(SLIDE_EVENTS.syncDispatch, this.onSyncDispatch);
|
|
176
|
+
|
|
177
|
+
slide.on(SLIDE_EVENTS.renderEnd, this.resolveReady);
|
|
178
|
+
|
|
179
|
+
this.sideEffect.add(() => {
|
|
180
|
+
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
181
|
+
return () => document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private onSyncDispatch = (event: SyncEvent) => {
|
|
186
|
+
if (this.context.getIsWritable() && this.room) {
|
|
187
|
+
const payload: MagixPayload = {
|
|
188
|
+
type: SLIDE_EVENTS.syncDispatch,
|
|
189
|
+
payload: event,
|
|
190
|
+
};
|
|
191
|
+
verbose("[Slide] dispatch", JSON.stringify(event));
|
|
192
|
+
this.context.dispatchMagixEvent(SLIDE_EVENTS.syncDispatch, payload);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
private magixEventListener: MagixEventListener = ev => {
|
|
197
|
+
const { type, payload } = ev.payload;
|
|
198
|
+
if (type === SLIDE_EVENTS.syncDispatch) {
|
|
199
|
+
this.syncStateOnce();
|
|
200
|
+
verbose("[Slide] receive", JSON.stringify(payload));
|
|
201
|
+
this.slide.emit(SLIDE_EVENTS.syncReceive, payload);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
private syncStateOnce() {
|
|
206
|
+
// sync state before the first event, so that they can be in the correct order
|
|
207
|
+
if (this.syncStateOnceFlag) {
|
|
208
|
+
if (this.context.getIsWritable()) {
|
|
209
|
+
this.context.storage.ensureState(EmptyAttributes);
|
|
210
|
+
}
|
|
211
|
+
const { state } = this.context.storage.state;
|
|
212
|
+
if (state) {
|
|
213
|
+
log("[Slide] sync with state (once)", JSON.stringify(state));
|
|
214
|
+
this.slide.setSlideState(state);
|
|
215
|
+
this.syncStateOnceFlag = false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private onStateChange = (state: SlideState) => {
|
|
221
|
+
if (this.context.getIsWritable()) {
|
|
222
|
+
verbose("[Slide] state change", JSON.stringify(state, null, 2));
|
|
223
|
+
this.context.storage.setState({ state });
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
private pollCount = 0;
|
|
228
|
+
private pollReadyState = () => {
|
|
229
|
+
if (this.ready) {
|
|
230
|
+
if (this._toFreeze === 1) {
|
|
231
|
+
this.freeze();
|
|
232
|
+
} else if (this._toFreeze === -1) {
|
|
233
|
+
this.unfreeze();
|
|
234
|
+
}
|
|
235
|
+
} else if (this.pollCount < MaxPollCount) {
|
|
236
|
+
this.pollCount++;
|
|
237
|
+
setTimeout(this.pollReadyState, 500);
|
|
238
|
+
} else {
|
|
239
|
+
this.pollCount = 0;
|
|
240
|
+
log("[Slide] renderSlide 1 (retry after timeout)");
|
|
241
|
+
this.slide.renderSlide(1);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// cache `slideCount`, because once the slide is frozen,
|
|
246
|
+
// the `slideCount` will be 0
|
|
247
|
+
private _pageCount = 0;
|
|
248
|
+
public get pageCount() {
|
|
249
|
+
if (this._pageCount > 0) return this._pageCount;
|
|
250
|
+
this._pageCount = this.slide.slideCount;
|
|
251
|
+
return this._pageCount;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
public get page() {
|
|
255
|
+
return this.slide.slideState.currentSlideIndex;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private createSlide(anchor: HTMLDivElement) {
|
|
259
|
+
const options = this.context.getAppOptions() || {};
|
|
260
|
+
const slide = new Slide({
|
|
261
|
+
anchor,
|
|
262
|
+
interactive: true,
|
|
263
|
+
mode: "interactive",
|
|
264
|
+
controller: logger.enable,
|
|
265
|
+
enableGlobalClick: true,
|
|
266
|
+
renderOptions: {
|
|
267
|
+
minFPS: options.minFPS || 25,
|
|
268
|
+
maxFPS: options.maxFPS || 30,
|
|
269
|
+
autoFPS: options.autoFPS ?? true,
|
|
270
|
+
autoResolution: options.autoResolution ?? true,
|
|
271
|
+
resolution: options.resolution,
|
|
272
|
+
transactionBgColor: options.bgColor || cachedGetBgColor(anchor),
|
|
273
|
+
maxResolutionLevel: options.maxResolutionLevel,
|
|
274
|
+
forceCanvas: options.forceCanvas,
|
|
275
|
+
enableNvidiaDetect: options.enableNvidiaDetect,
|
|
276
|
+
},
|
|
277
|
+
fixedFrameSize: options.fixedFrameSize,
|
|
278
|
+
loaderDelegate: options.loaderDelegate,
|
|
279
|
+
navigatorDelegate: options.navigatorDelegate,
|
|
280
|
+
urlInterrupter: options.urlInterrupter,
|
|
281
|
+
resourceTimeout: options.resourceTimeout,
|
|
282
|
+
rtcAudio: options.rtcAudio,
|
|
283
|
+
useLocalCache: options.useLocalCache,
|
|
284
|
+
logger: options.logger,
|
|
285
|
+
timestamp: this.timestamp,
|
|
286
|
+
});
|
|
287
|
+
if (import.meta.env.DEV) {
|
|
288
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
289
|
+
(window as any).slide = slide;
|
|
290
|
+
}
|
|
291
|
+
return slide;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private destroyed = false;
|
|
295
|
+
|
|
296
|
+
public destroy() {
|
|
297
|
+
this.sideEffect.flushAll();
|
|
298
|
+
if (!this.destroyed) {
|
|
299
|
+
log("[Slide] destroy slide (once)");
|
|
300
|
+
this.slide.destroy();
|
|
301
|
+
this.destroyed = true;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
public timestamp = () => {
|
|
306
|
+
if (this.room && this.room.calibrationTimestamp) {
|
|
307
|
+
return this.room.calibrationTimestamp;
|
|
308
|
+
} else if (this.player) {
|
|
309
|
+
return this.player.beginTimestamp + this.player.progressTime;
|
|
310
|
+
} else {
|
|
311
|
+
return Date.now();
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
public isFrozen = false;
|
|
316
|
+
private _toFreeze: -1 | 0 | 1 = 0; // -1: unfreeze, 0: no change, 1: freeze
|
|
317
|
+
|
|
318
|
+
public freeze = () => {
|
|
319
|
+
this.isFrozen = true;
|
|
320
|
+
if (this.ready) {
|
|
321
|
+
log("[Slide] freeze", this.context.appId);
|
|
322
|
+
this.slide.frozen();
|
|
323
|
+
} else {
|
|
324
|
+
this._toFreeze = 1;
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
public unfreeze = async () => {
|
|
329
|
+
if (!this.visible) return;
|
|
330
|
+
this.isFrozen = false;
|
|
331
|
+
if (this.ready) {
|
|
332
|
+
log("[Slide] unfreeze", this.context.appId);
|
|
333
|
+
this.slide.release();
|
|
334
|
+
} else {
|
|
335
|
+
this._toFreeze = -1;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
private onVisibilityChange = async () => {
|
|
340
|
+
if (!(this.visible = document.visibilityState === "visible")) {
|
|
341
|
+
this.savedIsFrozen = this.isFrozen;
|
|
342
|
+
log("[Slide] freeze because tab becomes invisible");
|
|
343
|
+
this.freeze();
|
|
344
|
+
} else {
|
|
345
|
+
log("[Slide] unfreeze because tab becomes visible", { savedIsFrozen: this.savedIsFrozen });
|
|
346
|
+
if (!this.savedIsFrozen) {
|
|
347
|
+
this.unfreeze();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|