@netless/app-presentation 0.1.13-beta.0 → 0.1.13-beta.2
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 +84 -1
- package/dist/index.global.js +282 -14
- package/dist/index.js +282 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +282 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/app-presentation.ts +322 -4
- package/src/preload.ts +14 -0
- package/src/presentation.ts +18 -1
- package/src/runtime-lifecycle.ts +24 -0
- package/src/style.scss +7 -1
package/src/app-presentation.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
isValidSharedViewport,
|
|
36
36
|
isValidSize,
|
|
37
37
|
} from "./camera-reference";
|
|
38
|
+
import { createPresentationRuntimeTeardown } from "./runtime-lifecycle";
|
|
38
39
|
import debounce from "lodash/debounce";
|
|
39
40
|
|
|
40
41
|
export type Logger = (...data: any[]) => void;
|
|
@@ -96,6 +97,17 @@ export interface PresentationAppOptions {
|
|
|
96
97
|
goToPageByClick?: boolean;
|
|
97
98
|
/** 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 */
|
|
98
99
|
useClipView?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Max time (ms) `setup()` waits for the first visible page image to load
|
|
102
|
+
* before resolving anyway. Default: 5_000.
|
|
103
|
+
*/
|
|
104
|
+
setupReadyTimeout?: number;
|
|
105
|
+
/**
|
|
106
|
+
* Blur thumbnail degradation keeps cached (focused-lost) presentations on
|
|
107
|
+
* the low-res `previewURL` thumbnail to save memory. Enabled by default in
|
|
108
|
+
* maximized/minimized states; set `true` to disable it.
|
|
109
|
+
*/
|
|
110
|
+
disableBlurThumbnailDegradation?: boolean;
|
|
99
111
|
}
|
|
100
112
|
|
|
101
113
|
export interface PresentationController {
|
|
@@ -154,6 +166,285 @@ const ppt2page = (
|
|
|
154
166
|
return { ...size, src: ppt.src, thumbnail: ppt.previewURL, name };
|
|
155
167
|
};
|
|
156
168
|
|
|
169
|
+
/**
|
|
170
|
+
* The whiteboard SDK renders the scene ppt as a real `<img alt="background">`
|
|
171
|
+
* inside the background HTML engine layer of the view. All blur degradation
|
|
172
|
+
* below works directly on that DOM node; no scene API is ever called.
|
|
173
|
+
*/
|
|
174
|
+
const findBackgroundImage = (view: View): HTMLImageElement | null => {
|
|
175
|
+
const container = (view as any).divElement as HTMLElement | undefined | null;
|
|
176
|
+
if (!container || typeof container.querySelector !== "function") return null;
|
|
177
|
+
return container.querySelector('img[alt="background"]');
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Degradation is a lazy-mode cache optimization only: inactive unless the
|
|
182
|
+
* WindowManager runs with lazySetupInMaximizedMode enabled (read dynamically
|
|
183
|
+
* — lazy can be disabled at runtime, e.g. when forceMaximized is cleared).
|
|
184
|
+
* It also only applies when the window cannot be visible: maximized
|
|
185
|
+
* (covered by the top app) or minimized. Normal mode may show several
|
|
186
|
+
* windows side by side, so a blurred app there must keep the full image.
|
|
187
|
+
*/
|
|
188
|
+
const isBlurDegradationAllowed = (context: AppContext): boolean => {
|
|
189
|
+
if ((context.getWindowManager() as any)?.lazySetupInMaximizedMode !== true) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
const boxStatus = context.getBoxStatus();
|
|
193
|
+
if (boxStatus) {
|
|
194
|
+
return boxStatus !== "normal";
|
|
195
|
+
}
|
|
196
|
+
const boxState = context.getWindowManager().boxState;
|
|
197
|
+
return boxState !== undefined && boxState !== "normal";
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const DEFAULT_SETUP_READY_TIMEOUT = 5_000;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Resolve once the current page background image has loaded (or after
|
|
204
|
+
* `timeoutMs`). `false` means timeout or the app was disposed mid-wait;
|
|
205
|
+
* callers must treat it as "continue loading in background", not an error.
|
|
206
|
+
*/
|
|
207
|
+
const waitForCurrentPageImage = (
|
|
208
|
+
view: View,
|
|
209
|
+
timeoutMs: number,
|
|
210
|
+
isDisposed: () => boolean,
|
|
211
|
+
): Promise<boolean> => {
|
|
212
|
+
return new Promise<boolean>(resolve => {
|
|
213
|
+
let settled = false;
|
|
214
|
+
let pollTimer: number | undefined;
|
|
215
|
+
const settle = (loaded: boolean) => {
|
|
216
|
+
if (settled) return;
|
|
217
|
+
settled = true;
|
|
218
|
+
window.clearTimeout(timeoutTimer);
|
|
219
|
+
if (pollTimer !== undefined) window.clearInterval(pollTimer);
|
|
220
|
+
resolve(loaded);
|
|
221
|
+
};
|
|
222
|
+
const timeoutTimer = window.setTimeout(() => settle(false), timeoutMs);
|
|
223
|
+
const check = () => {
|
|
224
|
+
if (isDisposed()) {
|
|
225
|
+
settle(false);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const img = findBackgroundImage(view);
|
|
229
|
+
if (img && img.complete && img.naturalWidth > 0) {
|
|
230
|
+
settle(true);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
check();
|
|
234
|
+
if (!settled) {
|
|
235
|
+
pollTimer = window.setInterval(check, 100);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Compare image URLs by resolved absolute href so relative/absolute
|
|
242
|
+
* representations of the same URL do not look different (and to keep the
|
|
243
|
+
* load-capture re-degrade loop terminating).
|
|
244
|
+
*/
|
|
245
|
+
const resolveImageUrl = (url: string): string => {
|
|
246
|
+
try {
|
|
247
|
+
return new URL(url, window.location.href).href;
|
|
248
|
+
} catch {
|
|
249
|
+
return url;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const TRANSPARENT_BACKGROUND =
|
|
254
|
+
"data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
|
|
255
|
+
const BACKGROUND_SUSPENDED_CLASS =
|
|
256
|
+
"netless-app-presentation-background-suspended";
|
|
257
|
+
|
|
258
|
+
const hasUsableThumbnail = (page: PresentationPage): boolean => {
|
|
259
|
+
const thumbnailURL = page.thumbnail;
|
|
260
|
+
if (!thumbnailURL) return false;
|
|
261
|
+
try {
|
|
262
|
+
const source = new URL(page.src, window.location.href);
|
|
263
|
+
const thumbnail = new URL(thumbnailURL, window.location.href);
|
|
264
|
+
const sameResourcePath =
|
|
265
|
+
source.origin === thumbnail.origin &&
|
|
266
|
+
source.pathname === thumbnail.pathname;
|
|
267
|
+
// OSS image resize URLs can return the complete animated GIF unchanged.
|
|
268
|
+
// Treat query-only GIF variants as the original resource, not a thumbnail.
|
|
269
|
+
if (/\.gif$/i.test(source.pathname) && sameResourcePath) return false;
|
|
270
|
+
} catch {
|
|
271
|
+
if (/\.gif(?:$|[?#])/i.test(page.src)) return false;
|
|
272
|
+
}
|
|
273
|
+
return resolveImageUrl(thumbnailURL) !== resolveImageUrl(page.src);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Blur thumbnail degradation (see lazy-setup design doc §15): when the app
|
|
278
|
+
* loses focus but stays cached (multi-runtime cache mode), swap the scene
|
|
279
|
+
* background `<img>` src to the low-res `PptDescription.previewURL`
|
|
280
|
+
* (`page.thumbnail`) via a pure DOM change and pause full-image preloading;
|
|
281
|
+
* restore the full image when focus comes back. A page without a real
|
|
282
|
+
* thumbnail keeps its measurable view container, but its background layer is
|
|
283
|
+
* hidden and replaced with a static pixel after TeleBox commits the blur.
|
|
284
|
+
*
|
|
285
|
+
* While degraded, every load of a background `<img>` is re-degraded
|
|
286
|
+
* immediately via a capture-phase `load` listener on the view container
|
|
287
|
+
* (`load` does not bubble but capture works). This covers remote page turns,
|
|
288
|
+
* SDK React re-renders and the SDK's own background-image onError retries —
|
|
289
|
+
* any path that (re)renders the full-size image while the app stays blurred.
|
|
290
|
+
*/
|
|
291
|
+
export const setupBlurThumbnailDegradation = (config: {
|
|
292
|
+
context: AppContext;
|
|
293
|
+
view: View;
|
|
294
|
+
pages: PresentationPage[];
|
|
295
|
+
pageIndex$: Readable<number>;
|
|
296
|
+
app: Presentation;
|
|
297
|
+
dispose: ReturnType<typeof disposableStore>;
|
|
298
|
+
}): void => {
|
|
299
|
+
const { context, view, pages, pageIndex$, app, dispose } = config;
|
|
300
|
+
const box = context.getBox();
|
|
301
|
+
const boxBlurClass = box.wrapClassName("blur");
|
|
302
|
+
// Two separate flags: `blurEpisode` covers the whole blurred period (pages
|
|
303
|
+
// without a thumbnail don't end it, so a later page degrades again), while
|
|
304
|
+
// `reDegradeEnabled` only turns off when the degraded thumbnail itself
|
|
305
|
+
// fails, so we don't fight the SDK's background onError retry.
|
|
306
|
+
let blurEpisode = false;
|
|
307
|
+
let reDegradeEnabled = false;
|
|
308
|
+
let boxBlurred = box.$box.classList.contains(boxBlurClass);
|
|
309
|
+
|
|
310
|
+
const setBackgroundSuspended = (suspended: boolean): void => {
|
|
311
|
+
app.whiteboardDOM.classList.toggle(
|
|
312
|
+
BACKGROUND_SUSPENDED_CLASS,
|
|
313
|
+
suspended,
|
|
314
|
+
);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const degrade = (): void => {
|
|
318
|
+
const page = pages[pageIndex$.value];
|
|
319
|
+
if (!page) return;
|
|
320
|
+
const thumbnailURL = page.thumbnail;
|
|
321
|
+
const img = findBackgroundImage(view);
|
|
322
|
+
if (!thumbnailURL || !hasUsableThumbnail(page)) {
|
|
323
|
+
if (boxBlurred) {
|
|
324
|
+
setBackgroundSuspended(true);
|
|
325
|
+
if (
|
|
326
|
+
img &&
|
|
327
|
+
resolveImageUrl(img.src) !== resolveImageUrl(TRANSPARENT_BACKGROUND)
|
|
328
|
+
) {
|
|
329
|
+
img.src = TRANSPARENT_BACKGROUND;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
setBackgroundSuspended(false);
|
|
335
|
+
if (img && resolveImageUrl(img.src) !== resolveImageUrl(thumbnailURL)) {
|
|
336
|
+
img.src = thumbnailURL;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const restore = (): void => {
|
|
341
|
+
const page = pages[pageIndex$.value];
|
|
342
|
+
const img = findBackgroundImage(view);
|
|
343
|
+
if (page && img && resolveImageUrl(img.src) !== resolveImageUrl(page.src)) {
|
|
344
|
+
img.src = page.src;
|
|
345
|
+
}
|
|
346
|
+
setBackgroundSuspended(false);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const suspendCachedResources = (): void => {
|
|
350
|
+
if (!blurEpisode || !boxBlurred) return;
|
|
351
|
+
app.suspendPreviewResources();
|
|
352
|
+
const page = pages[pageIndex$.value];
|
|
353
|
+
if (!page || !hasUsableThumbnail(page) || reDegradeEnabled) {
|
|
354
|
+
degrade();
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const restoreFocusedResources = (): void => {
|
|
359
|
+
if (!blurEpisode) return;
|
|
360
|
+
blurEpisode = false;
|
|
361
|
+
reDegradeEnabled = false;
|
|
362
|
+
restore();
|
|
363
|
+
app.preload.resume(pageIndex$.value);
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const isBackgroundImg = (target: EventTarget | null): boolean => {
|
|
367
|
+
const el = target as HTMLImageElement | null;
|
|
368
|
+
return Boolean(
|
|
369
|
+
el && el.tagName === "IMG" && el.matches?.('img[alt="background"]'),
|
|
370
|
+
);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// Re-degrade any background img that finishes loading a full-size image
|
|
374
|
+
// while the app is blurred. Listeners sit at document level (capture
|
|
375
|
+
// phase - load/error do not bubble but capture descends from document)
|
|
376
|
+
// and resolve the view container lazily per event, so a rebinding of
|
|
377
|
+
// view.divElement (bindContainer etc.) is followed automatically. The
|
|
378
|
+
// contains() check keeps the app's own preview-panel thumbnails untouched.
|
|
379
|
+
const ownsTarget = (target: EventTarget | null): target is HTMLImageElement => {
|
|
380
|
+
if (!isBackgroundImg(target)) return false;
|
|
381
|
+
const el = (view as any).divElement as HTMLElement | undefined | null;
|
|
382
|
+
return !!(el && el.contains(target as Node));
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const onImgLoad = (ev: Event): void => {
|
|
386
|
+
if (!blurEpisode || !reDegradeEnabled) return;
|
|
387
|
+
if (!ownsTarget(ev.target)) return;
|
|
388
|
+
degrade();
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// The degraded thumbnail failed to load: stop re-degrading for this blur
|
|
392
|
+
// episode so the SDK's onError retry can settle on the full image. Focus
|
|
393
|
+
// still force-restores afterwards.
|
|
394
|
+
const onImgError = (ev: Event): void => {
|
|
395
|
+
if (!blurEpisode || !reDegradeEnabled) return;
|
|
396
|
+
if (!ownsTarget(ev.target)) return;
|
|
397
|
+
reDegradeEnabled = false;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const offFocus = context.emitter.on("focus", (isFocused: boolean) => {
|
|
401
|
+
if (isFocused) {
|
|
402
|
+
restoreFocusedResources();
|
|
403
|
+
} else {
|
|
404
|
+
if (!isBlurDegradationAllowed(context)) return;
|
|
405
|
+
blurEpisode = true;
|
|
406
|
+
reDegradeEnabled = true;
|
|
407
|
+
degrade();
|
|
408
|
+
app.preload.pause();
|
|
409
|
+
suspendCachedResources();
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
dispose.add(offFocus);
|
|
413
|
+
|
|
414
|
+
const updateBoxBlurred = (blurred: boolean): void => {
|
|
415
|
+
if (boxBlurred === blurred) return;
|
|
416
|
+
boxBlurred = blurred;
|
|
417
|
+
if (blurred) suspendCachedResources();
|
|
418
|
+
else restoreFocusedResources();
|
|
419
|
+
};
|
|
420
|
+
const onBoxBlur = (): void => updateBoxBlurred(true);
|
|
421
|
+
const onBoxFocus = (): void => updateBoxBlurred(false);
|
|
422
|
+
box.events.on("blur", onBoxBlur);
|
|
423
|
+
box.events.on("focus", onBoxFocus);
|
|
424
|
+
dispose.add(() => {
|
|
425
|
+
box.events.off("blur", onBoxBlur);
|
|
426
|
+
box.events.off("focus", onBoxFocus);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// WindowManager may update TeleBox focus with skipUpdate=true. In that
|
|
430
|
+
// path the visual class changes but TeleBox intentionally emits no event.
|
|
431
|
+
const boxClassObserver = new MutationObserver(() => {
|
|
432
|
+
updateBoxBlurred(box.$box.classList.contains(boxBlurClass));
|
|
433
|
+
});
|
|
434
|
+
boxClassObserver.observe(box.$box, {
|
|
435
|
+
attributes: true,
|
|
436
|
+
attributeFilter: ["class"],
|
|
437
|
+
});
|
|
438
|
+
dispose.add(() => boxClassObserver.disconnect());
|
|
439
|
+
|
|
440
|
+
document.addEventListener("load", onImgLoad, true);
|
|
441
|
+
document.addEventListener("error", onImgError, true);
|
|
442
|
+
dispose.add(() => {
|
|
443
|
+
document.removeEventListener("load", onImgLoad, true);
|
|
444
|
+
document.removeEventListener("error", onImgError, true);
|
|
445
|
+
});
|
|
446
|
+
};
|
|
447
|
+
|
|
157
448
|
const createLogger = (room: Room | undefined): Logger => {
|
|
158
449
|
if (room && (room as any).logger) {
|
|
159
450
|
return (...args) => (room as any).logger.info(...args);
|
|
@@ -641,6 +932,12 @@ export const NetlessAppPresentation: NetlessApp<
|
|
|
641
932
|
app.log = log;
|
|
642
933
|
app.warn = warn;
|
|
643
934
|
|
|
935
|
+
// Setup only touches the current page's full image; the rest of the deck
|
|
936
|
+
// keeps loading through idle callbacks after the setup queue moves on.
|
|
937
|
+
if (pageIndex$.value !== 0) {
|
|
938
|
+
app.preload.touch(pageIndex$.value, true);
|
|
939
|
+
}
|
|
940
|
+
|
|
644
941
|
const getCameraDiagnosticState = (
|
|
645
942
|
reason: "initialize" | "moveCamera",
|
|
646
943
|
requestedCamera?: MoveCameraRequest,
|
|
@@ -785,6 +1082,10 @@ export const NetlessAppPresentation: NetlessApp<
|
|
|
785
1082
|
}),
|
|
786
1083
|
);
|
|
787
1084
|
|
|
1085
|
+
if (!options.disableBlurThumbnailDegradation) {
|
|
1086
|
+
setupBlurThumbnailDegradation({ context, view, pages, pageIndex$, app, dispose });
|
|
1087
|
+
}
|
|
1088
|
+
|
|
788
1089
|
const getOriginScale = () => {
|
|
789
1090
|
const page = app.page();
|
|
790
1091
|
if (!page || !isValidSize(page)) return 0;
|
|
@@ -941,13 +1242,23 @@ export const NetlessAppPresentation: NetlessApp<
|
|
|
941
1242
|
|
|
942
1243
|
let disposed = false;
|
|
943
1244
|
let offDestroy: (() => void) | undefined;
|
|
1245
|
+
// The public readonly box type hides host lifecycle mutators, while the
|
|
1246
|
+
// TeleBox runtime exposes them for app mount cleanup.
|
|
1247
|
+
const teardownRuntime = createPresentationRuntimeTeardown(
|
|
1248
|
+
box as ReadonlyTeleBox & {
|
|
1249
|
+
unmountContent(): unknown;
|
|
1250
|
+
unmountFooter(): unknown;
|
|
1251
|
+
unmountStyles(): unknown;
|
|
1252
|
+
},
|
|
1253
|
+
dispose,
|
|
1254
|
+
);
|
|
944
1255
|
const teardown = () => {
|
|
945
1256
|
if (disposed) return;
|
|
946
1257
|
disposed = true;
|
|
947
1258
|
const removeDestroy = offDestroy;
|
|
948
1259
|
offDestroy = undefined;
|
|
949
1260
|
removeDestroy?.();
|
|
950
|
-
|
|
1261
|
+
teardownRuntime();
|
|
951
1262
|
};
|
|
952
1263
|
offDestroy = context.emitter.on("destroy", teardown);
|
|
953
1264
|
teardownByContext.set(context, teardown);
|
|
@@ -1207,9 +1518,16 @@ export const NetlessAppPresentation: NetlessApp<
|
|
|
1207
1518
|
// Older WindowManager declarations model setup as synchronous even though
|
|
1208
1519
|
// AppProxy awaits its result. Keep that source compatibility until the new
|
|
1209
1520
|
// `SetupResult | Promise<SetupResult>` declaration is the minimum version.
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1521
|
+
const setupReadyTimeout = options.setupReadyTimeout ?? DEFAULT_SETUP_READY_TIMEOUT;
|
|
1522
|
+
return prepareScenesPromise.then(async () => {
|
|
1523
|
+
// Keep the serial setup queue honest: resolve only once the current
|
|
1524
|
+
// page image is actually visible (or the bounded wait gives up).
|
|
1525
|
+
const loaded = await waitForCurrentPageImage(view, setupReadyTimeout, () => disposed);
|
|
1526
|
+
if (!loaded && !disposed) {
|
|
1527
|
+
warn(`[Presentation] setup ready wait timed out after ${setupReadyTimeout}ms`);
|
|
1528
|
+
}
|
|
1529
|
+
return controller;
|
|
1530
|
+
}) as unknown as PresentationController;
|
|
1213
1531
|
},
|
|
1214
1532
|
teardown(context) {
|
|
1215
1533
|
teardownByContext.get(context)?.();
|
package/src/preload.ts
CHANGED
|
@@ -35,6 +35,9 @@ export class Preload implements IDisposable {
|
|
|
35
35
|
// 当前触摸的索引
|
|
36
36
|
touchIndex: number = 0;
|
|
37
37
|
preloadSize: number = 0;
|
|
38
|
+
// 暂停后不再创建预加载链接(app 失焦降级时调用);已插入的 link 会被移除,
|
|
39
|
+
// 挂起的 idle 回调重新进入 touch() 时也会被跳过
|
|
40
|
+
paused: boolean = false;
|
|
38
41
|
constructor(readonly pages: PresentationPage[]) {
|
|
39
42
|
this.preloadMap = new Map(pages.map((e,index) => [index, {
|
|
40
43
|
src: e.src,
|
|
@@ -43,7 +46,18 @@ export class Preload implements IDisposable {
|
|
|
43
46
|
this.preloadSize = this.preloadMap.size;
|
|
44
47
|
this.touch(0, true);
|
|
45
48
|
}
|
|
49
|
+
pause() {
|
|
50
|
+
if (this.paused) return;
|
|
51
|
+
this.paused = true;
|
|
52
|
+
this.destroySomeLink();
|
|
53
|
+
}
|
|
54
|
+
resume(index?: number) {
|
|
55
|
+
if (!this.paused) return;
|
|
56
|
+
this.paused = false;
|
|
57
|
+
this.touch(index ?? this.touchIndex, true);
|
|
58
|
+
}
|
|
46
59
|
touch(index: number, force: boolean = false) {
|
|
60
|
+
if (this.paused) return;
|
|
47
61
|
if (index >= this.preloadSize) {
|
|
48
62
|
this.touchIndex = 0;
|
|
49
63
|
} else {
|
package/src/presentation.ts
CHANGED
|
@@ -110,7 +110,7 @@ export class Presentation implements IDisposable<void> {
|
|
|
110
110
|
if (this.initialized) return
|
|
111
111
|
this.initialized = true
|
|
112
112
|
|
|
113
|
-
this.dispose.add(() => this.
|
|
113
|
+
this.dispose.add(() => this.destroyPreviewLazyload())
|
|
114
114
|
|
|
115
115
|
this.contentDOM.classList.toggle(this.c('readonly'), this.readonly)
|
|
116
116
|
this.dom.appendChild(this.contentDOM)
|
|
@@ -276,6 +276,18 @@ export class Presentation implements IDisposable<void> {
|
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
/** Release preview images while this presentation stays in the runtime cache. */
|
|
280
|
+
suspendPreviewResources() {
|
|
281
|
+
this.togglePreview(false)
|
|
282
|
+
this.destroyPreviewLazyload()
|
|
283
|
+
this.previewDOM.querySelectorAll<HTMLImageElement>('img').forEach(img => {
|
|
284
|
+
img.removeAttribute('src')
|
|
285
|
+
img.removeAttribute('srcset')
|
|
286
|
+
img.removeAttribute('sizes')
|
|
287
|
+
LazyLoad.resetStatus(img)
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
279
291
|
// [origin] means the cause:
|
|
280
292
|
// - navigation: user clicked on the left / right button on footer
|
|
281
293
|
// - input: user filled the bottom-right page number input box
|
|
@@ -322,6 +334,11 @@ export class Presentation implements IDisposable<void> {
|
|
|
322
334
|
}
|
|
323
335
|
}
|
|
324
336
|
|
|
337
|
+
private destroyPreviewLazyload() {
|
|
338
|
+
this.previewLazyload?.destroy()
|
|
339
|
+
this.previewLazyload = null
|
|
340
|
+
}
|
|
341
|
+
|
|
325
342
|
private c(className: string): string {
|
|
326
343
|
return `${this.namespace}-${className}`
|
|
327
344
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface PresentationRuntimeBox {
|
|
2
|
+
unmountContent(): unknown;
|
|
3
|
+
unmountFooter(): unknown;
|
|
4
|
+
unmountStyles(): unknown;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Build an idempotent runtime teardown while preserving the TeleBox shell. */
|
|
8
|
+
export const createPresentationRuntimeTeardown = (
|
|
9
|
+
box: PresentationRuntimeBox,
|
|
10
|
+
dispose: () => void,
|
|
11
|
+
): (() => void) => {
|
|
12
|
+
let tornDown = false;
|
|
13
|
+
return () => {
|
|
14
|
+
if (tornDown) return;
|
|
15
|
+
tornDown = true;
|
|
16
|
+
try {
|
|
17
|
+
dispose();
|
|
18
|
+
} finally {
|
|
19
|
+
box.unmountContent();
|
|
20
|
+
box.unmountFooter();
|
|
21
|
+
box.unmountStyles();
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
};
|
package/src/style.scss
CHANGED
|
@@ -12,6 +12,12 @@ $namespace: 'netless-app-presentation';
|
|
|
12
12
|
overflow: hidden;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// Keep the view container measurable while releasing an unfocused cached
|
|
16
|
+
// page's background from painting and compositing.
|
|
17
|
+
.#{$namespace}-background-suspended .background {
|
|
18
|
+
display: none !important;
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
.#{$namespace}-preview-mask {
|
|
16
22
|
display: none;
|
|
17
23
|
position: absolute;
|
|
@@ -307,4 +313,4 @@ $namespace: 'netless-app-presentation';
|
|
|
307
313
|
|
|
308
314
|
.#{$namespace}-scrollbar-y {
|
|
309
315
|
width: 6px;
|
|
310
|
-
}
|
|
316
|
+
}
|