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