@netless/app-presentation 0.1.12 → 0.1.13-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +18 -2
- package/dist/index.global.js +445 -99
- package/dist/index.js +445 -99
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +445 -99
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/app-presentation.ts +1041 -499
- package/src/preload.ts +14 -0
package/src/app-presentation.ts
CHANGED
|
@@ -1,17 +1,43 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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 {
|
|
11
|
-
|
|
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
|
|
50
|
+
type MoveCameraRequest = { centerX: number; centerY: number; scale: number };
|
|
25
51
|
|
|
26
|
-
const emptySceneName =
|
|
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,11 +91,22 @@ 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 */
|
|
72
98
|
useClipView?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Max time (ms) `setup()` waits for the first visible page image to load
|
|
101
|
+
* before resolving anyway. Default: 5_000.
|
|
102
|
+
*/
|
|
103
|
+
setupReadyTimeout?: number;
|
|
104
|
+
/**
|
|
105
|
+
* Blur thumbnail degradation keeps cached (focused-lost) presentations on
|
|
106
|
+
* the low-res `previewURL` thumbnail to save memory. Enabled by default in
|
|
107
|
+
* maximized/minimized states; set `true` to disable it.
|
|
108
|
+
*/
|
|
109
|
+
disableBlurThumbnailDegradation?: boolean;
|
|
73
110
|
}
|
|
74
111
|
|
|
75
112
|
export interface PresentationController {
|
|
@@ -89,9 +126,9 @@ export interface PresentationController {
|
|
|
89
126
|
/** Resolves after the whiteboard View has accepted the next scene path. */
|
|
90
127
|
nextPageAsync(): Promise<boolean>;
|
|
91
128
|
/** `index` ranges from 0 to `length - 1` */
|
|
92
|
-
pageState(): { index: number
|
|
129
|
+
pageState(): { index: number; length: number };
|
|
93
130
|
|
|
94
|
-
toPdf(): Promise<{ pdf: ArrayBuffer
|
|
131
|
+
toPdf(): Promise<{ pdf: ArrayBuffer; title: string } | null>;
|
|
95
132
|
|
|
96
133
|
log: Logger;
|
|
97
134
|
/** set the docs view readonly */
|
|
@@ -99,154 +136,399 @@ export interface PresentationController {
|
|
|
99
136
|
/** set the presentation readonly */
|
|
100
137
|
setReadonly: (bol: boolean) => void;
|
|
101
138
|
/** Moves the camera through the API, regardless of whether scrollbars are shown. */
|
|
102
|
-
moveCamera: (camera: {
|
|
139
|
+
moveCamera: (camera: {
|
|
140
|
+
centerX: number;
|
|
141
|
+
centerY: number;
|
|
142
|
+
scale: number;
|
|
143
|
+
}) => void;
|
|
103
144
|
/** get the origin scale */
|
|
104
145
|
getOriginScale: () => number;
|
|
105
146
|
/** get the view scale */
|
|
106
|
-
getScale: () =>number;
|
|
147
|
+
getScale: () => number;
|
|
107
148
|
/** get the page size */
|
|
108
|
-
getPageSize: () => { width: number
|
|
149
|
+
getPageSize: () => { width: number; height: number };
|
|
109
150
|
/** screenshot the current page */
|
|
110
|
-
screenshotCurrentPageAsync: (
|
|
151
|
+
screenshotCurrentPageAsync: (
|
|
152
|
+
context: CanvasRenderingContext2D,
|
|
153
|
+
width?: number,
|
|
154
|
+
height?: number,
|
|
155
|
+
) => Promise<void>;
|
|
111
156
|
}
|
|
112
157
|
|
|
113
158
|
const ppt2page = (
|
|
114
159
|
ppt: SceneDefinition["ppt"],
|
|
115
160
|
name?: string,
|
|
116
|
-
originSize?: Size
|
|
161
|
+
originSize?: Size,
|
|
117
162
|
): PresentationPage | null => {
|
|
118
|
-
if (!ppt) return null
|
|
119
|
-
const size = fitPageSizeToOrigin(ppt, originSize)
|
|
120
|
-
return { ...size, src: ppt.src, thumbnail: ppt.previewURL, name }
|
|
121
|
-
}
|
|
163
|
+
if (!ppt) return null;
|
|
164
|
+
const size = fitPageSizeToOrigin(ppt, originSize);
|
|
165
|
+
return { ...size, src: ppt.src, thumbnail: ppt.previewURL, name };
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The whiteboard SDK renders the scene ppt as a real `<img alt="background">`
|
|
170
|
+
* inside the background HTML engine layer of the view. All blur degradation
|
|
171
|
+
* below works directly on that DOM node; no scene API is ever called.
|
|
172
|
+
*/
|
|
173
|
+
const findBackgroundImage = (view: View): HTMLImageElement | null => {
|
|
174
|
+
const container = (view as any).divElement as HTMLElement | undefined | null;
|
|
175
|
+
if (!container || typeof container.querySelector !== "function") return null;
|
|
176
|
+
return container.querySelector('img[alt="background"]');
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Degradation only applies when the window cannot be visible: maximized
|
|
181
|
+
* (covered by the top app) or minimized. Normal mode may show several
|
|
182
|
+
* windows side by side, so a blurred app there must keep the full image.
|
|
183
|
+
*/
|
|
184
|
+
const isBlurDegradationAllowed = (context: AppContext): boolean => {
|
|
185
|
+
const boxStatus = context.getBoxStatus();
|
|
186
|
+
if (boxStatus) {
|
|
187
|
+
return boxStatus !== "normal";
|
|
188
|
+
}
|
|
189
|
+
const boxState = context.getWindowManager().boxState;
|
|
190
|
+
return boxState !== undefined && boxState !== "normal";
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const DEFAULT_SETUP_READY_TIMEOUT = 5_000;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Resolve once the current page background image has loaded (or after
|
|
197
|
+
* `timeoutMs`). `false` means timeout or the app was disposed mid-wait;
|
|
198
|
+
* callers must treat it as "continue loading in background", not an error.
|
|
199
|
+
*/
|
|
200
|
+
const waitForCurrentPageImage = (
|
|
201
|
+
view: View,
|
|
202
|
+
timeoutMs: number,
|
|
203
|
+
isDisposed: () => boolean,
|
|
204
|
+
): Promise<boolean> => {
|
|
205
|
+
return new Promise<boolean>(resolve => {
|
|
206
|
+
let settled = false;
|
|
207
|
+
let pollTimer: number | undefined;
|
|
208
|
+
const settle = (loaded: boolean) => {
|
|
209
|
+
if (settled) return;
|
|
210
|
+
settled = true;
|
|
211
|
+
window.clearTimeout(timeoutTimer);
|
|
212
|
+
if (pollTimer !== undefined) window.clearInterval(pollTimer);
|
|
213
|
+
resolve(loaded);
|
|
214
|
+
};
|
|
215
|
+
const timeoutTimer = window.setTimeout(() => settle(false), timeoutMs);
|
|
216
|
+
const check = () => {
|
|
217
|
+
if (isDisposed()) {
|
|
218
|
+
settle(false);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const img = findBackgroundImage(view);
|
|
222
|
+
if (img && img.complete && img.naturalWidth > 0) {
|
|
223
|
+
settle(true);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
check();
|
|
227
|
+
if (!settled) {
|
|
228
|
+
pollTimer = window.setInterval(check, 100);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Compare image URLs by resolved absolute href so relative/absolute
|
|
235
|
+
* representations of the same URL do not look different (and to keep the
|
|
236
|
+
* load-capture re-degrade loop terminating).
|
|
237
|
+
*/
|
|
238
|
+
const resolveImageUrl = (url: string): string => {
|
|
239
|
+
try {
|
|
240
|
+
return new URL(url, window.location.href).href;
|
|
241
|
+
} catch {
|
|
242
|
+
return url;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Blur thumbnail degradation (see lazy-setup design doc §15): when the app
|
|
248
|
+
* loses focus but stays cached (multi-runtime cache mode), swap the scene
|
|
249
|
+
* background `<img>` src to the low-res `PptDescription.previewURL`
|
|
250
|
+
* (`page.thumbnail`) via a pure DOM change and pause full-image preloading;
|
|
251
|
+
* restore the full image when focus comes back. Skipped when the page has no
|
|
252
|
+
* thumbnail.
|
|
253
|
+
*
|
|
254
|
+
* While degraded, every load of a background `<img>` is re-degraded
|
|
255
|
+
* immediately via a capture-phase `load` listener on the view container
|
|
256
|
+
* (`load` does not bubble but capture works). This covers remote page turns,
|
|
257
|
+
* SDK React re-renders and the SDK's own background-image onError retries —
|
|
258
|
+
* any path that (re)renders the full-size image while the app stays blurred.
|
|
259
|
+
*/
|
|
260
|
+
const setupBlurThumbnailDegradation = (config: {
|
|
261
|
+
context: AppContext;
|
|
262
|
+
view: View;
|
|
263
|
+
pages: PresentationPage[];
|
|
264
|
+
pageIndex$: Readable<number>;
|
|
265
|
+
app: Presentation;
|
|
266
|
+
dispose: ReturnType<typeof disposableStore>;
|
|
267
|
+
}): void => {
|
|
268
|
+
const { context, view, pages, pageIndex$, app, dispose } = config;
|
|
269
|
+
// Two separate flags: `blurEpisode` covers the whole blurred period (pages
|
|
270
|
+
// without a thumbnail don't end it, so a later page degrades again), while
|
|
271
|
+
// `reDegradeEnabled` only turns off when the degraded thumbnail itself
|
|
272
|
+
// fails, so we don't fight the SDK's background onError retry.
|
|
273
|
+
let blurEpisode = false;
|
|
274
|
+
let reDegradeEnabled = false;
|
|
275
|
+
|
|
276
|
+
const degrade = (): void => {
|
|
277
|
+
const page = pages[pageIndex$.value];
|
|
278
|
+
if (!page?.thumbnail) return; // keep the full image as-is
|
|
279
|
+
const img = findBackgroundImage(view);
|
|
280
|
+
if (img && resolveImageUrl(img.src) !== resolveImageUrl(page.thumbnail)) {
|
|
281
|
+
img.src = page.thumbnail;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const restore = (): void => {
|
|
286
|
+
const page = pages[pageIndex$.value];
|
|
287
|
+
const img = findBackgroundImage(view);
|
|
288
|
+
if (page && img && resolveImageUrl(img.src) !== resolveImageUrl(page.src)) {
|
|
289
|
+
img.src = page.src;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const isBackgroundImg = (target: EventTarget | null): boolean => {
|
|
294
|
+
const el = target as HTMLImageElement | null;
|
|
295
|
+
return Boolean(
|
|
296
|
+
el && el.tagName === "IMG" && el.matches?.('img[alt="background"]'),
|
|
297
|
+
);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// Re-degrade any background img that finishes loading a full-size image
|
|
301
|
+
// while the app is blurred. Scoped to the view container so the app's own
|
|
302
|
+
// preview-panel thumbnails are untouched.
|
|
303
|
+
const onImgLoad = (ev: Event): void => {
|
|
304
|
+
if (!blurEpisode || !reDegradeEnabled) return;
|
|
305
|
+
if (!isBackgroundImg(ev.target)) return;
|
|
306
|
+
degrade();
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// The degraded thumbnail failed to load: stop re-degrading for this blur
|
|
310
|
+
// episode so the SDK's onError retry can settle on the full image. Focus
|
|
311
|
+
// still force-restores afterwards.
|
|
312
|
+
const onImgError = (ev: Event): void => {
|
|
313
|
+
if (!blurEpisode || !reDegradeEnabled) return;
|
|
314
|
+
if (!isBackgroundImg(ev.target)) return;
|
|
315
|
+
reDegradeEnabled = false;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const offFocus = context.emitter.on("focus", (isFocused: boolean) => {
|
|
319
|
+
if (isFocused) {
|
|
320
|
+
if (blurEpisode) {
|
|
321
|
+
blurEpisode = false;
|
|
322
|
+
reDegradeEnabled = false;
|
|
323
|
+
restore();
|
|
324
|
+
app.preload.resume(pageIndex$.value);
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
if (!isBlurDegradationAllowed(context)) return;
|
|
328
|
+
blurEpisode = true;
|
|
329
|
+
reDegradeEnabled = true;
|
|
330
|
+
degrade();
|
|
331
|
+
app.preload.pause();
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
dispose.add(offFocus);
|
|
335
|
+
|
|
336
|
+
const viewEl = (view as any).divElement as HTMLElement | undefined | null;
|
|
337
|
+
if (viewEl && typeof viewEl.addEventListener === "function") {
|
|
338
|
+
viewEl.addEventListener("load", onImgLoad, true);
|
|
339
|
+
viewEl.addEventListener("error", onImgError, true);
|
|
340
|
+
dispose.add(() => {
|
|
341
|
+
viewEl.removeEventListener("load", onImgLoad, true);
|
|
342
|
+
viewEl.removeEventListener("error", onImgError, true);
|
|
343
|
+
});
|
|
344
|
+
} else {
|
|
345
|
+
// View not mounted yet (should not happen after context.mountView):
|
|
346
|
+
// keep the degradation on blur only, without re-degrade coverage.
|
|
347
|
+
warnOnceMissingViewContainer(view);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// One-shot diagnostic for an unexpected missing view container.
|
|
352
|
+
const degradedWarnedViews = new WeakSet<object>();
|
|
353
|
+
const warnOnceMissingViewContainer = (view: View): void => {
|
|
354
|
+
if (degradedWarnedViews.has(view)) return;
|
|
355
|
+
degradedWarnedViews.add(view);
|
|
356
|
+
console.warn(
|
|
357
|
+
"[Presentation]: blur thumbnail degradation runs without re-degrade coverage (view container unavailable)",
|
|
358
|
+
);
|
|
359
|
+
};
|
|
122
360
|
|
|
123
361
|
const createLogger = (room: Room | undefined): Logger => {
|
|
124
362
|
if (room && (room as any).logger) {
|
|
125
|
-
return (...args) => (room as any).logger.info(...args)
|
|
363
|
+
return (...args) => (room as any).logger.info(...args);
|
|
126
364
|
} else {
|
|
127
|
-
return (...args) => console.log(...args)
|
|
365
|
+
return (...args) => console.log(...args);
|
|
128
366
|
}
|
|
129
|
-
}
|
|
367
|
+
};
|
|
130
368
|
|
|
131
|
-
const createDiagnosticLogger = (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
369
|
+
const createDiagnosticLogger = (
|
|
370
|
+
context: AppContext,
|
|
371
|
+
): PresentationDiagnosticLogger => {
|
|
372
|
+
const createAppLogger = (context as any).createLogger;
|
|
373
|
+
if (typeof createAppLogger === "function") {
|
|
374
|
+
return createAppLogger.call(context, "camera", {
|
|
375
|
+
debounceTime: 300,
|
|
376
|
+
maxWaitTime: 2000,
|
|
377
|
+
});
|
|
135
378
|
}
|
|
136
379
|
|
|
137
380
|
// 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 = (
|
|
381
|
+
const roomLogger = (context.getRoom() as any)?.logger;
|
|
382
|
+
const prefix = `[Presentation][${context.appId}][camera]`;
|
|
383
|
+
const emit = (
|
|
384
|
+
level: "info" | "warn" | "error",
|
|
385
|
+
event: string,
|
|
386
|
+
...data: unknown[]
|
|
387
|
+
) => {
|
|
141
388
|
try {
|
|
142
|
-
const printer = roomLogger?.[level]
|
|
143
|
-
if (typeof printer ===
|
|
389
|
+
const printer = roomLogger?.[level];
|
|
390
|
+
if (typeof printer === "function")
|
|
391
|
+
printer.call(roomLogger, `${prefix}[${event}]`, ...data);
|
|
144
392
|
} catch {
|
|
145
393
|
// Diagnostics must never change the App API result or replace its original error.
|
|
146
394
|
}
|
|
147
|
-
}
|
|
148
|
-
const debouncedByEvent = new Map<string, ReturnType<typeof debounce>>()
|
|
395
|
+
};
|
|
396
|
+
const debouncedByEvent = new Map<string, ReturnType<typeof debounce>>();
|
|
149
397
|
const debouncedInfo = (event: string, payload?: unknown) => {
|
|
150
|
-
let emitDebounced = debouncedByEvent.get(event)
|
|
398
|
+
let emitDebounced = debouncedByEvent.get(event);
|
|
151
399
|
if (!emitDebounced) {
|
|
152
400
|
emitDebounced = debounce(
|
|
153
|
-
(nextPayload?: unknown) => emit(
|
|
401
|
+
(nextPayload?: unknown) => emit("info", event, nextPayload),
|
|
154
402
|
300,
|
|
155
|
-
{ maxWait: 2000 }
|
|
156
|
-
)
|
|
157
|
-
debouncedByEvent.set(event, emitDebounced)
|
|
403
|
+
{ maxWait: 2000 },
|
|
404
|
+
);
|
|
405
|
+
debouncedByEvent.set(event, emitDebounced);
|
|
158
406
|
}
|
|
159
|
-
emitDebounced(payload)
|
|
160
|
-
}
|
|
407
|
+
emitDebounced(payload);
|
|
408
|
+
};
|
|
161
409
|
return {
|
|
162
|
-
info: (event, payload) => emit(
|
|
163
|
-
warn: (event, payload) => emit(
|
|
164
|
-
error: (event, error, payload) => emit(
|
|
410
|
+
info: (event, payload) => emit("info", event, payload),
|
|
411
|
+
warn: (event, payload) => emit("warn", event, payload),
|
|
412
|
+
error: (event, error, payload) => emit("error", event, error, payload),
|
|
165
413
|
debouncedInfo,
|
|
166
|
-
flush: () => debouncedByEvent.forEach(logger => logger.flush()),
|
|
167
|
-
}
|
|
168
|
-
}
|
|
414
|
+
flush: () => debouncedByEvent.forEach((logger) => logger.flush()),
|
|
415
|
+
};
|
|
416
|
+
};
|
|
169
417
|
|
|
170
418
|
const safeResourceLocation = (value: string): string => {
|
|
171
|
-
if (value.startsWith(
|
|
172
|
-
if (value.startsWith(
|
|
419
|
+
if (value.startsWith("data:")) return "data:[omitted]";
|
|
420
|
+
if (value.startsWith("blob:")) return "blob:[omitted]";
|
|
173
421
|
try {
|
|
174
|
-
const url = new URL(value)
|
|
175
|
-
return `${url.origin}${url.pathname}
|
|
422
|
+
const url = new URL(value);
|
|
423
|
+
return `${url.origin}${url.pathname}`;
|
|
176
424
|
} catch {
|
|
177
|
-
return value.split(
|
|
425
|
+
return value.split("?")[0];
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
const scenesEqual = (
|
|
429
|
+
scenes1?: SceneDefinition[],
|
|
430
|
+
scenes2?: SceneDefinition[],
|
|
431
|
+
): boolean => {
|
|
432
|
+
if (!scenes1 || !scenes2) {
|
|
433
|
+
return false;
|
|
178
434
|
}
|
|
179
|
-
}
|
|
180
|
-
const scenesEqual = (scenes1?: SceneDefinition[], scenes2?: SceneDefinition[]): boolean => {
|
|
181
|
-
if (!scenes1 || !scenes2) {return false}
|
|
182
435
|
if (scenes1.length !== scenes2.length) return false;
|
|
183
436
|
return scenes1.every((scene, index) => {
|
|
184
437
|
const scene2 = scenes2[index];
|
|
185
|
-
return
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
438
|
+
return (
|
|
439
|
+
scene.name === scene2.name &&
|
|
440
|
+
scene.ppt?.width === scene2.ppt?.width &&
|
|
441
|
+
scene.ppt?.height === scene2.ppt?.height &&
|
|
442
|
+
scene.ppt?.src === scene2.ppt?.src
|
|
443
|
+
);
|
|
189
444
|
});
|
|
190
445
|
};
|
|
191
446
|
|
|
192
|
-
|
|
447
|
+
const teardownByContext = new WeakMap<object, () => void>();
|
|
448
|
+
|
|
449
|
+
export const NetlessAppPresentation: NetlessApp<
|
|
450
|
+
PresentationAttributes,
|
|
451
|
+
{},
|
|
452
|
+
PresentationAppOptions,
|
|
453
|
+
PresentationController
|
|
454
|
+
> & {
|
|
455
|
+
teardown(context: AppContext): void;
|
|
456
|
+
} = {
|
|
193
457
|
kind: "Presentation",
|
|
194
458
|
setup(context) {
|
|
195
|
-
const diagnosticLogger = createDiagnosticLogger(context)
|
|
196
|
-
const view = context.getView()
|
|
459
|
+
const diagnosticLogger = createDiagnosticLogger(context);
|
|
460
|
+
const view = context.getView();
|
|
197
461
|
if (!view)
|
|
198
|
-
throw new Error(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
const
|
|
462
|
+
throw new Error(
|
|
463
|
+
"[Presentation]: no whiteboard view, make sure you have added options.scenePath in addApp()",
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
const options = context.getAppOptions() || {};
|
|
467
|
+
const room = context.getRoom();
|
|
468
|
+
const log = options.log || createLogger(room);
|
|
469
|
+
const roomLogger = (room as any)?.logger;
|
|
470
|
+
const warn: Logger = (...data) =>
|
|
471
|
+
roomLogger?.warn ? roomLogger.warn(...data) : log(...data);
|
|
472
|
+
const configuredOriginSize = context.storage.state.originSize;
|
|
206
473
|
const originSize = isValidSize(configuredOriginSize)
|
|
207
|
-
? {
|
|
208
|
-
|
|
474
|
+
? {
|
|
475
|
+
width: configuredOriginSize.width,
|
|
476
|
+
height: configuredOriginSize.height,
|
|
477
|
+
}
|
|
478
|
+
: undefined;
|
|
209
479
|
if (configuredOriginSize != null && !originSize) {
|
|
210
|
-
warn(
|
|
480
|
+
warn(
|
|
481
|
+
`[Presentation] originSize should contain finite positive width and height, got ${JSON.stringify(configuredOriginSize)}`,
|
|
482
|
+
);
|
|
211
483
|
}
|
|
212
|
-
const pages = context
|
|
484
|
+
const pages = context
|
|
485
|
+
.getScenes()
|
|
213
486
|
?.map(({ ppt, name }) => ppt2page(ppt, name, originSize))
|
|
214
|
-
.filter(Boolean) as PresentationPage[]
|
|
487
|
+
.filter(Boolean) as PresentationPage[];
|
|
215
488
|
if (!pages || pages.length === 0)
|
|
216
|
-
throw new Error(
|
|
217
|
-
|
|
218
|
-
|
|
489
|
+
throw new Error(
|
|
490
|
+
"[Presentation]: empty scenes, make sure you have added options.scenes in addApp()",
|
|
491
|
+
);
|
|
492
|
+
if (pages[0].src.startsWith("ppt"))
|
|
493
|
+
throw new Error(
|
|
494
|
+
"[Presentation]: legacy dynamic PPT is unsupported, please use the projector converter and @netless/slide to render it",
|
|
495
|
+
);
|
|
219
496
|
|
|
220
497
|
// Now it must have a blank scene points to "{scenePath}/{scenes[0].name}", e.g. "/pdf/123456/1"
|
|
221
498
|
// 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
|
|
499
|
+
const scenePath = context.getInitScenePath()!;
|
|
500
|
+
let maxCameraScale = options.maxCameraScale ?? 3;
|
|
224
501
|
if (!(Number.isFinite(maxCameraScale) && maxCameraScale! > 0)) {
|
|
225
|
-
warn(
|
|
226
|
-
|
|
502
|
+
warn(
|
|
503
|
+
`[Presentation] maxCameraScale should be a positive number, got ${options.maxCameraScale}`,
|
|
504
|
+
);
|
|
505
|
+
maxCameraScale = 3;
|
|
227
506
|
}
|
|
228
507
|
|
|
229
|
-
log(`[Presentation] new ${context.appId}`)
|
|
230
|
-
|
|
231
|
-
const dispose = disposableStore()
|
|
232
|
-
dispose.add(() => log(`[Presentation] dispose ${context.appId}`))
|
|
233
|
-
dispose.add(() => diagnosticLogger.flush())
|
|
508
|
+
log(`[Presentation] new ${context.appId}`);
|
|
234
509
|
|
|
235
|
-
const
|
|
510
|
+
const dispose = disposableStore();
|
|
511
|
+
dispose.add(() => log(`[Presentation] dispose ${context.appId}`));
|
|
512
|
+
dispose.add(() => diagnosticLogger.flush());
|
|
236
513
|
|
|
514
|
+
const view$$ = context.createStorage("view", {
|
|
515
|
+
uid: "",
|
|
516
|
+
originX: 0,
|
|
517
|
+
originY: 0,
|
|
518
|
+
width: 0,
|
|
519
|
+
height: 0,
|
|
520
|
+
});
|
|
237
521
|
|
|
238
|
-
const _addScenePathListener = (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return () => windowManger?.emitter.off(name, listener)
|
|
245
|
-
}
|
|
522
|
+
const _addScenePathListener = (name: keyof PublicEvent, listener: any) => {
|
|
523
|
+
const windowManger = (context as any).manager
|
|
524
|
+
.windowManger as WindowManager;
|
|
525
|
+
windowManger.emitter.on(name, listener);
|
|
526
|
+
return () => windowManger?.emitter.off(name, listener);
|
|
527
|
+
};
|
|
246
528
|
|
|
247
529
|
const getPageIndex = (view: View) => {
|
|
248
530
|
const focusScenePath = view.focusScenePath;
|
|
249
|
-
const name = focusScenePath?.split(
|
|
531
|
+
const name = focusScenePath?.split("/").pop();
|
|
250
532
|
let _pageIndex = pages.findIndex((page, index) => {
|
|
251
533
|
const n = page.name ?? String(index + 1);
|
|
252
534
|
return n === name;
|
|
@@ -255,44 +537,51 @@ export const NetlessAppPresentation: NetlessApp<PresentationAttributes, {}, Pres
|
|
|
255
537
|
_pageIndex = 0;
|
|
256
538
|
}
|
|
257
539
|
return _pageIndex;
|
|
258
|
-
}
|
|
540
|
+
};
|
|
259
541
|
|
|
260
542
|
let pageIndex = getPageIndex(view);
|
|
261
|
-
const pageIndex$ = readable<number>(pageIndex, set => {
|
|
543
|
+
const pageIndex$ = readable<number>(pageIndex, (set) => {
|
|
262
544
|
set(pageIndex);
|
|
263
|
-
return _addScenePathListener(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
545
|
+
return _addScenePathListener(
|
|
546
|
+
"onAppScenePathChange",
|
|
547
|
+
(payload: AppPayload) => {
|
|
548
|
+
const { appId } = payload;
|
|
549
|
+
if (appId === context.appId) {
|
|
550
|
+
const _pageIndex = getPageIndex(payload.view);
|
|
551
|
+
set(_pageIndex);
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
);
|
|
270
555
|
});
|
|
271
556
|
|
|
272
557
|
dispose.add(() => {
|
|
273
558
|
pageIndex$.dispose();
|
|
274
|
-
})
|
|
559
|
+
});
|
|
275
560
|
|
|
276
561
|
// let lastIndex = -1
|
|
277
562
|
|
|
278
|
-
const me = context.getRoom()?.uid || context.getDisplayer().observerId +
|
|
563
|
+
const me = context.getRoom()?.uid || context.getDisplayer().observerId + "";
|
|
279
564
|
|
|
280
|
-
let throttleSyncView = 0
|
|
565
|
+
let throttleSyncView = 0;
|
|
281
566
|
|
|
282
567
|
const syncPage = async (index: number, logger?: any): Promise<boolean> => {
|
|
568
|
+
if (!context.getIsWritable()) return false;
|
|
283
569
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const scenes = context.getDisplayer().entireScenes()[scenePath]
|
|
287
|
-
if (!scenes) return false
|
|
570
|
+
const scenes = context.getDisplayer().entireScenes()[scenePath];
|
|
571
|
+
if (!scenes) return false;
|
|
288
572
|
|
|
289
573
|
const p = pages[index];
|
|
290
574
|
const name = p.name ?? String(index + 1);
|
|
291
575
|
|
|
292
576
|
// "Prepare scenes" may not run correctly if the user suddenly disconnected after adding the app.
|
|
293
577
|
// 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({
|
|
578
|
+
if (!scenes.some((scene) => scene.name === name)) {
|
|
579
|
+
await context.addPage({
|
|
580
|
+
scene: {
|
|
581
|
+
name,
|
|
582
|
+
ppt: { width: p.width, height: p.height, src: p.src },
|
|
583
|
+
},
|
|
584
|
+
});
|
|
296
585
|
}
|
|
297
586
|
|
|
298
587
|
if (logger) {
|
|
@@ -300,125 +589,140 @@ export const NetlessAppPresentation: NetlessApp<PresentationAttributes, {}, Pres
|
|
|
300
589
|
}
|
|
301
590
|
|
|
302
591
|
// Switch to that page.
|
|
303
|
-
await context.setScenePath(`${scenePath}/${name}`)
|
|
304
|
-
return true
|
|
305
|
-
}
|
|
592
|
+
await context.setScenePath(`${scenePath}/${name}`);
|
|
593
|
+
return true;
|
|
594
|
+
};
|
|
306
595
|
|
|
307
596
|
const prepareScenes = async (): Promise<void> => {
|
|
308
|
-
if (!context.isAddApp) return
|
|
597
|
+
if (!context.isAddApp) return;
|
|
309
598
|
// Caution: some user may insert a 500-page PDF.
|
|
310
599
|
if (pages.length > 100)
|
|
311
|
-
warn(
|
|
312
|
-
|
|
600
|
+
warn(
|
|
601
|
+
`[Presentation]: too many pages (${pages.length}), may cause performance issues`,
|
|
602
|
+
);
|
|
603
|
+
if (!room || !room.isWritable) return;
|
|
313
604
|
if (pageIndex$.value < 0 || pageIndex$.value >= pages.length) {
|
|
314
|
-
throw new Error(
|
|
605
|
+
throw new Error(
|
|
606
|
+
`[Presentation] Invalid page index: ${pageIndex$.value}, scenes length: ${pages.length}`,
|
|
607
|
+
);
|
|
315
608
|
}
|
|
316
609
|
|
|
317
|
-
const scenes = room.entireScenes()[scenePath]
|
|
610
|
+
const scenes = room.entireScenes()[scenePath];
|
|
318
611
|
if (!scenes || !scenes[pageIndex$.value]) {
|
|
319
|
-
throw new Error(
|
|
612
|
+
throw new Error(
|
|
613
|
+
`[Presentation]: no initial scene found at ${scenePath}, page index: ${pageIndex$.value}`,
|
|
614
|
+
);
|
|
320
615
|
}
|
|
321
|
-
const { name, ppt } = scenes[pageIndex$.value]
|
|
616
|
+
const { name, ppt } = scenes[pageIndex$.value];
|
|
322
617
|
const nextScenes = pages.map((page, index) => ({
|
|
323
618
|
name: page.name ?? String(index + 1),
|
|
324
|
-
ppt: { width: page.width, height: page.height, src: page.src }
|
|
325
|
-
}))
|
|
619
|
+
ppt: { width: page.width, height: page.height, src: page.src },
|
|
620
|
+
}));
|
|
326
621
|
|
|
327
622
|
if (!scenesEqual(scenes, nextScenes)) {
|
|
328
|
-
room.removeScenes(scenePath)
|
|
329
|
-
room.putScenes(scenePath, nextScenes)
|
|
623
|
+
room.removeScenes(scenePath);
|
|
624
|
+
room.putScenes(scenePath, nextScenes);
|
|
330
625
|
}
|
|
331
626
|
|
|
332
|
-
const shouldRedirect = name === nextScenes[pageIndex$.value].name && !ppt
|
|
627
|
+
const shouldRedirect = name === nextScenes[pageIndex$.value].name && !ppt;
|
|
333
628
|
if (shouldRedirect) {
|
|
334
|
-
await context.addPage({ scene: { name: emptySceneName } })
|
|
335
|
-
log(`[Presentation] setup setScenePath ${scenePath}/${emptySceneName}`)
|
|
336
|
-
await context.setScenePath(`${scenePath}/${emptySceneName}`)
|
|
629
|
+
await context.addPage({ scene: { name: emptySceneName } });
|
|
630
|
+
log(`[Presentation] setup setScenePath ${scenePath}/${emptySceneName}`);
|
|
631
|
+
await context.setScenePath(`${scenePath}/${emptySceneName}`);
|
|
337
632
|
}
|
|
338
633
|
|
|
339
|
-
await syncPage(pageIndex$.value, (room as any).logger)
|
|
634
|
+
await syncPage(pageIndex$.value, (room as any).logger);
|
|
340
635
|
if (shouldRedirect) {
|
|
341
|
-
log(`[Presentation] setup removeScenes ${scenePath}/${emptySceneName}`)
|
|
342
|
-
room.removeScenes(`${scenePath}/${emptySceneName}`)
|
|
636
|
+
log(`[Presentation] setup removeScenes ${scenePath}/${emptySceneName}`);
|
|
637
|
+
room.removeScenes(`${scenePath}/${emptySceneName}`);
|
|
343
638
|
}
|
|
344
|
-
}
|
|
639
|
+
};
|
|
345
640
|
|
|
346
|
-
const prepareScenesPromise = prepareScenes()
|
|
641
|
+
const prepareScenesPromise = prepareScenes();
|
|
347
642
|
|
|
348
643
|
const canJumpPage = (index: number): boolean => {
|
|
349
644
|
if (!context.getIsWritable()) {
|
|
350
|
-
warn(
|
|
351
|
-
|
|
645
|
+
warn(
|
|
646
|
+
"[Presentation]: no permission, make sure you have test room.isWritable",
|
|
647
|
+
);
|
|
648
|
+
return false;
|
|
352
649
|
}
|
|
353
650
|
|
|
354
651
|
if (!(0 <= index && index < pages.length)) {
|
|
355
|
-
warn(
|
|
356
|
-
|
|
652
|
+
warn(
|
|
653
|
+
`[Presentation]: page ${index + 1} out of bounds [1, ${pages.length}]`,
|
|
654
|
+
);
|
|
655
|
+
return false;
|
|
357
656
|
}
|
|
358
657
|
|
|
359
|
-
const scenes = context.getDisplayer().entireScenes()[scenePath]
|
|
658
|
+
const scenes = context.getDisplayer().entireScenes()[scenePath];
|
|
360
659
|
if (!scenes) {
|
|
361
|
-
warn(
|
|
362
|
-
|
|
660
|
+
warn(
|
|
661
|
+
`[Presentation]: no scenes found at ${scenePath}, make sure you have added options.scenePath in addApp()`,
|
|
662
|
+
);
|
|
663
|
+
return false;
|
|
363
664
|
}
|
|
364
665
|
|
|
365
|
-
return true
|
|
366
|
-
}
|
|
666
|
+
return true;
|
|
667
|
+
};
|
|
367
668
|
|
|
368
669
|
const jumpPage = (index: number): boolean => {
|
|
369
|
-
if (!canJumpPage(index)) return false
|
|
670
|
+
if (!canJumpPage(index)) return false;
|
|
370
671
|
|
|
371
|
-
void syncPage(index).catch(error => {
|
|
372
|
-
warn(
|
|
373
|
-
diagnosticLogger.error(
|
|
672
|
+
void syncPage(index).catch((error) => {
|
|
673
|
+
warn("[Presentation]: failed to sync page", error);
|
|
674
|
+
diagnosticLogger.error("jumpPage.failed", error, {
|
|
374
675
|
index,
|
|
375
676
|
pageIndex: pageIndex$.value,
|
|
376
677
|
focusScenePath: view.focusScenePath,
|
|
377
|
-
})
|
|
378
|
-
})
|
|
379
|
-
return true
|
|
380
|
-
}
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
return true;
|
|
681
|
+
};
|
|
381
682
|
|
|
382
|
-
const prevPage = () => jumpPage(pageIndex$.value - 1)
|
|
383
|
-
const nextPage = () => jumpPage(pageIndex$.value + 1)
|
|
683
|
+
const prevPage = () => jumpPage(pageIndex$.value - 1);
|
|
684
|
+
const nextPage = () => jumpPage(pageIndex$.value + 1);
|
|
384
685
|
const jumpPageAsync = async (index: number): Promise<boolean> => {
|
|
385
|
-
if (!canJumpPage(index)) return false
|
|
686
|
+
if (!canJumpPage(index)) return false;
|
|
386
687
|
try {
|
|
387
|
-
return await syncPage(index)
|
|
688
|
+
return await syncPage(index);
|
|
388
689
|
} catch (error) {
|
|
389
|
-
diagnosticLogger.error(
|
|
690
|
+
diagnosticLogger.error("jumpPageAsync.failed", error, {
|
|
390
691
|
index,
|
|
391
692
|
pageIndex: pageIndex$.value,
|
|
392
693
|
focusScenePath: view.focusScenePath,
|
|
393
|
-
})
|
|
394
|
-
throw error
|
|
694
|
+
});
|
|
695
|
+
throw error;
|
|
395
696
|
}
|
|
396
|
-
}
|
|
397
|
-
const prevPageAsync = () => jumpPageAsync(pageIndex$.value - 1)
|
|
398
|
-
const nextPageAsync = () => jumpPageAsync(pageIndex$.value + 1)
|
|
399
|
-
const pageState = () => ({ index: pageIndex$.value, length: pages.length })
|
|
697
|
+
};
|
|
698
|
+
const prevPageAsync = () => jumpPageAsync(pageIndex$.value - 1);
|
|
699
|
+
const nextPageAsync = () => jumpPageAsync(pageIndex$.value + 1);
|
|
700
|
+
const pageState = () => ({ index: pageIndex$.value, length: pages.length });
|
|
400
701
|
|
|
401
702
|
const scaleDocsToFit = () => {
|
|
402
|
-
const page = app.page()
|
|
703
|
+
const page = app.page();
|
|
403
704
|
if (page && isValidSize(page)) {
|
|
404
|
-
const referenceSize = getCameraReferenceSize(originSize, page)
|
|
705
|
+
const referenceSize = getCameraReferenceSize(originSize, page);
|
|
405
706
|
if (originSize) {
|
|
406
|
-
const fitScale = getFitScale(view.size, referenceSize)
|
|
407
|
-
if (!fitScale) return
|
|
707
|
+
const fitScale = getFitScale(view.size, referenceSize);
|
|
708
|
+
if (!fitScale) return;
|
|
408
709
|
const { minScale, maxScale } = getCameraScaleRange(
|
|
409
710
|
fitScale,
|
|
410
711
|
maxCameraScale,
|
|
411
|
-
options.disableCameraTransform
|
|
412
|
-
)
|
|
712
|
+
options.disableCameraTransform,
|
|
713
|
+
);
|
|
413
714
|
view.setCameraBound({
|
|
414
715
|
damping: 1,
|
|
415
716
|
maxContentMode: () => maxScale,
|
|
416
717
|
minContentMode: () => minScale,
|
|
417
|
-
centerX: 0,
|
|
418
|
-
|
|
718
|
+
centerX: 0,
|
|
719
|
+
centerY: 0,
|
|
720
|
+
width: page.width,
|
|
721
|
+
height: page.height,
|
|
722
|
+
});
|
|
419
723
|
if (isValidSharedViewport(view$$.state)) {
|
|
420
|
-
syncViewFromRemote(true)
|
|
421
|
-
return
|
|
724
|
+
syncViewFromRemote(true);
|
|
725
|
+
return;
|
|
422
726
|
}
|
|
423
727
|
}
|
|
424
728
|
view.moveCameraToContain({
|
|
@@ -426,119 +730,161 @@ export const NetlessAppPresentation: NetlessApp<PresentationAttributes, {}, Pres
|
|
|
426
730
|
originY: -referenceSize.height / 2,
|
|
427
731
|
width: referenceSize.width,
|
|
428
732
|
height: referenceSize.height,
|
|
429
|
-
animationMode:
|
|
430
|
-
})
|
|
733
|
+
animationMode: "immediately" as AnimationMode.Immediately,
|
|
734
|
+
});
|
|
431
735
|
if (!originSize) {
|
|
432
736
|
const { minScale, maxScale } = getCameraScaleRange(
|
|
433
737
|
view.camera.scale,
|
|
434
738
|
maxCameraScale,
|
|
435
|
-
options.disableCameraTransform
|
|
436
|
-
)
|
|
739
|
+
options.disableCameraTransform,
|
|
740
|
+
);
|
|
437
741
|
view.setCameraBound({
|
|
438
742
|
damping: 1,
|
|
439
743
|
maxContentMode: () => maxScale,
|
|
440
744
|
minContentMode: () => minScale,
|
|
441
|
-
centerX: 0,
|
|
442
|
-
|
|
745
|
+
centerX: 0,
|
|
746
|
+
centerY: 0,
|
|
747
|
+
width: page.width,
|
|
748
|
+
height: page.height,
|
|
749
|
+
});
|
|
443
750
|
}
|
|
444
|
-
syncViewFromRemote(true)
|
|
751
|
+
syncViewFromRemote(true);
|
|
445
752
|
}
|
|
446
|
-
}
|
|
447
|
-
let pendingMoveCameraRequest: MoveCameraRequest | undefined
|
|
753
|
+
};
|
|
754
|
+
let pendingMoveCameraRequest: MoveCameraRequest | undefined;
|
|
448
755
|
const syncView = () => {
|
|
449
756
|
if (context.getIsWritable()) {
|
|
450
757
|
if (options.debounceSync) {
|
|
451
758
|
clearTimeout(throttleSyncView);
|
|
452
759
|
throttleSyncView = 0;
|
|
453
760
|
}
|
|
454
|
-
if (throttleSyncView > 0) return
|
|
455
|
-
const page = app.page()
|
|
761
|
+
if (throttleSyncView > 0) return;
|
|
762
|
+
const page = app.page();
|
|
456
763
|
if (page && isValidSize(page)) {
|
|
457
764
|
throttleSyncView = setTimeout(() => {
|
|
458
|
-
throttleSyncView = 0
|
|
765
|
+
throttleSyncView = 0;
|
|
459
766
|
try {
|
|
460
767
|
const { camera, size } = view;
|
|
461
|
-
const referenceSize = getCameraReferenceSize(originSize, page)
|
|
462
|
-
const viewport = cameraToSharedViewport(
|
|
463
|
-
|
|
768
|
+
const referenceSize = getCameraReferenceSize(originSize, page);
|
|
769
|
+
const viewport = cameraToSharedViewport(
|
|
770
|
+
camera,
|
|
771
|
+
size,
|
|
772
|
+
referenceSize,
|
|
773
|
+
);
|
|
774
|
+
if (viewport) view$$.setState({ uid: me, ...viewport });
|
|
464
775
|
if (pendingMoveCameraRequest) {
|
|
465
776
|
diagnosticLogger.debouncedInfo(
|
|
466
|
-
|
|
467
|
-
getCameraDiagnosticState(
|
|
468
|
-
|
|
469
|
-
|
|
777
|
+
"moveCamera",
|
|
778
|
+
getCameraDiagnosticState(
|
|
779
|
+
"moveCamera",
|
|
780
|
+
pendingMoveCameraRequest,
|
|
781
|
+
),
|
|
782
|
+
);
|
|
783
|
+
pendingMoveCameraRequest = undefined;
|
|
470
784
|
}
|
|
471
785
|
} catch (error) {
|
|
472
786
|
diagnosticLogger.error(
|
|
473
|
-
|
|
787
|
+
"syncView.failed",
|
|
474
788
|
error,
|
|
475
|
-
getCameraDiagnosticState(
|
|
476
|
-
|
|
477
|
-
|
|
789
|
+
getCameraDiagnosticState(
|
|
790
|
+
"moveCamera",
|
|
791
|
+
pendingMoveCameraRequest,
|
|
792
|
+
),
|
|
793
|
+
);
|
|
794
|
+
pendingMoveCameraRequest = undefined;
|
|
478
795
|
}
|
|
479
|
-
}, 50)
|
|
796
|
+
}, 50);
|
|
480
797
|
}
|
|
481
798
|
}
|
|
482
|
-
}
|
|
799
|
+
};
|
|
483
800
|
|
|
484
801
|
dispose.add(() => {
|
|
485
|
-
clearTimeout(throttleSyncView)
|
|
486
|
-
throttleSyncView = 0
|
|
487
|
-
pendingMoveCameraRequest = undefined
|
|
488
|
-
})
|
|
802
|
+
clearTimeout(throttleSyncView);
|
|
803
|
+
throttleSyncView = 0;
|
|
804
|
+
pendingMoveCameraRequest = undefined;
|
|
805
|
+
});
|
|
489
806
|
|
|
490
807
|
const syncViewFromRemote = (force = false, animate = false) => {
|
|
491
|
-
const { uid, originX, originY, width, height } = view$$.state
|
|
808
|
+
const { uid, originX, originY, width, height } = view$$.state;
|
|
492
809
|
if ((force || uid !== me) && width > 0 && height > 0) {
|
|
493
810
|
view.moveCameraToContain({
|
|
494
|
-
originX,
|
|
495
|
-
|
|
496
|
-
|
|
811
|
+
originX,
|
|
812
|
+
originY,
|
|
813
|
+
width,
|
|
814
|
+
height,
|
|
815
|
+
animationMode: (animate
|
|
816
|
+
? "continuous"
|
|
817
|
+
: "immediately") as AnimationMode,
|
|
818
|
+
});
|
|
497
819
|
}
|
|
498
|
-
}
|
|
820
|
+
};
|
|
499
821
|
|
|
500
|
-
dispose.add(
|
|
822
|
+
dispose.add(
|
|
823
|
+
view$$.addStateChangedListener(() => syncViewFromRemote(false, true)),
|
|
824
|
+
);
|
|
501
825
|
|
|
502
826
|
const getPageSize = () => {
|
|
503
|
-
const { width, height } = app.page() || {}
|
|
504
|
-
return { width: width || 0, height: height || 0 }
|
|
827
|
+
const { width, height } = app.page() || {};
|
|
828
|
+
return { width: width || 0, height: height || 0 };
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
const box = context.getBox();
|
|
832
|
+
const app = dispose.add(
|
|
833
|
+
createPresentation(
|
|
834
|
+
box,
|
|
835
|
+
pages,
|
|
836
|
+
jumpPage,
|
|
837
|
+
pageIndex$,
|
|
838
|
+
view,
|
|
839
|
+
options.thumbnail,
|
|
840
|
+
options.useClipView,
|
|
841
|
+
),
|
|
842
|
+
);
|
|
843
|
+
app.contentDOM.dataset.appPresentationVersion = __VERSION__;
|
|
844
|
+
app.scaleDocsToFit = scaleDocsToFit;
|
|
845
|
+
app.log = log;
|
|
846
|
+
app.warn = warn;
|
|
847
|
+
|
|
848
|
+
// Setup only touches the current page's full image; the rest of the deck
|
|
849
|
+
// keeps loading through idle callbacks after the setup queue moves on.
|
|
850
|
+
if (pageIndex$.value !== 0) {
|
|
851
|
+
app.preload.touch(pageIndex$.value, true);
|
|
505
852
|
}
|
|
506
853
|
|
|
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
|
|
513
|
-
|
|
514
854
|
const getCameraDiagnosticState = (
|
|
515
|
-
reason:
|
|
516
|
-
requestedCamera?: MoveCameraRequest
|
|
855
|
+
reason: "initialize" | "moveCamera",
|
|
856
|
+
requestedCamera?: MoveCameraRequest,
|
|
517
857
|
) => {
|
|
518
|
-
const page = app.page()
|
|
519
|
-
const pageSize =
|
|
520
|
-
|
|
521
|
-
|
|
858
|
+
const page = app.page();
|
|
859
|
+
const pageSize =
|
|
860
|
+
page && isValidSize(page)
|
|
861
|
+
? { width: page.width, height: page.height }
|
|
862
|
+
: undefined;
|
|
522
863
|
const referenceSize = pageSize
|
|
523
864
|
? getCameraReferenceSize(originSize, pageSize)
|
|
524
|
-
: undefined
|
|
525
|
-
const viewSize = { width: view.size.width, height: view.size.height }
|
|
865
|
+
: undefined;
|
|
866
|
+
const viewSize = { width: view.size.width, height: view.size.height };
|
|
526
867
|
const viewCamera = {
|
|
527
868
|
centerX: view.camera.centerX,
|
|
528
869
|
centerY: view.camera.centerY,
|
|
529
870
|
scale: view.camera.scale,
|
|
530
|
-
}
|
|
531
|
-
const sharedViewport = { ...view$$.state }
|
|
532
|
-
const originScale = referenceSize
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
871
|
+
};
|
|
872
|
+
const sharedViewport = { ...view$$.state };
|
|
873
|
+
const originScale = referenceSize
|
|
874
|
+
? getFitScale(viewSize, referenceSize)
|
|
875
|
+
: undefined;
|
|
876
|
+
const normalizedScale =
|
|
877
|
+
originScale && originScale > 0
|
|
878
|
+
? viewCamera.scale / originScale
|
|
879
|
+
: undefined;
|
|
880
|
+
const sharedScaleX =
|
|
881
|
+
referenceSize && sharedViewport.width > 0
|
|
882
|
+
? referenceSize.width / sharedViewport.width
|
|
883
|
+
: undefined;
|
|
884
|
+
const sharedScaleY =
|
|
885
|
+
referenceSize && sharedViewport.height > 0
|
|
886
|
+
? referenceSize.height / sharedViewport.height
|
|
887
|
+
: undefined;
|
|
542
888
|
|
|
543
889
|
return {
|
|
544
890
|
reason,
|
|
@@ -555,367 +901,542 @@ export const NetlessAppPresentation: NetlessApp<PresentationAttributes, {}, Pres
|
|
|
555
901
|
sharedScaleY,
|
|
556
902
|
focusScenePath: view.focusScenePath,
|
|
557
903
|
isWritable: context.getIsWritable(),
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
let didReportInitializedCamera = false
|
|
562
|
-
const reportInitializedCamera = (source:
|
|
563
|
-
if (
|
|
564
|
-
|
|
565
|
-
|
|
904
|
+
};
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
let didReportInitializedCamera = false;
|
|
908
|
+
const reportInitializedCamera = (source: "setup" | "onSizeUpdated") => {
|
|
909
|
+
if (
|
|
910
|
+
didReportInitializedCamera ||
|
|
911
|
+
!isValidSize(view.size) ||
|
|
912
|
+
!isValidSize(app.page())
|
|
913
|
+
)
|
|
914
|
+
return;
|
|
915
|
+
didReportInitializedCamera = true;
|
|
916
|
+
diagnosticLogger.info("initialize", {
|
|
566
917
|
source,
|
|
567
|
-
...getCameraDiagnosticState(
|
|
568
|
-
})
|
|
569
|
-
}
|
|
918
|
+
...getCameraDiagnosticState("initialize"),
|
|
919
|
+
});
|
|
920
|
+
};
|
|
570
921
|
|
|
571
922
|
if (originSize) {
|
|
572
|
-
let previousPageIndex = pageIndex$.value
|
|
573
|
-
dispose.add(
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
923
|
+
let previousPageIndex = pageIndex$.value;
|
|
924
|
+
dispose.add(
|
|
925
|
+
pageIndex$.subscribe((nextPageIndex) => {
|
|
926
|
+
if (nextPageIndex === previousPageIndex) return;
|
|
927
|
+
previousPageIndex = nextPageIndex;
|
|
928
|
+
scaleDocsToFit();
|
|
929
|
+
}),
|
|
930
|
+
);
|
|
578
931
|
}
|
|
579
932
|
|
|
580
933
|
if (options.justDocsViewReadonly) {
|
|
581
|
-
app.setDocsViewReadonly(true)
|
|
934
|
+
app.setDocsViewReadonly(true);
|
|
582
935
|
}
|
|
583
936
|
|
|
584
937
|
const goToPageByClick = () => {
|
|
585
|
-
const currentApplianceName =
|
|
586
|
-
|
|
938
|
+
const currentApplianceName =
|
|
939
|
+
context.getRoom()?.state?.memberState?.currentApplianceName ?? "";
|
|
940
|
+
if (!app.readonly && currentApplianceName === "clicker") {
|
|
587
941
|
nextPage();
|
|
588
942
|
}
|
|
589
|
-
}
|
|
943
|
+
};
|
|
590
944
|
|
|
591
|
-
if(room && options.goToPageByClick && app.whiteboardDOM) {
|
|
945
|
+
if (room && options.goToPageByClick && app.whiteboardDOM) {
|
|
592
946
|
dispose.make(() => {
|
|
593
|
-
app.whiteboardDOM.addEventListener(
|
|
594
|
-
return () =>
|
|
595
|
-
|
|
947
|
+
app.whiteboardDOM.addEventListener("click", goToPageByClick);
|
|
948
|
+
return () =>
|
|
949
|
+
app.whiteboardDOM.removeEventListener("click", goToPageByClick);
|
|
950
|
+
});
|
|
596
951
|
}
|
|
597
952
|
|
|
598
|
-
context.mountView(app.whiteboardDOM)
|
|
953
|
+
context.mountView(app.whiteboardDOM);
|
|
599
954
|
if (shouldDisableDeviceCameraTransform(options)) {
|
|
600
|
-
view.disableCameraTransform = true
|
|
955
|
+
view.disableCameraTransform = true;
|
|
601
956
|
}
|
|
602
|
-
scaleDocsToFit()
|
|
957
|
+
scaleDocsToFit();
|
|
603
958
|
dispose.make(() => {
|
|
604
959
|
const onSizeUpdated = () => {
|
|
605
|
-
scaleDocsToFit()
|
|
606
|
-
reportInitializedCamera(
|
|
607
|
-
}
|
|
608
|
-
view.callbacks.on(
|
|
609
|
-
return () => view.callbacks.off(
|
|
610
|
-
})
|
|
960
|
+
scaleDocsToFit();
|
|
961
|
+
reportInitializedCamera("onSizeUpdated");
|
|
962
|
+
};
|
|
963
|
+
view.callbacks.on("onSizeUpdated", onSizeUpdated);
|
|
964
|
+
return () => view.callbacks.off("onSizeUpdated", onSizeUpdated);
|
|
965
|
+
});
|
|
611
966
|
|
|
612
967
|
// Init viewport if provided `viewport`.
|
|
613
968
|
if (options.viewport && context.isAddApp && app.page()) {
|
|
614
|
-
const page = app.page()
|
|
615
|
-
const viewport =
|
|
616
|
-
|
|
969
|
+
const page = app.page()!;
|
|
970
|
+
const viewport =
|
|
971
|
+
typeof options.viewport === "function"
|
|
972
|
+
? options.viewport(page)
|
|
973
|
+
: options.viewport;
|
|
974
|
+
const fix = (i: number, x: number) => (i == 0 ? i : i <= 1 ? i * x : i);
|
|
617
975
|
view$$.setState({
|
|
618
976
|
uid: me,
|
|
619
977
|
originX: fix(viewport.x, page.width) - page.width / 2,
|
|
620
978
|
originY: fix(viewport.y, page.height) - page.height / 2,
|
|
621
979
|
width: fix(viewport.width, page.width),
|
|
622
980
|
height: fix(viewport.height, page.height),
|
|
623
|
-
})
|
|
981
|
+
});
|
|
624
982
|
}
|
|
625
983
|
|
|
626
984
|
dispose.make(() => {
|
|
627
|
-
view.callbacks.on(
|
|
628
|
-
return () => view.callbacks.off(
|
|
629
|
-
})
|
|
630
|
-
|
|
631
|
-
syncViewFromRemote(true)
|
|
632
|
-
reportInitializedCamera('setup')
|
|
985
|
+
view.callbacks.on("onCameraUpdatedByDevice", syncView);
|
|
986
|
+
return () => view.callbacks.off("onCameraUpdatedByDevice", syncView);
|
|
987
|
+
});
|
|
633
988
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
}))
|
|
989
|
+
syncViewFromRemote(true);
|
|
990
|
+
reportInitializedCamera("setup");
|
|
637
991
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
992
|
+
dispose.add(
|
|
993
|
+
context.emitter.on("writableChange", (isWritable: boolean): void => {
|
|
994
|
+
app.setReadonly(!isWritable);
|
|
995
|
+
}),
|
|
996
|
+
);
|
|
643
997
|
|
|
644
|
-
|
|
645
|
-
|
|
998
|
+
if (!options.disableBlurThumbnailDegradation) {
|
|
999
|
+
setupBlurThumbnailDegradation({ context, view, pages, pageIndex$, app, dispose });
|
|
646
1000
|
}
|
|
647
1001
|
|
|
648
|
-
const
|
|
1002
|
+
const getOriginScale = () => {
|
|
1003
|
+
const page = app.page();
|
|
1004
|
+
if (!page || !isValidSize(page)) return 0;
|
|
1005
|
+
return (
|
|
1006
|
+
getFitScale(view.size, getCameraReferenceSize(originSize, page)) || 0
|
|
1007
|
+
);
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
const getScale = () => {
|
|
1011
|
+
return view.camera.scale;
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
const screenshotCurrentPage = async (
|
|
1015
|
+
_context: CanvasRenderingContext2D,
|
|
1016
|
+
_width?: number,
|
|
1017
|
+
_height?: number,
|
|
1018
|
+
) => {
|
|
649
1019
|
const currentPage = pages[pageIndex$.value];
|
|
650
1020
|
if (!currentPage) {
|
|
651
|
-
throw new Error(
|
|
1021
|
+
throw new Error("[Presentation]: current page not found");
|
|
652
1022
|
}
|
|
653
1023
|
const { width, height, src } = currentPage;
|
|
654
1024
|
|
|
655
|
-
const img = document.createElement(
|
|
1025
|
+
const img = document.createElement("img");
|
|
656
1026
|
img.width = width;
|
|
657
1027
|
img.height = height;
|
|
658
|
-
img.crossOrigin =
|
|
1028
|
+
img.crossOrigin = "Anonymous";
|
|
659
1029
|
await new Promise<void>((resolve, reject) => {
|
|
660
|
-
img.onload = () => resolve()
|
|
661
|
-
img.onerror = () =>
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1030
|
+
img.onload = () => resolve();
|
|
1031
|
+
img.onerror = () =>
|
|
1032
|
+
reject(
|
|
1033
|
+
new Error(
|
|
1034
|
+
`[Presentation]: failed to load screenshot page image: ${safeResourceLocation(src)}`,
|
|
1035
|
+
),
|
|
1036
|
+
);
|
|
1037
|
+
img.src = src;
|
|
1038
|
+
});
|
|
1039
|
+
_context.drawImage(
|
|
1040
|
+
img,
|
|
1041
|
+
0,
|
|
1042
|
+
0,
|
|
1043
|
+
width,
|
|
1044
|
+
height,
|
|
1045
|
+
0,
|
|
1046
|
+
0,
|
|
1047
|
+
_width || width,
|
|
1048
|
+
_height || height,
|
|
1049
|
+
);
|
|
667
1050
|
const currentScenePath = view.focusScenePath;
|
|
668
1051
|
if (!currentScenePath) {
|
|
669
|
-
throw new Error(
|
|
1052
|
+
throw new Error("[Presentation]: current scene path not found");
|
|
670
1053
|
}
|
|
671
1054
|
const windowManger = context.getWindowManager() as any;
|
|
672
1055
|
if (windowManger._appliancePlugin) {
|
|
673
|
-
await windowManger._appliancePlugin.screenshotToCanvasAsync(
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1056
|
+
await windowManger._appliancePlugin.screenshotToCanvasAsync(
|
|
1057
|
+
_context,
|
|
1058
|
+
currentScenePath,
|
|
1059
|
+
_width || width,
|
|
1060
|
+
_height || height,
|
|
1061
|
+
{
|
|
1062
|
+
centerX: 0,
|
|
1063
|
+
centerY: 0,
|
|
1064
|
+
scale:
|
|
1065
|
+
_width && _height
|
|
1066
|
+
? Math.min(_width / width, _height / height)
|
|
1067
|
+
: 1,
|
|
1068
|
+
},
|
|
1069
|
+
);
|
|
678
1070
|
} else {
|
|
679
|
-
await view.screenshotToCanvasAsync(
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1071
|
+
await view.screenshotToCanvasAsync(
|
|
1072
|
+
_context,
|
|
1073
|
+
currentScenePath,
|
|
1074
|
+
_width || width,
|
|
1075
|
+
_height || height,
|
|
1076
|
+
{
|
|
1077
|
+
centerX: 0,
|
|
1078
|
+
centerY: 0,
|
|
1079
|
+
scale:
|
|
1080
|
+
_width && _height
|
|
1081
|
+
? Math.min(_width / width, _height / height)
|
|
1082
|
+
: 1,
|
|
1083
|
+
},
|
|
1084
|
+
);
|
|
684
1085
|
}
|
|
685
|
-
}
|
|
1086
|
+
};
|
|
686
1087
|
|
|
687
1088
|
const screenshotCurrentPageAsync = async (
|
|
688
1089
|
_context: CanvasRenderingContext2D,
|
|
689
1090
|
_width?: number,
|
|
690
|
-
_height?: number
|
|
1091
|
+
_height?: number,
|
|
691
1092
|
) => {
|
|
692
1093
|
try {
|
|
693
|
-
await screenshotCurrentPage(_context, _width, _height)
|
|
1094
|
+
await screenshotCurrentPage(_context, _width, _height);
|
|
694
1095
|
} catch (error) {
|
|
695
|
-
diagnosticLogger.error(
|
|
1096
|
+
diagnosticLogger.error("screenshotCurrentPage.failed", error, {
|
|
696
1097
|
pageIndex: pageIndex$.value,
|
|
697
1098
|
outputSize: { width: _width, height: _height },
|
|
698
|
-
camera: getCameraDiagnosticState(
|
|
699
|
-
})
|
|
700
|
-
throw error
|
|
1099
|
+
camera: getCameraDiagnosticState("initialize"),
|
|
1100
|
+
});
|
|
1101
|
+
throw error;
|
|
701
1102
|
}
|
|
702
|
-
}
|
|
1103
|
+
};
|
|
703
1104
|
|
|
704
|
-
let scrollbar:Scrollbar | undefined;
|
|
1105
|
+
let scrollbar: Scrollbar | undefined;
|
|
705
1106
|
if (options.useScrollbar) {
|
|
706
1107
|
dispose.make(() => {
|
|
707
|
-
scrollbar = new Scrollbar(
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
1108
|
+
scrollbar = new Scrollbar(
|
|
1109
|
+
app.contentDOM,
|
|
1110
|
+
{
|
|
1111
|
+
appId: context.appId,
|
|
1112
|
+
getPageSize,
|
|
1113
|
+
getOriginScale,
|
|
1114
|
+
syncView,
|
|
1115
|
+
getWritable: () => context.getIsWritable(),
|
|
1116
|
+
scrollbarEventCallback: options.scrollbarEventCallback,
|
|
1117
|
+
},
|
|
1118
|
+
view,
|
|
1119
|
+
);
|
|
1120
|
+
return () => scrollbar?.destroy();
|
|
1121
|
+
});
|
|
717
1122
|
}
|
|
718
1123
|
|
|
719
|
-
const moveCamera = (camera: {
|
|
1124
|
+
const moveCamera = (camera: {
|
|
1125
|
+
centerX: number;
|
|
1126
|
+
centerY: number;
|
|
1127
|
+
scale: number;
|
|
1128
|
+
}) => {
|
|
720
1129
|
try {
|
|
721
1130
|
if (!context.getIsWritable()) {
|
|
722
|
-
throw new Error(
|
|
1131
|
+
throw new Error(
|
|
1132
|
+
"[Presentation]: moveCamera must be called in writable room",
|
|
1133
|
+
);
|
|
723
1134
|
}
|
|
724
|
-
pendingMoveCameraRequest = { ...camera }
|
|
1135
|
+
pendingMoveCameraRequest = { ...camera };
|
|
725
1136
|
if (scrollbar) {
|
|
726
1137
|
scrollbar.moveCamera(camera);
|
|
727
1138
|
return;
|
|
728
1139
|
}
|
|
729
1140
|
view.moveCamera({
|
|
730
1141
|
...camera,
|
|
731
|
-
animationMode:
|
|
732
|
-
})
|
|
733
|
-
syncView()
|
|
1142
|
+
animationMode: "immediately" as AnimationMode.Immediately,
|
|
1143
|
+
});
|
|
1144
|
+
syncView();
|
|
734
1145
|
} catch (error) {
|
|
735
|
-
pendingMoveCameraRequest = undefined
|
|
1146
|
+
pendingMoveCameraRequest = undefined;
|
|
736
1147
|
diagnosticLogger.error(
|
|
737
|
-
|
|
1148
|
+
"moveCamera.failed",
|
|
738
1149
|
error,
|
|
739
|
-
getCameraDiagnosticState(
|
|
740
|
-
)
|
|
741
|
-
throw error
|
|
1150
|
+
getCameraDiagnosticState("moveCamera", camera),
|
|
1151
|
+
);
|
|
1152
|
+
throw error;
|
|
742
1153
|
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
let disposed = false;
|
|
1157
|
+
let offDestroy: (() => void) | undefined;
|
|
1158
|
+
const teardown = () => {
|
|
1159
|
+
if (disposed) return;
|
|
1160
|
+
disposed = true;
|
|
1161
|
+
const removeDestroy = offDestroy;
|
|
1162
|
+
offDestroy = undefined;
|
|
1163
|
+
removeDestroy?.();
|
|
1164
|
+
dispose();
|
|
1165
|
+
};
|
|
1166
|
+
offDestroy = context.emitter.on("destroy", teardown);
|
|
1167
|
+
teardownByContext.set(context, teardown);
|
|
1168
|
+
|
|
1169
|
+
const reportProgress = (
|
|
1170
|
+
progress: number,
|
|
1171
|
+
result: { pdf: ArrayBuffer; title: string } | null,
|
|
1172
|
+
) => {
|
|
1173
|
+
window.postMessage(
|
|
1174
|
+
{
|
|
1175
|
+
type: "@netless/_result_save_pdf_",
|
|
1176
|
+
appId: context.appId,
|
|
1177
|
+
progress,
|
|
1178
|
+
result,
|
|
1179
|
+
},
|
|
1180
|
+
"*",
|
|
1181
|
+
);
|
|
1182
|
+
return result;
|
|
1183
|
+
};
|
|
751
1184
|
|
|
752
1185
|
const base64url = async (url: string): Promise<string> => {
|
|
753
1186
|
try {
|
|
754
|
-
const a = new URL(url)
|
|
755
|
-
a.searchParams.set(
|
|
756
|
-
url = a.toString()
|
|
1187
|
+
const a = new URL(url);
|
|
1188
|
+
a.searchParams.set("t", Date.now().toString());
|
|
1189
|
+
url = a.toString();
|
|
757
1190
|
} catch {}
|
|
758
1191
|
|
|
759
|
-
const data = await fetch(url)
|
|
1192
|
+
const data = await fetch(url);
|
|
760
1193
|
if (!data.ok) {
|
|
761
1194
|
throw new Error(
|
|
762
|
-
`[Presentation]: failed to fetch ${safeResourceLocation(url)}, status: ${data.status} ${data.statusText}
|
|
763
|
-
)
|
|
1195
|
+
`[Presentation]: failed to fetch ${safeResourceLocation(url)}, status: ${data.status} ${data.statusText}`,
|
|
1196
|
+
);
|
|
764
1197
|
}
|
|
765
1198
|
|
|
766
|
-
const blob = await data.blob()
|
|
767
|
-
const reader = new FileReader()
|
|
1199
|
+
const blob = await data.blob();
|
|
1200
|
+
const reader = new FileReader();
|
|
768
1201
|
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<{
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1202
|
+
reader.onload = () => resolve(reader.result as string);
|
|
1203
|
+
reader.onerror = () => reject(reader.error);
|
|
1204
|
+
reader.readAsDataURL(blob);
|
|
1205
|
+
});
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1208
|
+
const toPdfInternal = async (): Promise<{
|
|
1209
|
+
pdf: ArrayBuffer;
|
|
1210
|
+
title: string;
|
|
1211
|
+
} | null> => {
|
|
1212
|
+
const MAX = 1920;
|
|
1213
|
+
const firstPage = pages[0];
|
|
1214
|
+
const { width, height } = firstPage;
|
|
1215
|
+
let pdfWidth = Math.floor(width),
|
|
1216
|
+
pdfHeight = Math.floor(height);
|
|
780
1217
|
if (pdfWidth > MAX) {
|
|
781
|
-
pdfWidth = MAX
|
|
782
|
-
pdfHeight = Math.floor(height * pdfWidth / width)
|
|
1218
|
+
pdfWidth = MAX;
|
|
1219
|
+
pdfHeight = Math.floor((height * pdfWidth) / width);
|
|
783
1220
|
}
|
|
784
1221
|
if (pdfHeight > MAX) {
|
|
785
|
-
pdfHeight = MAX
|
|
786
|
-
pdfWidth = Math.floor(width * pdfHeight / height)
|
|
1222
|
+
pdfHeight = MAX;
|
|
1223
|
+
pdfWidth = Math.floor((width * pdfHeight) / height);
|
|
787
1224
|
}
|
|
788
|
-
const scenes = context.getDisplayer().entireScenes()[scenePath]
|
|
1225
|
+
const scenes = context.getDisplayer().entireScenes()[scenePath];
|
|
789
1226
|
if (!scenes) {
|
|
790
|
-
throw new Error(
|
|
1227
|
+
throw new Error(
|
|
1228
|
+
`[Presentation]: no scenes found while exporting PDF: ${scenePath}`,
|
|
1229
|
+
);
|
|
791
1230
|
}
|
|
792
1231
|
|
|
793
|
-
const stage_canvas = document.createElement(
|
|
794
|
-
stage_canvas.width = pdfWidth
|
|
795
|
-
stage_canvas.height = pdfHeight
|
|
796
|
-
const stage = stage_canvas.getContext(
|
|
1232
|
+
const stage_canvas = document.createElement("canvas");
|
|
1233
|
+
stage_canvas.width = pdfWidth;
|
|
1234
|
+
stage_canvas.height = pdfHeight;
|
|
1235
|
+
const stage = stage_canvas.getContext("2d");
|
|
797
1236
|
|
|
798
|
-
const wb_canvas = document.createElement(
|
|
799
|
-
wb_canvas.width = pdfWidth
|
|
800
|
-
wb_canvas.height = pdfHeight
|
|
801
|
-
const wb = wb_canvas.getContext(
|
|
802
|
-
if (!wb || !stage) return reportProgress(100, null)
|
|
1237
|
+
const wb_canvas = document.createElement("canvas");
|
|
1238
|
+
wb_canvas.width = pdfWidth;
|
|
1239
|
+
wb_canvas.height = pdfHeight;
|
|
1240
|
+
const wb = wb_canvas.getContext("2d");
|
|
1241
|
+
if (!wb || !stage) return reportProgress(100, null);
|
|
803
1242
|
|
|
804
|
-
const { jsPDF } = await import("jspdf")
|
|
1243
|
+
const { jsPDF } = await import("jspdf");
|
|
805
1244
|
const pdf = new jsPDF({
|
|
806
1245
|
format: [firstPage.width, firstPage.height],
|
|
807
|
-
orientation: firstPage.width > firstPage.height ?
|
|
1246
|
+
orientation: firstPage.width > firstPage.height ? "l" : "p",
|
|
808
1247
|
compress: true,
|
|
809
|
-
})
|
|
1248
|
+
});
|
|
810
1249
|
|
|
811
1250
|
for (let index = 0; index < pages.length; ++index) {
|
|
812
1251
|
const p = pages[index];
|
|
813
1252
|
const { width, height, src } = p;
|
|
814
1253
|
|
|
815
|
-
const url = await base64url(src)
|
|
816
|
-
const img = document.createElement(
|
|
1254
|
+
const url = await base64url(src);
|
|
1255
|
+
const img = document.createElement("img");
|
|
817
1256
|
await new Promise<void>((resolve, reject) => {
|
|
818
|
-
img.onload = () => resolve()
|
|
819
|
-
img.onerror = () =>
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1257
|
+
img.onload = () => resolve();
|
|
1258
|
+
img.onerror = () =>
|
|
1259
|
+
reject(
|
|
1260
|
+
new Error(
|
|
1261
|
+
`[Presentation]: failed to load PDF page image, page index: ${index}`,
|
|
1262
|
+
),
|
|
1263
|
+
);
|
|
1264
|
+
img.src = url;
|
|
1265
|
+
});
|
|
1266
|
+
stage.drawImage(img, 0, 0, width, height);
|
|
1267
|
+
|
|
1268
|
+
wb.clearRect(0, 0, pdfWidth, pdfHeight);
|
|
1269
|
+
const name = p.name ?? String(index + 1);
|
|
1270
|
+
if (scenes.some((scene) => scene.name == name)) {
|
|
1271
|
+
const camera = {
|
|
1272
|
+
centerX: 0,
|
|
1273
|
+
centerY: 0,
|
|
1274
|
+
scale: Math.min(wb_canvas.width / width, wb_canvas.height / height),
|
|
1275
|
+
};
|
|
830
1276
|
const sPath = `${scenePath}/${name}`;
|
|
831
1277
|
// appliancePlugin is a performance optimization for whiteboard;
|
|
832
1278
|
const windowManger = context.getWindowManager() as any;
|
|
833
1279
|
if (windowManger._appliancePlugin) {
|
|
834
|
-
await (
|
|
1280
|
+
await (
|
|
1281
|
+
windowManger as any
|
|
1282
|
+
)._appliancePlugin.screenshotToCanvasAsync(
|
|
1283
|
+
wb,
|
|
1284
|
+
sPath,
|
|
1285
|
+
wb_canvas.width,
|
|
1286
|
+
wb_canvas.height,
|
|
1287
|
+
camera,
|
|
1288
|
+
);
|
|
835
1289
|
} else {
|
|
836
1290
|
await view.screenshotToCanvasAsync(
|
|
837
|
-
wb,
|
|
838
|
-
|
|
1291
|
+
wb,
|
|
1292
|
+
sPath,
|
|
1293
|
+
wb_canvas.width,
|
|
1294
|
+
wb_canvas.height,
|
|
839
1295
|
camera,
|
|
840
|
-
)
|
|
1296
|
+
);
|
|
841
1297
|
}
|
|
842
1298
|
|
|
843
1299
|
try {
|
|
844
|
-
const wb_url = wb_canvas.toDataURL(
|
|
845
|
-
const wb_img = document.createElement(
|
|
846
|
-
await new Promise(resolve => {
|
|
847
|
-
|
|
1300
|
+
const wb_url = wb_canvas.toDataURL("image/png");
|
|
1301
|
+
const wb_img = document.createElement("img");
|
|
1302
|
+
await new Promise((resolve) => {
|
|
1303
|
+
wb_img.onload = resolve;
|
|
1304
|
+
wb_img.src = wb_url;
|
|
1305
|
+
});
|
|
1306
|
+
stage.drawImage(wb_img, 0, 0, pdfWidth, pdfHeight);
|
|
848
1307
|
} catch (err) {
|
|
849
|
-
warn(err)
|
|
1308
|
+
warn(err);
|
|
850
1309
|
}
|
|
851
1310
|
}
|
|
852
1311
|
|
|
853
|
-
const output = stage_canvas.toDataURL(
|
|
854
|
-
if (index > 0) pdf.addPage()
|
|
1312
|
+
const output = stage_canvas.toDataURL("image/jpeg", 0.6);
|
|
1313
|
+
if (index > 0) pdf.addPage();
|
|
855
1314
|
|
|
856
|
-
pdf.addImage(output,
|
|
857
|
-
stage.clearRect(0, 0, pdfWidth, pdfHeight)
|
|
858
|
-
const progress = Math.ceil((index + 1) / pages.length * 100)
|
|
859
|
-
if (progress < 100) reportProgress(progress, null)
|
|
1315
|
+
pdf.addImage(output, "JPEG", 0, 0, pdfWidth, pdfHeight, "", "FAST");
|
|
1316
|
+
stage.clearRect(0, 0, pdfWidth, pdfHeight);
|
|
1317
|
+
const progress = Math.ceil(((index + 1) / pages.length) * 100);
|
|
1318
|
+
if (progress < 100) reportProgress(progress, null);
|
|
860
1319
|
}
|
|
861
1320
|
|
|
862
|
-
const data = pdf.output(
|
|
863
|
-
const title = box.title
|
|
864
|
-
return reportProgress(100, { pdf: data, title })
|
|
865
|
-
}
|
|
1321
|
+
const data = pdf.output("arraybuffer");
|
|
1322
|
+
const title = box.title;
|
|
1323
|
+
return reportProgress(100, { pdf: data, title });
|
|
1324
|
+
};
|
|
866
1325
|
|
|
867
|
-
const toPdf = async (): Promise<{
|
|
1326
|
+
const toPdf = async (): Promise<{
|
|
1327
|
+
pdf: ArrayBuffer;
|
|
1328
|
+
title: string;
|
|
1329
|
+
} | null> => {
|
|
868
1330
|
try {
|
|
869
|
-
return await toPdfInternal()
|
|
1331
|
+
return await toPdfInternal();
|
|
870
1332
|
} catch (error) {
|
|
871
|
-
diagnosticLogger.error(
|
|
1333
|
+
diagnosticLogger.error("toPdf.failed", error, {
|
|
872
1334
|
pageIndex: pageIndex$.value,
|
|
873
1335
|
pageCount: pages.length,
|
|
874
1336
|
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) })
|
|
1337
|
+
});
|
|
1338
|
+
throw error;
|
|
883
1339
|
}
|
|
884
|
-
}
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
dispose.add(
|
|
1343
|
+
listen(
|
|
1344
|
+
window,
|
|
1345
|
+
"message",
|
|
1346
|
+
(
|
|
1347
|
+
ev: MessageEvent<{
|
|
1348
|
+
appId: string;
|
|
1349
|
+
type: "@netless/_request_save_pdf_";
|
|
1350
|
+
}>,
|
|
1351
|
+
) => {
|
|
1352
|
+
if (
|
|
1353
|
+
ev.data &&
|
|
1354
|
+
ev.data.type == "@netless/_request_save_pdf_" &&
|
|
1355
|
+
ev.data.appId == context.appId
|
|
1356
|
+
) {
|
|
1357
|
+
toPdf().catch((err) => {
|
|
1358
|
+
warn(err);
|
|
1359
|
+
reportProgress(100, null);
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
},
|
|
1363
|
+
),
|
|
1364
|
+
);
|
|
885
1365
|
|
|
886
1366
|
const setDocsViewReadonly = (bol: boolean) => {
|
|
887
|
-
app.setDocsViewReadonly(bol)
|
|
888
|
-
}
|
|
1367
|
+
app.setDocsViewReadonly(bol);
|
|
1368
|
+
};
|
|
889
1369
|
|
|
890
1370
|
const setReadonly = (bol: boolean) => {
|
|
891
|
-
app.setReadonly(bol)
|
|
1371
|
+
app.setReadonly(bol);
|
|
892
1372
|
if (options.goToPageByClick) {
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
if(scrollbar) {
|
|
896
|
-
scrollbar.setReadonly(bol)
|
|
897
1373
|
}
|
|
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)
|
|
1374
|
+
if (scrollbar) {
|
|
1375
|
+
scrollbar.setReadonly(bol);
|
|
910
1376
|
}
|
|
911
|
-
}
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
const controller: PresentationController = {
|
|
1380
|
+
app,
|
|
1381
|
+
view,
|
|
1382
|
+
context,
|
|
1383
|
+
jumpPage,
|
|
1384
|
+
prevPage,
|
|
1385
|
+
nextPage,
|
|
1386
|
+
jumpPageAsync,
|
|
1387
|
+
prevPageAsync,
|
|
1388
|
+
nextPageAsync,
|
|
1389
|
+
pageState,
|
|
1390
|
+
toPdf,
|
|
1391
|
+
log,
|
|
1392
|
+
setDocsViewReadonly,
|
|
1393
|
+
setReadonly,
|
|
1394
|
+
moveCamera,
|
|
1395
|
+
getOriginScale,
|
|
1396
|
+
getScale,
|
|
1397
|
+
getPageSize,
|
|
1398
|
+
screenshotCurrentPageAsync,
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
dispose.add(
|
|
1402
|
+
listen(
|
|
1403
|
+
window,
|
|
1404
|
+
"message",
|
|
1405
|
+
(ev: MessageEvent<"@netless/_presentation_">) => {
|
|
1406
|
+
if (ev.data === "@netless/_presentation_") {
|
|
1407
|
+
if (typeof window !== "undefined")
|
|
1408
|
+
dispose.make(() => {
|
|
1409
|
+
const debug: Set<PresentationController> = ((
|
|
1410
|
+
window as any
|
|
1411
|
+
)._presentation_ ||= new Set());
|
|
1412
|
+
debug.add(controller);
|
|
1413
|
+
return () => debug.delete(controller);
|
|
1414
|
+
});
|
|
1415
|
+
console.log(controller);
|
|
1416
|
+
}
|
|
1417
|
+
},
|
|
1418
|
+
),
|
|
1419
|
+
);
|
|
912
1420
|
|
|
913
1421
|
// Older WindowManager declarations model setup as synchronous even though
|
|
914
1422
|
// AppProxy awaits its result. Keep that source compatibility until the new
|
|
915
1423
|
// `SetupResult | Promise<SetupResult>` declaration is the minimum version.
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1424
|
+
const setupReadyTimeout = options.setupReadyTimeout ?? DEFAULT_SETUP_READY_TIMEOUT;
|
|
1425
|
+
return prepareScenesPromise.then(async () => {
|
|
1426
|
+
// Keep the serial setup queue honest: resolve only once the current
|
|
1427
|
+
// page image is actually visible (or the bounded wait gives up).
|
|
1428
|
+
const loaded = await waitForCurrentPageImage(view, setupReadyTimeout, () => disposed);
|
|
1429
|
+
if (!loaded) {
|
|
1430
|
+
warn(`[Presentation] setup ready wait timed out after ${setupReadyTimeout}ms`);
|
|
1431
|
+
}
|
|
1432
|
+
return controller;
|
|
1433
|
+
}) as unknown as PresentationController;
|
|
1434
|
+
},
|
|
1435
|
+
teardown(context) {
|
|
1436
|
+
teardownByContext.get(context)?.();
|
|
1437
|
+
teardownByContext.delete(context);
|
|
1438
|
+
},
|
|
1439
|
+
};
|
|
919
1440
|
|
|
920
1441
|
/**
|
|
921
1442
|
* Add synchronization to the local presentation.
|
|
@@ -925,11 +1446,13 @@ class AppPresentation extends Presentation {
|
|
|
925
1446
|
warn?: Logger;
|
|
926
1447
|
box?: ReadonlyTeleBox;
|
|
927
1448
|
scaleDocsToFit?: () => void;
|
|
928
|
-
readonly jumpPage: (index: number) => void
|
|
1449
|
+
readonly jumpPage: (index: number) => void;
|
|
929
1450
|
|
|
930
|
-
constructor(
|
|
931
|
-
|
|
932
|
-
|
|
1451
|
+
constructor(
|
|
1452
|
+
config: PresentationConfig & { jumpPage: (index: number) => void },
|
|
1453
|
+
) {
|
|
1454
|
+
super(config);
|
|
1455
|
+
this.jumpPage = config.jumpPage;
|
|
933
1456
|
// this.image.style.display = 'none'
|
|
934
1457
|
}
|
|
935
1458
|
|
|
@@ -938,17 +1461,21 @@ class AppPresentation extends Presentation {
|
|
|
938
1461
|
super.updateImage();
|
|
939
1462
|
}
|
|
940
1463
|
|
|
941
|
-
override onNewPageIndex(
|
|
1464
|
+
override onNewPageIndex(
|
|
1465
|
+
index: number,
|
|
1466
|
+
origin: "navigation" | "keydown" | "input" | "preview",
|
|
1467
|
+
) {
|
|
942
1468
|
// If it is triggered by global keydown (left or right arrow),
|
|
943
1469
|
// only the focused one should work
|
|
944
|
-
if (origin === "keydown" && this.box && !this.box.focus)
|
|
945
|
-
return
|
|
1470
|
+
if (origin === "keydown" && this.box && !this.box.focus) return;
|
|
946
1471
|
if (this.log)
|
|
947
|
-
this.log("[Presentation] user navigate to", index + 1, `(${origin})`)
|
|
1472
|
+
this.log("[Presentation] user navigate to", index + 1, `(${origin})`);
|
|
948
1473
|
if (0 <= index && index < this.pages.length) {
|
|
949
|
-
this.jumpPage(index)
|
|
1474
|
+
this.jumpPage(index);
|
|
950
1475
|
} else {
|
|
951
|
-
this.warn?.(
|
|
1476
|
+
this.warn?.(
|
|
1477
|
+
`[Presentation]: page index ${index} out of bounds [0, ${this.pages.length - 1}]`,
|
|
1478
|
+
);
|
|
952
1479
|
}
|
|
953
1480
|
}
|
|
954
1481
|
}
|
|
@@ -962,44 +1489,56 @@ function createPresentation(
|
|
|
962
1489
|
thumbnail?: (src: string) => string,
|
|
963
1490
|
useClipView?: boolean,
|
|
964
1491
|
): AppPresentation {
|
|
965
|
-
box.mountStyles(styles)
|
|
1492
|
+
box.mountStyles(styles);
|
|
966
1493
|
|
|
967
|
-
const app = new AppPresentation({
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1494
|
+
const app = new AppPresentation({
|
|
1495
|
+
pages,
|
|
1496
|
+
readonly: box.readonly,
|
|
1497
|
+
jumpPage,
|
|
1498
|
+
thumbnail,
|
|
1499
|
+
});
|
|
1500
|
+
app.box = box;
|
|
1501
|
+
box.mountContent(app.contentDOM);
|
|
1502
|
+
box.mountFooter(app.footerDOM);
|
|
971
1503
|
|
|
972
|
-
app.dispose.add(
|
|
1504
|
+
app.dispose.add(
|
|
1505
|
+
pageIndex$.subscribe((pageIndex) => {
|
|
1506
|
+
app.setPageIndex(pageIndex);
|
|
1507
|
+
}),
|
|
1508
|
+
);
|
|
973
1509
|
|
|
974
|
-
if(useClipView && view){
|
|
1510
|
+
if (useClipView && view) {
|
|
975
1511
|
const onCameraUpdatedEffectForMaskView = debounce(() => {
|
|
976
1512
|
const { width: pageWidth, height: pageHeight } = app.page() || {};
|
|
977
|
-
if(!pageWidth || !pageHeight) {
|
|
1513
|
+
if (!pageWidth || !pageHeight) {
|
|
978
1514
|
return;
|
|
979
1515
|
}
|
|
980
1516
|
const { scale } = view.camera;
|
|
981
1517
|
const { width, height } = view.size;
|
|
982
|
-
const pageRatioX = Math.round(
|
|
983
|
-
|
|
1518
|
+
const pageRatioX = Math.round(
|
|
1519
|
+
((1 - (pageWidth * scale) / width) / 2) * 100,
|
|
1520
|
+
);
|
|
1521
|
+
const pageRatioY = Math.round(
|
|
1522
|
+
((1 - (pageHeight * scale) / height) / 2) * 100,
|
|
1523
|
+
);
|
|
984
1524
|
app.whiteboardDOM.style.clipPath = `inset(${pageRatioY}% ${pageRatioX}%)`;
|
|
985
|
-
}, 50)
|
|
1525
|
+
}, 50);
|
|
986
1526
|
onCameraUpdatedEffectForMaskView();
|
|
987
1527
|
const onCameraUpdatedEffect = () => {
|
|
988
|
-
view.callbacks.on("onSizeUpdated", onCameraUpdatedEffectForMaskView)
|
|
989
|
-
view.callbacks.on("onCameraUpdated", onCameraUpdatedEffectForMaskView)
|
|
1528
|
+
view.callbacks.on("onSizeUpdated", onCameraUpdatedEffectForMaskView);
|
|
1529
|
+
view.callbacks.on("onCameraUpdated", onCameraUpdatedEffectForMaskView);
|
|
990
1530
|
return () => {
|
|
991
1531
|
view.callbacks.off("onCameraUpdated", onCameraUpdatedEffectForMaskView);
|
|
992
1532
|
view.callbacks.off("onSizeUpdated", onCameraUpdatedEffectForMaskView);
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
app.dispose.make(onCameraUpdatedEffect)
|
|
1533
|
+
};
|
|
1534
|
+
};
|
|
1535
|
+
app.dispose.make(onCameraUpdatedEffect);
|
|
996
1536
|
}
|
|
997
|
-
|
|
998
1537
|
|
|
999
|
-
return app
|
|
1538
|
+
return app;
|
|
1000
1539
|
}
|
|
1001
1540
|
|
|
1002
|
-
export type RegisterFn = typeof WindowManager["register"]
|
|
1541
|
+
export type RegisterFn = (typeof WindowManager)["register"];
|
|
1003
1542
|
|
|
1004
1543
|
export interface InstallOptions {
|
|
1005
1544
|
/**
|
|
@@ -1008,11 +1547,11 @@ export interface InstallOptions {
|
|
|
1008
1547
|
*
|
|
1009
1548
|
* @example "DocsViewer"
|
|
1010
1549
|
*/
|
|
1011
|
-
as?: string
|
|
1550
|
+
as?: string;
|
|
1012
1551
|
/**
|
|
1013
1552
|
* Options to customize the local app (not synced to others).
|
|
1014
1553
|
*/
|
|
1015
|
-
appOptions?: PresentationAppOptions
|
|
1554
|
+
appOptions?: PresentationAppOptions;
|
|
1016
1555
|
}
|
|
1017
1556
|
|
|
1018
1557
|
/**
|
|
@@ -1021,10 +1560,13 @@ export interface InstallOptions {
|
|
|
1021
1560
|
*
|
|
1022
1561
|
* @example install(register, { as: "DocsViewer" })
|
|
1023
1562
|
*/
|
|
1024
|
-
export const install = (
|
|
1025
|
-
|
|
1563
|
+
export const install = (
|
|
1564
|
+
register: RegisterFn,
|
|
1565
|
+
options: InstallOptions = {},
|
|
1566
|
+
): Promise<void> => {
|
|
1567
|
+
let app = NetlessAppPresentation;
|
|
1026
1568
|
if (options.as) {
|
|
1027
|
-
app = Object.assign({}, app, { kind: options.as })
|
|
1569
|
+
app = Object.assign({}, app, { kind: options.as });
|
|
1028
1570
|
}
|
|
1029
|
-
return register({ kind: app.kind, src: app, appOptions: options.appOptions })
|
|
1030
|
-
}
|
|
1571
|
+
return register({ kind: app.kind, src: app, appOptions: options.appOptions });
|
|
1572
|
+
};
|