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