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