@netless/app-presentation 0.1.12 → 0.1.13-beta.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.
@@ -1,17 +1,43 @@
1
- import type { AnimationMode, AppContext, AppPayload, NetlessApp, PublicEvent, ReadonlyTeleBox, Room, SceneDefinition, Size, View, WindowManager } from "@netless/window-manager"
2
-
3
- import { disposableStore } from '@wopjs/disposable'
4
- import { listen } from '@wopjs/dom'
5
-
6
- import styles from './style.scss?inline';
7
- import { Presentation, type PresentationConfig, type PresentationPage } from "./presentation";
1
+ import type {
2
+ AnimationMode,
3
+ AppContext,
4
+ AppPayload,
5
+ NetlessApp,
6
+ PublicEvent,
7
+ ReadonlyTeleBox,
8
+ Room,
9
+ SceneDefinition,
10
+ Size,
11
+ View,
12
+ WindowManager,
13
+ } from "@netless/window-manager";
14
+
15
+ import { disposableStore } from "@wopjs/disposable";
16
+ import { listen } from "@wopjs/dom";
17
+
18
+ import styles from "./style.scss?inline";
19
+ import {
20
+ Presentation,
21
+ type PresentationConfig,
22
+ type PresentationPage,
23
+ } from "./presentation";
8
24
  import { readable, type Readable } from "./store";
9
25
  import { Scrollbar, type ScrollbarEventCallback } from "./scrollbar";
10
- import { getCameraScaleRange, shouldDisableDeviceCameraTransform } from "./camera-options";
11
- import { cameraToSharedViewport, fitPageSizeToOrigin, getCameraReferenceSize, getFitScale, isValidSharedViewport, isValidSize } from "./camera-reference";
26
+ import {
27
+ getCameraScaleRange,
28
+ shouldDisableDeviceCameraTransform,
29
+ } from "./camera-options";
30
+ import {
31
+ cameraToSharedViewport,
32
+ fitPageSizeToOrigin,
33
+ getCameraReferenceSize,
34
+ getFitScale,
35
+ isValidSharedViewport,
36
+ isValidSize,
37
+ } from "./camera-reference";
12
38
  import debounce from "lodash/debounce";
13
39
 
14
- export type Logger = (...data: any[]) => void
40
+ export type Logger = (...data: any[]) => void;
15
41
 
16
42
  interface PresentationDiagnosticLogger {
17
43
  info(event: string, payload?: unknown): void;
@@ -21,9 +47,9 @@ interface PresentationDiagnosticLogger {
21
47
  flush(): void;
22
48
  }
23
49
 
24
- type MoveCameraRequest = { centerX: number, centerY: number, scale: number }
50
+ type MoveCameraRequest = { centerX: number; centerY: number; scale: number };
25
51
 
26
- const emptySceneName = '$$empty$$'
52
+ const emptySceneName = "$$empty$$";
27
53
 
28
54
  export interface PresentationAttributes {
29
55
  /** Shared logical camera reference size. New pages are proportionally contained within it. */
@@ -65,7 +91,7 @@ export interface PresentationAppOptions {
65
91
  useScrollbar?: boolean;
66
92
  /** debounceSync is used to set the presentation debounce sync, it will be used in the presentation, and the presentation will be debounce sync when the app is initialized */
67
93
  debounceSync?: boolean;
68
- scrollbarEventCallback?: ScrollbarEventCallback
94
+ scrollbarEventCallback?: ScrollbarEventCallback;
69
95
  /** goToPageByClick is used to set the presentation go to page by click, it will be used in the presentation, and the presentation will be go to page by click when the app is initialized */
70
96
  goToPageByClick?: boolean;
71
97
  /** useClipView is used to set the presentation use clip view, it will be used in the presentation, and the presentation will be use clip view when the app is initialized */
@@ -89,9 +115,9 @@ export interface PresentationController {
89
115
  /** Resolves after the whiteboard View has accepted the next scene path. */
90
116
  nextPageAsync(): Promise<boolean>;
91
117
  /** `index` ranges from 0 to `length - 1` */
92
- pageState(): { index: number, length: number };
118
+ pageState(): { index: number; length: number };
93
119
 
94
- toPdf(): Promise<{ pdf: ArrayBuffer, title: string } | null>;
120
+ toPdf(): Promise<{ pdf: ArrayBuffer; title: string } | null>;
95
121
 
96
122
  log: Logger;
97
123
  /** set the docs view readonly */
@@ -99,154 +125,206 @@ export interface PresentationController {
99
125
  /** set the presentation readonly */
100
126
  setReadonly: (bol: boolean) => void;
101
127
  /** Moves the camera through the API, regardless of whether scrollbars are shown. */
102
- moveCamera: (camera: { centerX: number, centerY: number, scale: number }) => void;
128
+ moveCamera: (camera: {
129
+ centerX: number;
130
+ centerY: number;
131
+ scale: number;
132
+ }) => void;
103
133
  /** get the origin scale */
104
134
  getOriginScale: () => number;
105
135
  /** get the view scale */
106
- getScale: () =>number;
136
+ getScale: () => number;
107
137
  /** get the page size */
108
- getPageSize: () => { width: number, height: number };
138
+ getPageSize: () => { width: number; height: number };
109
139
  /** screenshot the current page */
110
- screenshotCurrentPageAsync: (context: CanvasRenderingContext2D, width?: number, height?: number) => Promise<void>;
140
+ screenshotCurrentPageAsync: (
141
+ context: CanvasRenderingContext2D,
142
+ width?: number,
143
+ height?: number,
144
+ ) => Promise<void>;
111
145
  }
112
146
 
113
147
  const ppt2page = (
114
148
  ppt: SceneDefinition["ppt"],
115
149
  name?: string,
116
- originSize?: Size
150
+ originSize?: Size,
117
151
  ): PresentationPage | null => {
118
- if (!ppt) return null
119
- const size = fitPageSizeToOrigin(ppt, originSize)
120
- return { ...size, src: ppt.src, thumbnail: ppt.previewURL, name }
121
- }
152
+ if (!ppt) return null;
153
+ const size = fitPageSizeToOrigin(ppt, originSize);
154
+ return { ...size, src: ppt.src, thumbnail: ppt.previewURL, name };
155
+ };
122
156
 
123
157
  const createLogger = (room: Room | undefined): Logger => {
124
158
  if (room && (room as any).logger) {
125
- return (...args) => (room as any).logger.info(...args)
159
+ return (...args) => (room as any).logger.info(...args);
126
160
  } else {
127
- return (...args) => console.log(...args)
161
+ return (...args) => console.log(...args);
128
162
  }
129
- }
163
+ };
130
164
 
131
- const createDiagnosticLogger = (context: AppContext): PresentationDiagnosticLogger => {
132
- const createAppLogger = (context as any).createLogger
133
- if (typeof createAppLogger === 'function') {
134
- return createAppLogger.call(context, 'camera', { debounceTime: 300, maxWaitTime: 2000 })
165
+ const createDiagnosticLogger = (
166
+ context: AppContext,
167
+ ): PresentationDiagnosticLogger => {
168
+ const createAppLogger = (context as any).createLogger;
169
+ if (typeof createAppLogger === "function") {
170
+ return createAppLogger.call(context, "camera", {
171
+ debounceTime: 300,
172
+ maxWaitTime: 2000,
173
+ });
135
174
  }
136
175
 
137
176
  // Compatibility fallback for WindowManager versions without AppContext.createLogger().
138
- const roomLogger = (context.getRoom() as any)?.logger
139
- const prefix = `[Presentation][${context.appId}][camera]`
140
- const emit = (level: 'info' | 'warn' | 'error', event: string, ...data: unknown[]) => {
177
+ const roomLogger = (context.getRoom() as any)?.logger;
178
+ const prefix = `[Presentation][${context.appId}][camera]`;
179
+ const emit = (
180
+ level: "info" | "warn" | "error",
181
+ event: string,
182
+ ...data: unknown[]
183
+ ) => {
141
184
  try {
142
- const printer = roomLogger?.[level]
143
- if (typeof printer === 'function') printer.call(roomLogger, `${prefix}[${event}]`, ...data)
185
+ const printer = roomLogger?.[level];
186
+ if (typeof printer === "function")
187
+ printer.call(roomLogger, `${prefix}[${event}]`, ...data);
144
188
  } catch {
145
189
  // Diagnostics must never change the App API result or replace its original error.
146
190
  }
147
- }
148
- const debouncedByEvent = new Map<string, ReturnType<typeof debounce>>()
191
+ };
192
+ const debouncedByEvent = new Map<string, ReturnType<typeof debounce>>();
149
193
  const debouncedInfo = (event: string, payload?: unknown) => {
150
- let emitDebounced = debouncedByEvent.get(event)
194
+ let emitDebounced = debouncedByEvent.get(event);
151
195
  if (!emitDebounced) {
152
196
  emitDebounced = debounce(
153
- (nextPayload?: unknown) => emit('info', event, nextPayload),
197
+ (nextPayload?: unknown) => emit("info", event, nextPayload),
154
198
  300,
155
- { maxWait: 2000 }
156
- )
157
- debouncedByEvent.set(event, emitDebounced)
199
+ { maxWait: 2000 },
200
+ );
201
+ debouncedByEvent.set(event, emitDebounced);
158
202
  }
159
- emitDebounced(payload)
160
- }
203
+ emitDebounced(payload);
204
+ };
161
205
  return {
162
- info: (event, payload) => emit('info', event, payload),
163
- warn: (event, payload) => emit('warn', event, payload),
164
- error: (event, error, payload) => emit('error', event, error, payload),
206
+ info: (event, payload) => emit("info", event, payload),
207
+ warn: (event, payload) => emit("warn", event, payload),
208
+ error: (event, error, payload) => emit("error", event, error, payload),
165
209
  debouncedInfo,
166
- flush: () => debouncedByEvent.forEach(logger => logger.flush()),
167
- }
168
- }
210
+ flush: () => debouncedByEvent.forEach((logger) => logger.flush()),
211
+ };
212
+ };
169
213
 
170
214
  const safeResourceLocation = (value: string): string => {
171
- if (value.startsWith('data:')) return 'data:[omitted]'
172
- if (value.startsWith('blob:')) return 'blob:[omitted]'
215
+ if (value.startsWith("data:")) return "data:[omitted]";
216
+ if (value.startsWith("blob:")) return "blob:[omitted]";
173
217
  try {
174
- const url = new URL(value)
175
- return `${url.origin}${url.pathname}`
218
+ const url = new URL(value);
219
+ return `${url.origin}${url.pathname}`;
176
220
  } catch {
177
- return value.split('?')[0]
221
+ return value.split("?")[0];
222
+ }
223
+ };
224
+ const scenesEqual = (
225
+ scenes1?: SceneDefinition[],
226
+ scenes2?: SceneDefinition[],
227
+ ): boolean => {
228
+ if (!scenes1 || !scenes2) {
229
+ return false;
178
230
  }
179
- }
180
- const scenesEqual = (scenes1?: SceneDefinition[], scenes2?: SceneDefinition[]): boolean => {
181
- if (!scenes1 || !scenes2) {return false}
182
231
  if (scenes1.length !== scenes2.length) return false;
183
232
  return scenes1.every((scene, index) => {
184
233
  const scene2 = scenes2[index];
185
- return scene.name === scene2.name &&
186
- scene.ppt?.width === scene2.ppt?.width &&
187
- scene.ppt?.height === scene2.ppt?.height &&
188
- scene.ppt?.src === scene2.ppt?.src;
234
+ return (
235
+ scene.name === scene2.name &&
236
+ scene.ppt?.width === scene2.ppt?.width &&
237
+ scene.ppt?.height === scene2.ppt?.height &&
238
+ scene.ppt?.src === scene2.ppt?.src
239
+ );
189
240
  });
190
241
  };
191
242
 
192
- export const NetlessAppPresentation: NetlessApp<PresentationAttributes, {}, PresentationAppOptions, PresentationController> = {
243
+ const teardownByContext = new WeakMap<object, () => void>();
244
+
245
+ export const NetlessAppPresentation: NetlessApp<
246
+ PresentationAttributes,
247
+ {},
248
+ PresentationAppOptions,
249
+ PresentationController
250
+ > & {
251
+ teardown(context: AppContext): void;
252
+ } = {
193
253
  kind: "Presentation",
194
254
  setup(context) {
195
- const diagnosticLogger = createDiagnosticLogger(context)
196
- const view = context.getView()
255
+ const diagnosticLogger = createDiagnosticLogger(context);
256
+ const view = context.getView();
197
257
  if (!view)
198
- throw new Error("[Presentation]: no whiteboard view, make sure you have added options.scenePath in addApp()")
199
-
200
- const options = context.getAppOptions() || {}
201
- const room = context.getRoom()
202
- const log = options.log || createLogger(room)
203
- const roomLogger = (room as any)?.logger
204
- const warn: Logger = (...data) => roomLogger?.warn ? roomLogger.warn(...data) : log(...data)
205
- const configuredOriginSize = context.storage.state.originSize
258
+ throw new Error(
259
+ "[Presentation]: no whiteboard view, make sure you have added options.scenePath in addApp()",
260
+ );
261
+
262
+ const options = context.getAppOptions() || {};
263
+ const room = context.getRoom();
264
+ const log = options.log || createLogger(room);
265
+ const roomLogger = (room as any)?.logger;
266
+ const warn: Logger = (...data) =>
267
+ roomLogger?.warn ? roomLogger.warn(...data) : log(...data);
268
+ const configuredOriginSize = context.storage.state.originSize;
206
269
  const originSize = isValidSize(configuredOriginSize)
207
- ? { width: configuredOriginSize.width, height: configuredOriginSize.height }
208
- : undefined
270
+ ? {
271
+ width: configuredOriginSize.width,
272
+ height: configuredOriginSize.height,
273
+ }
274
+ : undefined;
209
275
  if (configuredOriginSize != null && !originSize) {
210
- warn(`[Presentation] originSize should contain finite positive width and height, got ${JSON.stringify(configuredOriginSize)}`)
276
+ warn(
277
+ `[Presentation] originSize should contain finite positive width and height, got ${JSON.stringify(configuredOriginSize)}`,
278
+ );
211
279
  }
212
- const pages = context.getScenes()
280
+ const pages = context
281
+ .getScenes()
213
282
  ?.map(({ ppt, name }) => ppt2page(ppt, name, originSize))
214
- .filter(Boolean) as PresentationPage[]
283
+ .filter(Boolean) as PresentationPage[];
215
284
  if (!pages || pages.length === 0)
216
- throw new Error("[Presentation]: empty scenes, make sure you have added options.scenes in addApp()")
217
- if (pages[0].src.startsWith('ppt'))
218
- throw new Error("[Presentation]: legacy dynamic PPT is unsupported, please use the projector converter and @netless/slide to render it")
285
+ throw new Error(
286
+ "[Presentation]: empty scenes, make sure you have added options.scenes in addApp()",
287
+ );
288
+ if (pages[0].src.startsWith("ppt"))
289
+ throw new Error(
290
+ "[Presentation]: legacy dynamic PPT is unsupported, please use the projector converter and @netless/slide to render it",
291
+ );
219
292
 
220
293
  // Now it must have a blank scene points to "{scenePath}/{scenes[0].name}", e.g. "/pdf/123456/1"
221
294
  // https://github.com/netless-io/window-manager/blob/c87df17/src/index.ts#L465-L476
222
- const scenePath = context.getInitScenePath()!
223
- let maxCameraScale = options.maxCameraScale ?? 3
295
+ const scenePath = context.getInitScenePath()!;
296
+ let maxCameraScale = options.maxCameraScale ?? 3;
224
297
  if (!(Number.isFinite(maxCameraScale) && maxCameraScale! > 0)) {
225
- warn(`[Presentation] maxCameraScale should be a positive number, got ${options.maxCameraScale}`)
226
- maxCameraScale = 3
298
+ warn(
299
+ `[Presentation] maxCameraScale should be a positive number, got ${options.maxCameraScale}`,
300
+ );
301
+ maxCameraScale = 3;
227
302
  }
228
303
 
229
- log(`[Presentation] new ${context.appId}`)
304
+ log(`[Presentation] new ${context.appId}`);
230
305
 
231
- const dispose = disposableStore()
232
- dispose.add(() => log(`[Presentation] dispose ${context.appId}`))
233
- dispose.add(() => diagnosticLogger.flush())
234
-
235
- const view$$ = context.createStorage('view', { uid: "", originX: 0, originY: 0, width: 0, height: 0 })
306
+ const dispose = disposableStore();
307
+ dispose.add(() => log(`[Presentation] dispose ${context.appId}`));
308
+ dispose.add(() => diagnosticLogger.flush());
236
309
 
310
+ const view$$ = context.createStorage("view", {
311
+ uid: "",
312
+ originX: 0,
313
+ originY: 0,
314
+ width: 0,
315
+ height: 0,
316
+ });
237
317
 
238
- const _addScenePathListener = (
239
- name: keyof PublicEvent,
240
- listener: any
241
- ) => {
242
- const windowManger = (context as any).manager.windowManger as WindowManager;
243
- windowManger.emitter.on(name, listener)
244
- return () => windowManger?.emitter.off(name, listener)
245
- }
318
+ const _addScenePathListener = (name: keyof PublicEvent, listener: any) => {
319
+ const windowManger = (context as any).manager
320
+ .windowManger as WindowManager;
321
+ windowManger.emitter.on(name, listener);
322
+ return () => windowManger?.emitter.off(name, listener);
323
+ };
246
324
 
247
325
  const getPageIndex = (view: View) => {
248
326
  const focusScenePath = view.focusScenePath;
249
- const name = focusScenePath?.split('/').pop();
327
+ const name = focusScenePath?.split("/").pop();
250
328
  let _pageIndex = pages.findIndex((page, index) => {
251
329
  const n = page.name ?? String(index + 1);
252
330
  return n === name;
@@ -255,44 +333,51 @@ export const NetlessAppPresentation: NetlessApp<PresentationAttributes, {}, Pres
255
333
  _pageIndex = 0;
256
334
  }
257
335
  return _pageIndex;
258
- }
336
+ };
259
337
 
260
338
  let pageIndex = getPageIndex(view);
261
- const pageIndex$ = readable<number>(pageIndex, set => {
339
+ const pageIndex$ = readable<number>(pageIndex, (set) => {
262
340
  set(pageIndex);
263
- return _addScenePathListener("onAppScenePathChange", (payload: AppPayload)=>{
264
- const { appId } = payload;
265
- if (appId === context.appId) {
266
- const _pageIndex = getPageIndex(payload.view);
267
- set(_pageIndex)
268
- }
269
- });
341
+ return _addScenePathListener(
342
+ "onAppScenePathChange",
343
+ (payload: AppPayload) => {
344
+ const { appId } = payload;
345
+ if (appId === context.appId) {
346
+ const _pageIndex = getPageIndex(payload.view);
347
+ set(_pageIndex);
348
+ }
349
+ },
350
+ );
270
351
  });
271
352
 
272
353
  dispose.add(() => {
273
354
  pageIndex$.dispose();
274
- })
355
+ });
275
356
 
276
357
  // let lastIndex = -1
277
358
 
278
- const me = context.getRoom()?.uid || context.getDisplayer().observerId + ''
359
+ const me = context.getRoom()?.uid || context.getDisplayer().observerId + "";
279
360
 
280
- let throttleSyncView = 0
361
+ let throttleSyncView = 0;
281
362
 
282
363
  const syncPage = async (index: number, logger?: any): Promise<boolean> => {
364
+ if (!context.getIsWritable()) return false;
283
365
 
284
- if (!context.getIsWritable()) return false
285
-
286
- const scenes = context.getDisplayer().entireScenes()[scenePath]
287
- if (!scenes) return false
366
+ const scenes = context.getDisplayer().entireScenes()[scenePath];
367
+ if (!scenes) return false;
288
368
 
289
369
  const p = pages[index];
290
370
  const name = p.name ?? String(index + 1);
291
371
 
292
372
  // "Prepare scenes" may not run correctly if the user suddenly disconnected after adding the app.
293
373
  // So here we add the missing pages again if not found. This is rare to happen.
294
- if (!scenes.some(scene => scene.name === name)) {
295
- await context.addPage({ scene: { name, ppt: { width: p.width, height: p.height, src: p.src } } })
374
+ if (!scenes.some((scene) => scene.name === name)) {
375
+ await context.addPage({
376
+ scene: {
377
+ name,
378
+ ppt: { width: p.width, height: p.height, src: p.src },
379
+ },
380
+ });
296
381
  }
297
382
 
298
383
  if (logger) {
@@ -300,125 +385,140 @@ export const NetlessAppPresentation: NetlessApp<PresentationAttributes, {}, Pres
300
385
  }
301
386
 
302
387
  // Switch to that page.
303
- await context.setScenePath(`${scenePath}/${name}`)
304
- return true
305
- }
388
+ await context.setScenePath(`${scenePath}/${name}`);
389
+ return true;
390
+ };
306
391
 
307
392
  const prepareScenes = async (): Promise<void> => {
308
- if (!context.isAddApp) return
393
+ if (!context.isAddApp) return;
309
394
  // Caution: some user may insert a 500-page PDF.
310
395
  if (pages.length > 100)
311
- warn(`[Presentation]: too many pages (${pages.length}), may cause performance issues`)
312
- if (!room || !room.isWritable) return
396
+ warn(
397
+ `[Presentation]: too many pages (${pages.length}), may cause performance issues`,
398
+ );
399
+ if (!room || !room.isWritable) return;
313
400
  if (pageIndex$.value < 0 || pageIndex$.value >= pages.length) {
314
- throw new Error(`[Presentation] Invalid page index: ${pageIndex$.value}, scenes length: ${pages.length}`)
401
+ throw new Error(
402
+ `[Presentation] Invalid page index: ${pageIndex$.value}, scenes length: ${pages.length}`,
403
+ );
315
404
  }
316
405
 
317
- const scenes = room.entireScenes()[scenePath]
406
+ const scenes = room.entireScenes()[scenePath];
318
407
  if (!scenes || !scenes[pageIndex$.value]) {
319
- throw new Error(`[Presentation]: no initial scene found at ${scenePath}, page index: ${pageIndex$.value}`)
408
+ throw new Error(
409
+ `[Presentation]: no initial scene found at ${scenePath}, page index: ${pageIndex$.value}`,
410
+ );
320
411
  }
321
- const { name, ppt } = scenes[pageIndex$.value]
412
+ const { name, ppt } = scenes[pageIndex$.value];
322
413
  const nextScenes = pages.map((page, index) => ({
323
414
  name: page.name ?? String(index + 1),
324
- ppt: { width: page.width, height: page.height, src: page.src }
325
- }))
415
+ ppt: { width: page.width, height: page.height, src: page.src },
416
+ }));
326
417
 
327
418
  if (!scenesEqual(scenes, nextScenes)) {
328
- room.removeScenes(scenePath)
329
- room.putScenes(scenePath, nextScenes)
419
+ room.removeScenes(scenePath);
420
+ room.putScenes(scenePath, nextScenes);
330
421
  }
331
422
 
332
- const shouldRedirect = name === nextScenes[pageIndex$.value].name && !ppt
423
+ const shouldRedirect = name === nextScenes[pageIndex$.value].name && !ppt;
333
424
  if (shouldRedirect) {
334
- await context.addPage({ scene: { name: emptySceneName } })
335
- log(`[Presentation] setup setScenePath ${scenePath}/${emptySceneName}`)
336
- await context.setScenePath(`${scenePath}/${emptySceneName}`)
425
+ await context.addPage({ scene: { name: emptySceneName } });
426
+ log(`[Presentation] setup setScenePath ${scenePath}/${emptySceneName}`);
427
+ await context.setScenePath(`${scenePath}/${emptySceneName}`);
337
428
  }
338
429
 
339
- await syncPage(pageIndex$.value, (room as any).logger)
430
+ await syncPage(pageIndex$.value, (room as any).logger);
340
431
  if (shouldRedirect) {
341
- log(`[Presentation] setup removeScenes ${scenePath}/${emptySceneName}`)
342
- room.removeScenes(`${scenePath}/${emptySceneName}`)
432
+ log(`[Presentation] setup removeScenes ${scenePath}/${emptySceneName}`);
433
+ room.removeScenes(`${scenePath}/${emptySceneName}`);
343
434
  }
344
- }
435
+ };
345
436
 
346
- const prepareScenesPromise = prepareScenes()
437
+ const prepareScenesPromise = prepareScenes();
347
438
 
348
439
  const canJumpPage = (index: number): boolean => {
349
440
  if (!context.getIsWritable()) {
350
- warn('[Presentation]: no permission, make sure you have test room.isWritable')
351
- return false
441
+ warn(
442
+ "[Presentation]: no permission, make sure you have test room.isWritable",
443
+ );
444
+ return false;
352
445
  }
353
446
 
354
447
  if (!(0 <= index && index < pages.length)) {
355
- warn(`[Presentation]: page ${index + 1} out of bounds [1, ${pages.length}]`)
356
- return false
448
+ warn(
449
+ `[Presentation]: page ${index + 1} out of bounds [1, ${pages.length}]`,
450
+ );
451
+ return false;
357
452
  }
358
453
 
359
- const scenes = context.getDisplayer().entireScenes()[scenePath]
454
+ const scenes = context.getDisplayer().entireScenes()[scenePath];
360
455
  if (!scenes) {
361
- warn(`[Presentation]: no scenes found at ${scenePath}, make sure you have added options.scenePath in addApp()`)
362
- return false
456
+ warn(
457
+ `[Presentation]: no scenes found at ${scenePath}, make sure you have added options.scenePath in addApp()`,
458
+ );
459
+ return false;
363
460
  }
364
461
 
365
- return true
366
- }
462
+ return true;
463
+ };
367
464
 
368
465
  const jumpPage = (index: number): boolean => {
369
- if (!canJumpPage(index)) return false
466
+ if (!canJumpPage(index)) return false;
370
467
 
371
- void syncPage(index).catch(error => {
372
- warn('[Presentation]: failed to sync page', error)
373
- diagnosticLogger.error('jumpPage.failed', error, {
468
+ void syncPage(index).catch((error) => {
469
+ warn("[Presentation]: failed to sync page", error);
470
+ diagnosticLogger.error("jumpPage.failed", error, {
374
471
  index,
375
472
  pageIndex: pageIndex$.value,
376
473
  focusScenePath: view.focusScenePath,
377
- })
378
- })
379
- return true
380
- }
474
+ });
475
+ });
476
+ return true;
477
+ };
381
478
 
382
- const prevPage = () => jumpPage(pageIndex$.value - 1)
383
- const nextPage = () => jumpPage(pageIndex$.value + 1)
479
+ const prevPage = () => jumpPage(pageIndex$.value - 1);
480
+ const nextPage = () => jumpPage(pageIndex$.value + 1);
384
481
  const jumpPageAsync = async (index: number): Promise<boolean> => {
385
- if (!canJumpPage(index)) return false
482
+ if (!canJumpPage(index)) return false;
386
483
  try {
387
- return await syncPage(index)
484
+ return await syncPage(index);
388
485
  } catch (error) {
389
- diagnosticLogger.error('jumpPageAsync.failed', error, {
486
+ diagnosticLogger.error("jumpPageAsync.failed", error, {
390
487
  index,
391
488
  pageIndex: pageIndex$.value,
392
489
  focusScenePath: view.focusScenePath,
393
- })
394
- throw error
490
+ });
491
+ throw error;
395
492
  }
396
- }
397
- const prevPageAsync = () => jumpPageAsync(pageIndex$.value - 1)
398
- const nextPageAsync = () => jumpPageAsync(pageIndex$.value + 1)
399
- const pageState = () => ({ index: pageIndex$.value, length: pages.length })
493
+ };
494
+ const prevPageAsync = () => jumpPageAsync(pageIndex$.value - 1);
495
+ const nextPageAsync = () => jumpPageAsync(pageIndex$.value + 1);
496
+ const pageState = () => ({ index: pageIndex$.value, length: pages.length });
400
497
 
401
498
  const scaleDocsToFit = () => {
402
- const page = app.page()
499
+ const page = app.page();
403
500
  if (page && isValidSize(page)) {
404
- const referenceSize = getCameraReferenceSize(originSize, page)
501
+ const referenceSize = getCameraReferenceSize(originSize, page);
405
502
  if (originSize) {
406
- const fitScale = getFitScale(view.size, referenceSize)
407
- if (!fitScale) return
503
+ const fitScale = getFitScale(view.size, referenceSize);
504
+ if (!fitScale) return;
408
505
  const { minScale, maxScale } = getCameraScaleRange(
409
506
  fitScale,
410
507
  maxCameraScale,
411
- options.disableCameraTransform
412
- )
508
+ options.disableCameraTransform,
509
+ );
413
510
  view.setCameraBound({
414
511
  damping: 1,
415
512
  maxContentMode: () => maxScale,
416
513
  minContentMode: () => minScale,
417
- centerX: 0, centerY: 0, width: page.width, height: page.height
418
- })
514
+ centerX: 0,
515
+ centerY: 0,
516
+ width: page.width,
517
+ height: page.height,
518
+ });
419
519
  if (isValidSharedViewport(view$$.state)) {
420
- syncViewFromRemote(true)
421
- return
520
+ syncViewFromRemote(true);
521
+ return;
422
522
  }
423
523
  }
424
524
  view.moveCameraToContain({
@@ -426,119 +526,155 @@ export const NetlessAppPresentation: NetlessApp<PresentationAttributes, {}, Pres
426
526
  originY: -referenceSize.height / 2,
427
527
  width: referenceSize.width,
428
528
  height: referenceSize.height,
429
- animationMode: 'immediately' as AnimationMode.Immediately
430
- })
529
+ animationMode: "immediately" as AnimationMode.Immediately,
530
+ });
431
531
  if (!originSize) {
432
532
  const { minScale, maxScale } = getCameraScaleRange(
433
533
  view.camera.scale,
434
534
  maxCameraScale,
435
- options.disableCameraTransform
436
- )
535
+ options.disableCameraTransform,
536
+ );
437
537
  view.setCameraBound({
438
538
  damping: 1,
439
539
  maxContentMode: () => maxScale,
440
540
  minContentMode: () => minScale,
441
- centerX: 0, centerY: 0, width: page.width, height: page.height
442
- })
541
+ centerX: 0,
542
+ centerY: 0,
543
+ width: page.width,
544
+ height: page.height,
545
+ });
443
546
  }
444
- syncViewFromRemote(true)
547
+ syncViewFromRemote(true);
445
548
  }
446
- }
447
- let pendingMoveCameraRequest: MoveCameraRequest | undefined
549
+ };
550
+ let pendingMoveCameraRequest: MoveCameraRequest | undefined;
448
551
  const syncView = () => {
449
552
  if (context.getIsWritable()) {
450
553
  if (options.debounceSync) {
451
554
  clearTimeout(throttleSyncView);
452
555
  throttleSyncView = 0;
453
556
  }
454
- if (throttleSyncView > 0) return
455
- const page = app.page()
557
+ if (throttleSyncView > 0) return;
558
+ const page = app.page();
456
559
  if (page && isValidSize(page)) {
457
560
  throttleSyncView = setTimeout(() => {
458
- throttleSyncView = 0
561
+ throttleSyncView = 0;
459
562
  try {
460
563
  const { camera, size } = view;
461
- const referenceSize = getCameraReferenceSize(originSize, page)
462
- const viewport = cameraToSharedViewport(camera, size, referenceSize)
463
- if (viewport) view$$.setState({ uid: me, ...viewport })
564
+ const referenceSize = getCameraReferenceSize(originSize, page);
565
+ const viewport = cameraToSharedViewport(
566
+ camera,
567
+ size,
568
+ referenceSize,
569
+ );
570
+ if (viewport) view$$.setState({ uid: me, ...viewport });
464
571
  if (pendingMoveCameraRequest) {
465
572
  diagnosticLogger.debouncedInfo(
466
- 'moveCamera',
467
- getCameraDiagnosticState('moveCamera', pendingMoveCameraRequest)
468
- )
469
- pendingMoveCameraRequest = undefined
573
+ "moveCamera",
574
+ getCameraDiagnosticState(
575
+ "moveCamera",
576
+ pendingMoveCameraRequest,
577
+ ),
578
+ );
579
+ pendingMoveCameraRequest = undefined;
470
580
  }
471
581
  } catch (error) {
472
582
  diagnosticLogger.error(
473
- 'syncView.failed',
583
+ "syncView.failed",
474
584
  error,
475
- getCameraDiagnosticState('moveCamera', pendingMoveCameraRequest)
476
- )
477
- pendingMoveCameraRequest = undefined
585
+ getCameraDiagnosticState(
586
+ "moveCamera",
587
+ pendingMoveCameraRequest,
588
+ ),
589
+ );
590
+ pendingMoveCameraRequest = undefined;
478
591
  }
479
- }, 50)
592
+ }, 50);
480
593
  }
481
594
  }
482
- }
595
+ };
483
596
 
484
597
  dispose.add(() => {
485
- clearTimeout(throttleSyncView)
486
- throttleSyncView = 0
487
- pendingMoveCameraRequest = undefined
488
- })
598
+ clearTimeout(throttleSyncView);
599
+ throttleSyncView = 0;
600
+ pendingMoveCameraRequest = undefined;
601
+ });
489
602
 
490
603
  const syncViewFromRemote = (force = false, animate = false) => {
491
- const { uid, originX, originY, width, height } = view$$.state
604
+ const { uid, originX, originY, width, height } = view$$.state;
492
605
  if ((force || uid !== me) && width > 0 && height > 0) {
493
606
  view.moveCameraToContain({
494
- originX, originY, width, height,
495
- animationMode: (animate ? 'continuous' : 'immediately') as AnimationMode
496
- })
607
+ originX,
608
+ originY,
609
+ width,
610
+ height,
611
+ animationMode: (animate
612
+ ? "continuous"
613
+ : "immediately") as AnimationMode,
614
+ });
497
615
  }
498
- }
616
+ };
499
617
 
500
- dispose.add(view$$.addStateChangedListener(() => syncViewFromRemote(false, true)))
618
+ dispose.add(
619
+ view$$.addStateChangedListener(() => syncViewFromRemote(false, true)),
620
+ );
501
621
 
502
622
  const getPageSize = () => {
503
- const { width, height } = app.page() || {}
504
- return { width: width || 0, height: height || 0 }
505
- }
506
-
507
- const box = context.getBox()
508
- const app = dispose.add(createPresentation(box, pages, jumpPage, pageIndex$, view, options.thumbnail, options.useClipView))
509
- app.contentDOM.dataset.appPresentationVersion = __VERSION__
510
- app.scaleDocsToFit = scaleDocsToFit
511
- app.log = log
512
- app.warn = warn
623
+ const { width, height } = app.page() || {};
624
+ return { width: width || 0, height: height || 0 };
625
+ };
626
+
627
+ const box = context.getBox();
628
+ const app = dispose.add(
629
+ createPresentation(
630
+ box,
631
+ pages,
632
+ jumpPage,
633
+ pageIndex$,
634
+ view,
635
+ options.thumbnail,
636
+ options.useClipView,
637
+ ),
638
+ );
639
+ app.contentDOM.dataset.appPresentationVersion = __VERSION__;
640
+ app.scaleDocsToFit = scaleDocsToFit;
641
+ app.log = log;
642
+ app.warn = warn;
513
643
 
514
644
  const getCameraDiagnosticState = (
515
- reason: 'initialize' | 'moveCamera',
516
- requestedCamera?: MoveCameraRequest
645
+ reason: "initialize" | "moveCamera",
646
+ requestedCamera?: MoveCameraRequest,
517
647
  ) => {
518
- const page = app.page()
519
- const pageSize = page && isValidSize(page)
520
- ? { width: page.width, height: page.height }
521
- : undefined
648
+ const page = app.page();
649
+ const pageSize =
650
+ page && isValidSize(page)
651
+ ? { width: page.width, height: page.height }
652
+ : undefined;
522
653
  const referenceSize = pageSize
523
654
  ? getCameraReferenceSize(originSize, pageSize)
524
- : undefined
525
- const viewSize = { width: view.size.width, height: view.size.height }
655
+ : undefined;
656
+ const viewSize = { width: view.size.width, height: view.size.height };
526
657
  const viewCamera = {
527
658
  centerX: view.camera.centerX,
528
659
  centerY: view.camera.centerY,
529
660
  scale: view.camera.scale,
530
- }
531
- const sharedViewport = { ...view$$.state }
532
- const originScale = referenceSize ? getFitScale(viewSize, referenceSize) : undefined
533
- const normalizedScale = originScale && originScale > 0
534
- ? viewCamera.scale / originScale
535
- : undefined
536
- const sharedScaleX = referenceSize && sharedViewport.width > 0
537
- ? referenceSize.width / sharedViewport.width
538
- : undefined
539
- const sharedScaleY = referenceSize && sharedViewport.height > 0
540
- ? referenceSize.height / sharedViewport.height
541
- : undefined
661
+ };
662
+ const sharedViewport = { ...view$$.state };
663
+ const originScale = referenceSize
664
+ ? getFitScale(viewSize, referenceSize)
665
+ : undefined;
666
+ const normalizedScale =
667
+ originScale && originScale > 0
668
+ ? viewCamera.scale / originScale
669
+ : undefined;
670
+ const sharedScaleX =
671
+ referenceSize && sharedViewport.width > 0
672
+ ? referenceSize.width / sharedViewport.width
673
+ : undefined;
674
+ const sharedScaleY =
675
+ referenceSize && sharedViewport.height > 0
676
+ ? referenceSize.height / sharedViewport.height
677
+ : undefined;
542
678
 
543
679
  return {
544
680
  reason,
@@ -555,367 +691,531 @@ export const NetlessAppPresentation: NetlessApp<PresentationAttributes, {}, Pres
555
691
  sharedScaleY,
556
692
  focusScenePath: view.focusScenePath,
557
693
  isWritable: context.getIsWritable(),
558
- }
559
- }
560
-
561
- let didReportInitializedCamera = false
562
- const reportInitializedCamera = (source: 'setup' | 'onSizeUpdated') => {
563
- if (didReportInitializedCamera || !isValidSize(view.size) || !isValidSize(app.page())) return
564
- didReportInitializedCamera = true
565
- diagnosticLogger.info('initialize', {
694
+ };
695
+ };
696
+
697
+ let didReportInitializedCamera = false;
698
+ const reportInitializedCamera = (source: "setup" | "onSizeUpdated") => {
699
+ if (
700
+ didReportInitializedCamera ||
701
+ !isValidSize(view.size) ||
702
+ !isValidSize(app.page())
703
+ )
704
+ return;
705
+ didReportInitializedCamera = true;
706
+ diagnosticLogger.info("initialize", {
566
707
  source,
567
- ...getCameraDiagnosticState('initialize'),
568
- })
569
- }
708
+ ...getCameraDiagnosticState("initialize"),
709
+ });
710
+ };
570
711
 
571
712
  if (originSize) {
572
- let previousPageIndex = pageIndex$.value
573
- dispose.add(pageIndex$.subscribe(nextPageIndex => {
574
- if (nextPageIndex === previousPageIndex) return
575
- previousPageIndex = nextPageIndex
576
- scaleDocsToFit()
577
- }))
713
+ let previousPageIndex = pageIndex$.value;
714
+ dispose.add(
715
+ pageIndex$.subscribe((nextPageIndex) => {
716
+ if (nextPageIndex === previousPageIndex) return;
717
+ previousPageIndex = nextPageIndex;
718
+ scaleDocsToFit();
719
+ }),
720
+ );
578
721
  }
579
722
 
580
723
  if (options.justDocsViewReadonly) {
581
- app.setDocsViewReadonly(true)
724
+ app.setDocsViewReadonly(true);
582
725
  }
583
726
 
584
727
  const goToPageByClick = () => {
585
- const currentApplianceName = context.getRoom()?.state?.memberState?.currentApplianceName ?? '';
586
- if (!app.readonly && currentApplianceName === 'clicker') {
728
+ const currentApplianceName =
729
+ context.getRoom()?.state?.memberState?.currentApplianceName ?? "";
730
+ if (!app.readonly && currentApplianceName === "clicker") {
587
731
  nextPage();
588
732
  }
589
- }
733
+ };
590
734
 
591
- if(room && options.goToPageByClick && app.whiteboardDOM) {
735
+ if (room && options.goToPageByClick && app.whiteboardDOM) {
592
736
  dispose.make(() => {
593
- app.whiteboardDOM.addEventListener('click', goToPageByClick);
594
- return () => app.whiteboardDOM.removeEventListener('click', goToPageByClick);
595
- })
737
+ app.whiteboardDOM.addEventListener("click", goToPageByClick);
738
+ return () =>
739
+ app.whiteboardDOM.removeEventListener("click", goToPageByClick);
740
+ });
596
741
  }
597
742
 
598
- context.mountView(app.whiteboardDOM)
743
+ context.mountView(app.whiteboardDOM);
599
744
  if (shouldDisableDeviceCameraTransform(options)) {
600
- view.disableCameraTransform = true
745
+ view.disableCameraTransform = true;
601
746
  }
602
- scaleDocsToFit()
747
+ scaleDocsToFit();
603
748
  dispose.make(() => {
604
749
  const onSizeUpdated = () => {
605
- scaleDocsToFit()
606
- reportInitializedCamera('onSizeUpdated')
607
- }
608
- view.callbacks.on('onSizeUpdated', onSizeUpdated)
609
- return () => view.callbacks.off('onSizeUpdated', onSizeUpdated)
610
- })
750
+ scaleDocsToFit();
751
+ reportInitializedCamera("onSizeUpdated");
752
+ };
753
+ view.callbacks.on("onSizeUpdated", onSizeUpdated);
754
+ return () => view.callbacks.off("onSizeUpdated", onSizeUpdated);
755
+ });
611
756
 
612
757
  // Init viewport if provided `viewport`.
613
758
  if (options.viewport && context.isAddApp && app.page()) {
614
- const page = app.page()!
615
- const viewport = typeof options.viewport === 'function' ? options.viewport(page) : options.viewport
616
- const fix = (i: number, x: number) => i == 0 ? i : i <= 1 ? i * x : i
759
+ const page = app.page()!;
760
+ const viewport =
761
+ typeof options.viewport === "function"
762
+ ? options.viewport(page)
763
+ : options.viewport;
764
+ const fix = (i: number, x: number) => (i == 0 ? i : i <= 1 ? i * x : i);
617
765
  view$$.setState({
618
766
  uid: me,
619
767
  originX: fix(viewport.x, page.width) - page.width / 2,
620
768
  originY: fix(viewport.y, page.height) - page.height / 2,
621
769
  width: fix(viewport.width, page.width),
622
770
  height: fix(viewport.height, page.height),
623
- })
771
+ });
624
772
  }
625
773
 
626
774
  dispose.make(() => {
627
- view.callbacks.on('onCameraUpdatedByDevice', syncView)
628
- return () => view.callbacks.off('onCameraUpdatedByDevice', syncView)
629
- })
775
+ view.callbacks.on("onCameraUpdatedByDevice", syncView);
776
+ return () => view.callbacks.off("onCameraUpdatedByDevice", syncView);
777
+ });
630
778
 
631
- syncViewFromRemote(true)
632
- reportInitializedCamera('setup')
779
+ syncViewFromRemote(true);
780
+ reportInitializedCamera("setup");
633
781
 
634
- dispose.add(context.emitter.on("writableChange", (isWritable: boolean): void => {
635
- app.setReadonly(!isWritable)
636
- }))
782
+ dispose.add(
783
+ context.emitter.on("writableChange", (isWritable: boolean): void => {
784
+ app.setReadonly(!isWritable);
785
+ }),
786
+ );
637
787
 
638
788
  const getOriginScale = () => {
639
- const page = app.page()
640
- if (!page || !isValidSize(page)) return 0
641
- return getFitScale(view.size, getCameraReferenceSize(originSize, page)) || 0
642
- }
643
-
644
- const getScale =() => {
645
- return view.camera.scale
646
- }
647
-
648
- const screenshotCurrentPage = async (_context: CanvasRenderingContext2D, _width?: number, _height?: number) => {
789
+ const page = app.page();
790
+ if (!page || !isValidSize(page)) return 0;
791
+ return (
792
+ getFitScale(view.size, getCameraReferenceSize(originSize, page)) || 0
793
+ );
794
+ };
795
+
796
+ const getScale = () => {
797
+ return view.camera.scale;
798
+ };
799
+
800
+ const screenshotCurrentPage = async (
801
+ _context: CanvasRenderingContext2D,
802
+ _width?: number,
803
+ _height?: number,
804
+ ) => {
649
805
  const currentPage = pages[pageIndex$.value];
650
806
  if (!currentPage) {
651
- throw new Error('[Presentation]: current page not found')
807
+ throw new Error("[Presentation]: current page not found");
652
808
  }
653
809
  const { width, height, src } = currentPage;
654
810
 
655
- const img = document.createElement('img')
811
+ const img = document.createElement("img");
656
812
  img.width = width;
657
813
  img.height = height;
658
- img.crossOrigin = 'Anonymous';
814
+ img.crossOrigin = "Anonymous";
659
815
  await new Promise<void>((resolve, reject) => {
660
- img.onload = () => resolve()
661
- img.onerror = () => reject(new Error(
662
- `[Presentation]: failed to load screenshot page image: ${safeResourceLocation(src)}`
663
- ))
664
- img.src = src
665
- })
666
- _context.drawImage(img, 0, 0, width, height, 0, 0, _width || width, _height || height);
816
+ img.onload = () => resolve();
817
+ img.onerror = () =>
818
+ reject(
819
+ new Error(
820
+ `[Presentation]: failed to load screenshot page image: ${safeResourceLocation(src)}`,
821
+ ),
822
+ );
823
+ img.src = src;
824
+ });
825
+ _context.drawImage(
826
+ img,
827
+ 0,
828
+ 0,
829
+ width,
830
+ height,
831
+ 0,
832
+ 0,
833
+ _width || width,
834
+ _height || height,
835
+ );
667
836
  const currentScenePath = view.focusScenePath;
668
837
  if (!currentScenePath) {
669
- throw new Error('[Presentation]: current scene path not found')
838
+ throw new Error("[Presentation]: current scene path not found");
670
839
  }
671
840
  const windowManger = context.getWindowManager() as any;
672
841
  if (windowManger._appliancePlugin) {
673
- await windowManger._appliancePlugin.screenshotToCanvasAsync(_context, currentScenePath, _width || width, _height || height, {
674
- centerX: 0,
675
- centerY: 0,
676
- scale: _width && _height ? Math.min(_width / width, _height / height) : 1,
677
- });
842
+ await windowManger._appliancePlugin.screenshotToCanvasAsync(
843
+ _context,
844
+ currentScenePath,
845
+ _width || width,
846
+ _height || height,
847
+ {
848
+ centerX: 0,
849
+ centerY: 0,
850
+ scale:
851
+ _width && _height
852
+ ? Math.min(_width / width, _height / height)
853
+ : 1,
854
+ },
855
+ );
678
856
  } else {
679
- await view.screenshotToCanvasAsync(_context, currentScenePath, _width || width, _height || height, {
680
- centerX: 0,
681
- centerY: 0,
682
- scale: _width && _height ? Math.min(_width / width, _height / height) : 1,
683
- });
857
+ await view.screenshotToCanvasAsync(
858
+ _context,
859
+ currentScenePath,
860
+ _width || width,
861
+ _height || height,
862
+ {
863
+ centerX: 0,
864
+ centerY: 0,
865
+ scale:
866
+ _width && _height
867
+ ? Math.min(_width / width, _height / height)
868
+ : 1,
869
+ },
870
+ );
684
871
  }
685
- }
872
+ };
686
873
 
687
874
  const screenshotCurrentPageAsync = async (
688
875
  _context: CanvasRenderingContext2D,
689
876
  _width?: number,
690
- _height?: number
877
+ _height?: number,
691
878
  ) => {
692
879
  try {
693
- await screenshotCurrentPage(_context, _width, _height)
880
+ await screenshotCurrentPage(_context, _width, _height);
694
881
  } catch (error) {
695
- diagnosticLogger.error('screenshotCurrentPage.failed', error, {
882
+ diagnosticLogger.error("screenshotCurrentPage.failed", error, {
696
883
  pageIndex: pageIndex$.value,
697
884
  outputSize: { width: _width, height: _height },
698
- camera: getCameraDiagnosticState('initialize'),
699
- })
700
- throw error
885
+ camera: getCameraDiagnosticState("initialize"),
886
+ });
887
+ throw error;
701
888
  }
702
- }
889
+ };
703
890
 
704
- let scrollbar:Scrollbar | undefined;
891
+ let scrollbar: Scrollbar | undefined;
705
892
  if (options.useScrollbar) {
706
893
  dispose.make(() => {
707
- scrollbar = new Scrollbar(app.contentDOM, {
708
- appId: context.appId,
709
- getPageSize,
710
- getOriginScale,
711
- syncView,
712
- getWritable: () => context.getIsWritable(),
713
- scrollbarEventCallback: options.scrollbarEventCallback
714
- }, view)
715
- return () => scrollbar?.destroy()
716
- })
894
+ scrollbar = new Scrollbar(
895
+ app.contentDOM,
896
+ {
897
+ appId: context.appId,
898
+ getPageSize,
899
+ getOriginScale,
900
+ syncView,
901
+ getWritable: () => context.getIsWritable(),
902
+ scrollbarEventCallback: options.scrollbarEventCallback,
903
+ },
904
+ view,
905
+ );
906
+ return () => scrollbar?.destroy();
907
+ });
717
908
  }
718
909
 
719
- const moveCamera = (camera: { centerX: number, centerY: number, scale: number }) => {
910
+ const moveCamera = (camera: {
911
+ centerX: number;
912
+ centerY: number;
913
+ scale: number;
914
+ }) => {
720
915
  try {
721
916
  if (!context.getIsWritable()) {
722
- throw new Error('[Presentation]: moveCamera must be called in writable room')
917
+ throw new Error(
918
+ "[Presentation]: moveCamera must be called in writable room",
919
+ );
723
920
  }
724
- pendingMoveCameraRequest = { ...camera }
921
+ pendingMoveCameraRequest = { ...camera };
725
922
  if (scrollbar) {
726
923
  scrollbar.moveCamera(camera);
727
924
  return;
728
925
  }
729
926
  view.moveCamera({
730
927
  ...camera,
731
- animationMode: 'immediately' as AnimationMode.Immediately
732
- })
733
- syncView()
928
+ animationMode: "immediately" as AnimationMode.Immediately,
929
+ });
930
+ syncView();
734
931
  } catch (error) {
735
- pendingMoveCameraRequest = undefined
932
+ pendingMoveCameraRequest = undefined;
736
933
  diagnosticLogger.error(
737
- 'moveCamera.failed',
934
+ "moveCamera.failed",
738
935
  error,
739
- getCameraDiagnosticState('moveCamera', camera)
740
- )
741
- throw error
936
+ getCameraDiagnosticState("moveCamera", camera),
937
+ );
938
+ throw error;
742
939
  }
743
- }
744
-
745
- context.emitter.on('destroy', () => dispose())
746
-
747
- const reportProgress = (progress: number, result: { pdf: ArrayBuffer, title: string } | null) => {
748
- window.postMessage({ type: '@netless/_result_save_pdf_', appId: context.appId, progress, result }, '*')
749
- return result
750
- }
940
+ };
941
+
942
+ let disposed = false;
943
+ let offDestroy: (() => void) | undefined;
944
+ const teardown = () => {
945
+ if (disposed) return;
946
+ disposed = true;
947
+ const removeDestroy = offDestroy;
948
+ offDestroy = undefined;
949
+ removeDestroy?.();
950
+ dispose();
951
+ };
952
+ offDestroy = context.emitter.on("destroy", teardown);
953
+ teardownByContext.set(context, teardown);
954
+
955
+ const reportProgress = (
956
+ progress: number,
957
+ result: { pdf: ArrayBuffer; title: string } | null,
958
+ ) => {
959
+ window.postMessage(
960
+ {
961
+ type: "@netless/_result_save_pdf_",
962
+ appId: context.appId,
963
+ progress,
964
+ result,
965
+ },
966
+ "*",
967
+ );
968
+ return result;
969
+ };
751
970
 
752
971
  const base64url = async (url: string): Promise<string> => {
753
972
  try {
754
- const a = new URL(url)
755
- a.searchParams.set('t', Date.now().toString())
756
- url = a.toString()
973
+ const a = new URL(url);
974
+ a.searchParams.set("t", Date.now().toString());
975
+ url = a.toString();
757
976
  } catch {}
758
977
 
759
- const data = await fetch(url)
978
+ const data = await fetch(url);
760
979
  if (!data.ok) {
761
980
  throw new Error(
762
- `[Presentation]: failed to fetch ${safeResourceLocation(url)}, status: ${data.status} ${data.statusText}`
763
- )
981
+ `[Presentation]: failed to fetch ${safeResourceLocation(url)}, status: ${data.status} ${data.statusText}`,
982
+ );
764
983
  }
765
984
 
766
- const blob = await data.blob()
767
- const reader = new FileReader()
985
+ const blob = await data.blob();
986
+ const reader = new FileReader();
768
987
  return new Promise((resolve, reject) => {
769
- reader.onload = () => resolve(reader.result as string)
770
- reader.onerror = () => reject(reader.error)
771
- reader.readAsDataURL(blob)
772
- })
773
- }
774
-
775
- const toPdfInternal = async (): Promise<{ pdf: ArrayBuffer, title: string } | null> => {
776
- const MAX = 1920
777
- const firstPage = pages[0]
778
- const { width, height } = firstPage
779
- let pdfWidth = Math.floor(width), pdfHeight = Math.floor(height)
988
+ reader.onload = () => resolve(reader.result as string);
989
+ reader.onerror = () => reject(reader.error);
990
+ reader.readAsDataURL(blob);
991
+ });
992
+ };
993
+
994
+ const toPdfInternal = async (): Promise<{
995
+ pdf: ArrayBuffer;
996
+ title: string;
997
+ } | null> => {
998
+ const MAX = 1920;
999
+ const firstPage = pages[0];
1000
+ const { width, height } = firstPage;
1001
+ let pdfWidth = Math.floor(width),
1002
+ pdfHeight = Math.floor(height);
780
1003
  if (pdfWidth > MAX) {
781
- pdfWidth = MAX
782
- pdfHeight = Math.floor(height * pdfWidth / width)
1004
+ pdfWidth = MAX;
1005
+ pdfHeight = Math.floor((height * pdfWidth) / width);
783
1006
  }
784
1007
  if (pdfHeight > MAX) {
785
- pdfHeight = MAX
786
- pdfWidth = Math.floor(width * pdfHeight / height)
1008
+ pdfHeight = MAX;
1009
+ pdfWidth = Math.floor((width * pdfHeight) / height);
787
1010
  }
788
- const scenes = context.getDisplayer().entireScenes()[scenePath]
1011
+ const scenes = context.getDisplayer().entireScenes()[scenePath];
789
1012
  if (!scenes) {
790
- throw new Error(`[Presentation]: no scenes found while exporting PDF: ${scenePath}`)
1013
+ throw new Error(
1014
+ `[Presentation]: no scenes found while exporting PDF: ${scenePath}`,
1015
+ );
791
1016
  }
792
1017
 
793
- const stage_canvas = document.createElement('canvas')
794
- stage_canvas.width = pdfWidth
795
- stage_canvas.height = pdfHeight
796
- const stage = stage_canvas.getContext('2d')
1018
+ const stage_canvas = document.createElement("canvas");
1019
+ stage_canvas.width = pdfWidth;
1020
+ stage_canvas.height = pdfHeight;
1021
+ const stage = stage_canvas.getContext("2d");
797
1022
 
798
- const wb_canvas = document.createElement('canvas')
799
- wb_canvas.width = pdfWidth
800
- wb_canvas.height = pdfHeight
801
- const wb = wb_canvas.getContext('2d')
802
- if (!wb || !stage) return reportProgress(100, null)
1023
+ const wb_canvas = document.createElement("canvas");
1024
+ wb_canvas.width = pdfWidth;
1025
+ wb_canvas.height = pdfHeight;
1026
+ const wb = wb_canvas.getContext("2d");
1027
+ if (!wb || !stage) return reportProgress(100, null);
803
1028
 
804
- const { jsPDF } = await import("jspdf")
1029
+ const { jsPDF } = await import("jspdf");
805
1030
  const pdf = new jsPDF({
806
1031
  format: [firstPage.width, firstPage.height],
807
- orientation: firstPage.width > firstPage.height ? 'l' : 'p',
1032
+ orientation: firstPage.width > firstPage.height ? "l" : "p",
808
1033
  compress: true,
809
- })
1034
+ });
810
1035
 
811
1036
  for (let index = 0; index < pages.length; ++index) {
812
1037
  const p = pages[index];
813
1038
  const { width, height, src } = p;
814
1039
 
815
- const url = await base64url(src)
816
- const img = document.createElement('img')
1040
+ const url = await base64url(src);
1041
+ const img = document.createElement("img");
817
1042
  await new Promise<void>((resolve, reject) => {
818
- img.onload = () => resolve()
819
- img.onerror = () => reject(new Error(
820
- `[Presentation]: failed to load PDF page image, page index: ${index}`
821
- ))
822
- img.src = url
823
- })
824
- stage.drawImage(img, 0, 0, width, height)
825
-
826
- wb.clearRect(0, 0, pdfWidth, pdfHeight)
827
- const name = p.name ?? String(index + 1)
828
- if (scenes.some(scene => scene.name == name)) {
829
- const camera = { centerX: 0, centerY: 0, scale: Math.min(wb_canvas.width / width, wb_canvas.height / height) };
1043
+ img.onload = () => resolve();
1044
+ img.onerror = () =>
1045
+ reject(
1046
+ new Error(
1047
+ `[Presentation]: failed to load PDF page image, page index: ${index}`,
1048
+ ),
1049
+ );
1050
+ img.src = url;
1051
+ });
1052
+ stage.drawImage(img, 0, 0, width, height);
1053
+
1054
+ wb.clearRect(0, 0, pdfWidth, pdfHeight);
1055
+ const name = p.name ?? String(index + 1);
1056
+ if (scenes.some((scene) => scene.name == name)) {
1057
+ const camera = {
1058
+ centerX: 0,
1059
+ centerY: 0,
1060
+ scale: Math.min(wb_canvas.width / width, wb_canvas.height / height),
1061
+ };
830
1062
  const sPath = `${scenePath}/${name}`;
831
1063
  // appliancePlugin is a performance optimization for whiteboard;
832
1064
  const windowManger = context.getWindowManager() as any;
833
1065
  if (windowManger._appliancePlugin) {
834
- await (windowManger as any)._appliancePlugin.screenshotToCanvasAsync(wb, sPath, wb_canvas.width, wb_canvas.height, camera);
1066
+ await (
1067
+ windowManger as any
1068
+ )._appliancePlugin.screenshotToCanvasAsync(
1069
+ wb,
1070
+ sPath,
1071
+ wb_canvas.width,
1072
+ wb_canvas.height,
1073
+ camera,
1074
+ );
835
1075
  } else {
836
1076
  await view.screenshotToCanvasAsync(
837
- wb, sPath,
838
- wb_canvas.width, wb_canvas.height,
1077
+ wb,
1078
+ sPath,
1079
+ wb_canvas.width,
1080
+ wb_canvas.height,
839
1081
  camera,
840
- )
1082
+ );
841
1083
  }
842
1084
 
843
1085
  try {
844
- const wb_url = wb_canvas.toDataURL('image/png')
845
- const wb_img = document.createElement('img')
846
- await new Promise(resolve => { wb_img.onload = resolve; wb_img.src = wb_url })
847
- stage.drawImage(wb_img, 0, 0, pdfWidth, pdfHeight)
1086
+ const wb_url = wb_canvas.toDataURL("image/png");
1087
+ const wb_img = document.createElement("img");
1088
+ await new Promise((resolve) => {
1089
+ wb_img.onload = resolve;
1090
+ wb_img.src = wb_url;
1091
+ });
1092
+ stage.drawImage(wb_img, 0, 0, pdfWidth, pdfHeight);
848
1093
  } catch (err) {
849
- warn(err)
1094
+ warn(err);
850
1095
  }
851
1096
  }
852
1097
 
853
- const output = stage_canvas.toDataURL('image/jpeg', 0.6)
854
- if (index > 0) pdf.addPage()
1098
+ const output = stage_canvas.toDataURL("image/jpeg", 0.6);
1099
+ if (index > 0) pdf.addPage();
855
1100
 
856
- pdf.addImage(output, 'JPEG', 0, 0, pdfWidth, pdfHeight, "", "FAST")
857
- stage.clearRect(0, 0, pdfWidth, pdfHeight)
858
- const progress = Math.ceil((index + 1) / pages.length * 100)
859
- if (progress < 100) reportProgress(progress, null)
1101
+ pdf.addImage(output, "JPEG", 0, 0, pdfWidth, pdfHeight, "", "FAST");
1102
+ stage.clearRect(0, 0, pdfWidth, pdfHeight);
1103
+ const progress = Math.ceil(((index + 1) / pages.length) * 100);
1104
+ if (progress < 100) reportProgress(progress, null);
860
1105
  }
861
1106
 
862
- const data = pdf.output('arraybuffer')
863
- const title = box.title
864
- return reportProgress(100, { pdf: data, title })
865
- }
1107
+ const data = pdf.output("arraybuffer");
1108
+ const title = box.title;
1109
+ return reportProgress(100, { pdf: data, title });
1110
+ };
866
1111
 
867
- const toPdf = async (): Promise<{ pdf: ArrayBuffer, title: string } | null> => {
1112
+ const toPdf = async (): Promise<{
1113
+ pdf: ArrayBuffer;
1114
+ title: string;
1115
+ } | null> => {
868
1116
  try {
869
- return await toPdfInternal()
1117
+ return await toPdfInternal();
870
1118
  } catch (error) {
871
- diagnosticLogger.error('toPdf.failed', error, {
1119
+ diagnosticLogger.error("toPdf.failed", error, {
872
1120
  pageIndex: pageIndex$.value,
873
1121
  pageCount: pages.length,
874
1122
  focusScenePath: view.focusScenePath,
875
- })
876
- throw error
877
- }
878
- }
879
-
880
- dispose.add(listen(window, 'message', (ev: MessageEvent<{ appId: string, type: "@netless/_request_save_pdf_" }>) => {
881
- if (ev.data && ev.data.type == '@netless/_request_save_pdf_' && ev.data.appId == context.appId) {
882
- toPdf().catch(err => { warn(err); reportProgress(100, null) })
1123
+ });
1124
+ throw error;
883
1125
  }
884
- }))
1126
+ };
1127
+
1128
+ dispose.add(
1129
+ listen(
1130
+ window,
1131
+ "message",
1132
+ (
1133
+ ev: MessageEvent<{
1134
+ appId: string;
1135
+ type: "@netless/_request_save_pdf_";
1136
+ }>,
1137
+ ) => {
1138
+ if (
1139
+ ev.data &&
1140
+ ev.data.type == "@netless/_request_save_pdf_" &&
1141
+ ev.data.appId == context.appId
1142
+ ) {
1143
+ toPdf().catch((err) => {
1144
+ warn(err);
1145
+ reportProgress(100, null);
1146
+ });
1147
+ }
1148
+ },
1149
+ ),
1150
+ );
885
1151
 
886
1152
  const setDocsViewReadonly = (bol: boolean) => {
887
- app.setDocsViewReadonly(bol)
888
- }
1153
+ app.setDocsViewReadonly(bol);
1154
+ };
889
1155
 
890
1156
  const setReadonly = (bol: boolean) => {
891
- app.setReadonly(bol)
1157
+ app.setReadonly(bol);
892
1158
  if (options.goToPageByClick) {
893
-
894
1159
  }
895
- if(scrollbar) {
896
- scrollbar.setReadonly(bol)
1160
+ if (scrollbar) {
1161
+ scrollbar.setReadonly(bol);
897
1162
  }
898
- }
899
-
900
- const controller: PresentationController = { app, view, context, jumpPage, prevPage, nextPage, jumpPageAsync, prevPageAsync, nextPageAsync, pageState, toPdf, log, setDocsViewReadonly, setReadonly, moveCamera, getOriginScale, getScale, getPageSize, screenshotCurrentPageAsync }
901
-
902
- dispose.add(listen(window, 'message', (ev: MessageEvent<"@netless/_presentation_">) => {
903
- if (ev.data === "@netless/_presentation_") {
904
- if (typeof window !== 'undefined') dispose.make(() => {
905
- const debug: Set<PresentationController> = ((window as any)._presentation_ ||= new Set())
906
- debug.add(controller)
907
- return () => debug.delete(controller)
908
- })
909
- console.log(controller)
910
- }
911
- }))
1163
+ };
1164
+
1165
+ const controller: PresentationController = {
1166
+ app,
1167
+ view,
1168
+ context,
1169
+ jumpPage,
1170
+ prevPage,
1171
+ nextPage,
1172
+ jumpPageAsync,
1173
+ prevPageAsync,
1174
+ nextPageAsync,
1175
+ pageState,
1176
+ toPdf,
1177
+ log,
1178
+ setDocsViewReadonly,
1179
+ setReadonly,
1180
+ moveCamera,
1181
+ getOriginScale,
1182
+ getScale,
1183
+ getPageSize,
1184
+ screenshotCurrentPageAsync,
1185
+ };
1186
+
1187
+ dispose.add(
1188
+ listen(
1189
+ window,
1190
+ "message",
1191
+ (ev: MessageEvent<"@netless/_presentation_">) => {
1192
+ if (ev.data === "@netless/_presentation_") {
1193
+ if (typeof window !== "undefined")
1194
+ dispose.make(() => {
1195
+ const debug: Set<PresentationController> = ((
1196
+ window as any
1197
+ )._presentation_ ||= new Set());
1198
+ debug.add(controller);
1199
+ return () => debug.delete(controller);
1200
+ });
1201
+ console.log(controller);
1202
+ }
1203
+ },
1204
+ ),
1205
+ );
912
1206
 
913
1207
  // Older WindowManager declarations model setup as synchronous even though
914
1208
  // AppProxy awaits its result. Keep that source compatibility until the new
915
1209
  // `SetupResult | Promise<SetupResult>` declaration is the minimum version.
916
- return prepareScenesPromise.then(() => controller) as unknown as PresentationController
917
- }
918
- }
1210
+ return prepareScenesPromise.then(
1211
+ () => controller,
1212
+ ) as unknown as PresentationController;
1213
+ },
1214
+ teardown(context) {
1215
+ teardownByContext.get(context)?.();
1216
+ teardownByContext.delete(context);
1217
+ },
1218
+ };
919
1219
 
920
1220
  /**
921
1221
  * Add synchronization to the local presentation.
@@ -925,11 +1225,13 @@ class AppPresentation extends Presentation {
925
1225
  warn?: Logger;
926
1226
  box?: ReadonlyTeleBox;
927
1227
  scaleDocsToFit?: () => void;
928
- readonly jumpPage: (index: number) => void
1228
+ readonly jumpPage: (index: number) => void;
929
1229
 
930
- constructor(config: PresentationConfig & { jumpPage: (index: number) => void }) {
931
- super(config)
932
- this.jumpPage = config.jumpPage
1230
+ constructor(
1231
+ config: PresentationConfig & { jumpPage: (index: number) => void },
1232
+ ) {
1233
+ super(config);
1234
+ this.jumpPage = config.jumpPage;
933
1235
  // this.image.style.display = 'none'
934
1236
  }
935
1237
 
@@ -938,17 +1240,21 @@ class AppPresentation extends Presentation {
938
1240
  super.updateImage();
939
1241
  }
940
1242
 
941
- override onNewPageIndex(index: number, origin: "navigation" | "keydown" | "input" | "preview") {
1243
+ override onNewPageIndex(
1244
+ index: number,
1245
+ origin: "navigation" | "keydown" | "input" | "preview",
1246
+ ) {
942
1247
  // If it is triggered by global keydown (left or right arrow),
943
1248
  // only the focused one should work
944
- if (origin === "keydown" && this.box && !this.box.focus)
945
- return
1249
+ if (origin === "keydown" && this.box && !this.box.focus) return;
946
1250
  if (this.log)
947
- this.log("[Presentation] user navigate to", index + 1, `(${origin})`)
1251
+ this.log("[Presentation] user navigate to", index + 1, `(${origin})`);
948
1252
  if (0 <= index && index < this.pages.length) {
949
- this.jumpPage(index)
1253
+ this.jumpPage(index);
950
1254
  } else {
951
- this.warn?.(`[Presentation]: page index ${index} out of bounds [0, ${this.pages.length - 1}]`)
1255
+ this.warn?.(
1256
+ `[Presentation]: page index ${index} out of bounds [0, ${this.pages.length - 1}]`,
1257
+ );
952
1258
  }
953
1259
  }
954
1260
  }
@@ -962,44 +1268,56 @@ function createPresentation(
962
1268
  thumbnail?: (src: string) => string,
963
1269
  useClipView?: boolean,
964
1270
  ): AppPresentation {
965
- box.mountStyles(styles)
1271
+ box.mountStyles(styles);
966
1272
 
967
- const app = new AppPresentation({ pages, readonly: box.readonly, jumpPage, thumbnail})
968
- app.box = box
969
- box.mountContent(app.contentDOM)
970
- box.mountFooter(app.footerDOM)
1273
+ const app = new AppPresentation({
1274
+ pages,
1275
+ readonly: box.readonly,
1276
+ jumpPage,
1277
+ thumbnail,
1278
+ });
1279
+ app.box = box;
1280
+ box.mountContent(app.contentDOM);
1281
+ box.mountFooter(app.footerDOM);
971
1282
 
972
- app.dispose.add(pageIndex$.subscribe(pageIndex => { app.setPageIndex(pageIndex) }))
1283
+ app.dispose.add(
1284
+ pageIndex$.subscribe((pageIndex) => {
1285
+ app.setPageIndex(pageIndex);
1286
+ }),
1287
+ );
973
1288
 
974
- if(useClipView && view){
1289
+ if (useClipView && view) {
975
1290
  const onCameraUpdatedEffectForMaskView = debounce(() => {
976
1291
  const { width: pageWidth, height: pageHeight } = app.page() || {};
977
- if(!pageWidth || !pageHeight) {
1292
+ if (!pageWidth || !pageHeight) {
978
1293
  return;
979
1294
  }
980
1295
  const { scale } = view.camera;
981
1296
  const { width, height } = view.size;
982
- const pageRatioX = Math.round((1 - (pageWidth * scale / width)) / 2 * 100);
983
- const pageRatioY = Math.round((1 - (pageHeight * scale / height)) / 2 * 100);
1297
+ const pageRatioX = Math.round(
1298
+ ((1 - (pageWidth * scale) / width) / 2) * 100,
1299
+ );
1300
+ const pageRatioY = Math.round(
1301
+ ((1 - (pageHeight * scale) / height) / 2) * 100,
1302
+ );
984
1303
  app.whiteboardDOM.style.clipPath = `inset(${pageRatioY}% ${pageRatioX}%)`;
985
- }, 50)
1304
+ }, 50);
986
1305
  onCameraUpdatedEffectForMaskView();
987
1306
  const onCameraUpdatedEffect = () => {
988
- view.callbacks.on("onSizeUpdated", onCameraUpdatedEffectForMaskView)
989
- view.callbacks.on("onCameraUpdated", onCameraUpdatedEffectForMaskView)
1307
+ view.callbacks.on("onSizeUpdated", onCameraUpdatedEffectForMaskView);
1308
+ view.callbacks.on("onCameraUpdated", onCameraUpdatedEffectForMaskView);
990
1309
  return () => {
991
1310
  view.callbacks.off("onCameraUpdated", onCameraUpdatedEffectForMaskView);
992
1311
  view.callbacks.off("onSizeUpdated", onCameraUpdatedEffectForMaskView);
993
- }
994
- }
995
- app.dispose.make(onCameraUpdatedEffect)
1312
+ };
1313
+ };
1314
+ app.dispose.make(onCameraUpdatedEffect);
996
1315
  }
997
-
998
1316
 
999
- return app
1317
+ return app;
1000
1318
  }
1001
1319
 
1002
- export type RegisterFn = typeof WindowManager["register"]
1320
+ export type RegisterFn = (typeof WindowManager)["register"];
1003
1321
 
1004
1322
  export interface InstallOptions {
1005
1323
  /**
@@ -1008,11 +1326,11 @@ export interface InstallOptions {
1008
1326
  *
1009
1327
  * @example "DocsViewer"
1010
1328
  */
1011
- as?: string
1329
+ as?: string;
1012
1330
  /**
1013
1331
  * Options to customize the local app (not synced to others).
1014
1332
  */
1015
- appOptions?: PresentationAppOptions
1333
+ appOptions?: PresentationAppOptions;
1016
1334
  }
1017
1335
 
1018
1336
  /**
@@ -1021,10 +1339,13 @@ export interface InstallOptions {
1021
1339
  *
1022
1340
  * @example install(register, { as: "DocsViewer" })
1023
1341
  */
1024
- export const install = (register: RegisterFn, options: InstallOptions = {}): Promise<void> => {
1025
- let app = NetlessAppPresentation
1342
+ export const install = (
1343
+ register: RegisterFn,
1344
+ options: InstallOptions = {},
1345
+ ): Promise<void> => {
1346
+ let app = NetlessAppPresentation;
1026
1347
  if (options.as) {
1027
- app = Object.assign({}, app, { kind: options.as })
1348
+ app = Object.assign({}, app, { kind: options.as });
1028
1349
  }
1029
- return register({ kind: app.kind, src: app, appOptions: options.appOptions })
1030
- }
1350
+ return register({ kind: app.kind, src: app, appOptions: options.appOptions });
1351
+ };