@netless/app-slide 0.3.0-canary.2 → 0.3.0-canary.21

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.
Files changed (47) hide show
  1. package/README-zh.md +17 -7
  2. package/README.md +4 -7
  3. package/dist/Logger.d.ts +12 -0
  4. package/dist/Refrigerator.d.ts +3 -1
  5. package/dist/SlideViewer/footer.d.ts +12 -0
  6. package/dist/{SlideViewer.d.ts → SlideViewer/index.d.ts} +33 -20
  7. package/dist/SlideViewer/sidebar.d.ts +12 -0
  8. package/dist/SlideViewer/typings.d.ts +18 -0
  9. package/dist/SlideViewer/utils.d.ts +7 -0
  10. package/dist/connect.d.ts +14 -0
  11. package/dist/constants.d.ts +1 -1
  12. package/dist/icons/index.d.ts +5 -0
  13. package/dist/icons/save.d.ts +1 -0
  14. package/dist/icons/spinner.d.ts +1 -0
  15. package/dist/index.d.ts +7 -6
  16. package/dist/main.js +3 -0
  17. package/dist/main.js.map +1 -0
  18. package/dist/main.mjs +1406 -0
  19. package/dist/main.mjs.map +1 -0
  20. package/dist/preview.d.ts +5 -0
  21. package/dist/typings.d.ts +9 -18
  22. package/dist/utils.d.ts +12 -0
  23. package/package.json +12 -8
  24. package/src/Logger.ts +38 -0
  25. package/src/Refrigerator.ts +11 -1
  26. package/src/SlideViewer/footer.ts +169 -0
  27. package/src/SlideViewer/index.ts +329 -0
  28. package/src/SlideViewer/sidebar.ts +140 -0
  29. package/src/SlideViewer/typings.ts +21 -0
  30. package/src/SlideViewer/utils.ts +37 -0
  31. package/src/connect.ts +152 -0
  32. package/src/constants.ts +1 -1
  33. package/src/icons/index.ts +5 -0
  34. package/src/icons/save.ts +18 -0
  35. package/src/icons/spinner.ts +37 -0
  36. package/src/index.ts +64 -192
  37. package/src/preview.ts +21 -0
  38. package/src/style.scss +67 -16
  39. package/src/typings.ts +18 -16
  40. package/src/utils.ts +90 -0
  41. package/dist/main.cjs.js +0 -6363
  42. package/dist/main.cjs.js.map +0 -1
  43. package/dist/main.es.js +0 -38214
  44. package/dist/main.es.js.map +0 -1
  45. package/dist/main.iife.js +0 -6363
  46. package/dist/main.iife.js.map +0 -1
  47. package/src/SlideViewer.ts +0 -557
@@ -0,0 +1,169 @@
1
+ import { class_name } from "../constants";
2
+ import { arrowLeftSVG, arrowRightSVG, pauseSVG, playSVG, sidebarSVG } from "../icons";
3
+ import { hc, noop, wrap_class } from "../utils";
4
+ import { saveSVG } from "../icons/save";
5
+ import { spinnerSVG } from "../icons/spinner";
6
+
7
+ export function create_footer() {
8
+ type NewPageIndexCallback = (newPageIndex: number) => void;
9
+ let new_page_index_callback: NewPageIndexCallback = noop;
10
+ const on_new_page_index = (callback: NewPageIndexCallback) => {
11
+ new_page_index_callback = callback;
12
+ };
13
+
14
+ let toggle_preview_callback: () => void = noop;
15
+ const on_toggle_preview = (callback: () => void) => {
16
+ toggle_preview_callback = callback;
17
+ };
18
+
19
+ let play_callback: () => void = noop;
20
+ const on_play = (callback: () => void) => {
21
+ play_callback = callback;
22
+ };
23
+
24
+ let readonly = false;
25
+ let sidebar = false;
26
+ let page_index = 0;
27
+ let page_count = 0;
28
+
29
+ const $footer = hc("div", "footer");
30
+
31
+ const $btnSidebar = hc("button", "btn-sidebar");
32
+ $btnSidebar.classList.add(wrap_class("footer-btn"));
33
+ $btnSidebar.appendChild(sidebarSVG(class_name));
34
+ $btnSidebar.onclick = function on_click_btn_sidebar() {
35
+ if (readonly) return;
36
+ toggle_preview_callback();
37
+ };
38
+ $btnSidebar.style.display = "none";
39
+ $footer.appendChild($btnSidebar);
40
+
41
+ const $pageJumps = hc("div", "page-jumps");
42
+
43
+ const $btnPageBack = hc("button", "btn-page-back");
44
+ $btnPageBack.classList.add(wrap_class("footer-btn"));
45
+ $btnPageBack.appendChild(arrowLeftSVG(class_name));
46
+ $btnPageBack.onclick = function on_click_btn_page_back() {
47
+ if (readonly) return;
48
+ new_page_index_callback(page_index - 1);
49
+ };
50
+ $pageJumps.appendChild($btnPageBack);
51
+
52
+ const $btnPlay = hc("button", "btn-page-play");
53
+ $btnPlay.classList.add(wrap_class("footer-btn"));
54
+ $btnPlay.appendChild(playSVG(class_name));
55
+ $btnPlay.appendChild(pauseSVG(class_name));
56
+ let returnPlayTimer = 0;
57
+ $btnPlay.onclick = function on_click_btn_play() {
58
+ if (readonly) return;
59
+ $btnPlay.classList.add(wrap_class("footer-btn-playing"));
60
+ play_callback();
61
+ clearTimeout(returnPlayTimer);
62
+ returnPlayTimer = window.setTimeout(() => {
63
+ $btnPlay.classList.remove(wrap_class("footer-btn-playing"));
64
+ }, 500);
65
+ };
66
+ $pageJumps.appendChild($btnPlay);
67
+
68
+ const $btnPageNext = hc("button", "btn-page-next");
69
+ $btnPageNext.classList.add(wrap_class("footer-btn"));
70
+ $btnPageNext.appendChild(arrowRightSVG(class_name));
71
+ $btnPageNext.onclick = function on_click_btn_page_next() {
72
+ if (readonly) return;
73
+ new_page_index_callback(page_index + 1);
74
+ };
75
+ $pageJumps.appendChild($btnPageNext);
76
+
77
+ const $pageNumber = hc("div", "page-number");
78
+
79
+ const $pageNumberInput = hc("input", "page-number-input");
80
+ $pageNumberInput.value = String(page_index + 1);
81
+ $pageNumberInput.onchange = function on_change_page_number_input() {
82
+ if (readonly) return;
83
+ if ($pageNumberInput.value) {
84
+ new_page_index_callback(Number($pageNumberInput.value) - 1);
85
+ }
86
+ };
87
+
88
+ const $totalPage = hc("span", "total-page");
89
+ $totalPage.textContent = " / ";
90
+
91
+ $pageNumber.appendChild($pageNumberInput);
92
+ $pageNumber.appendChild($totalPage);
93
+
94
+ $footer.appendChild($pageJumps);
95
+ $footer.appendChild($pageNumber);
96
+
97
+ const class_readonly = wrap_class("readonly");
98
+
99
+ let saveToPdf: (progressCallback: (p: number) => void) => void = noop;
100
+ const on_save_to_pdf = (callback: (progressCallback: (p: number) => void) => void) => {
101
+ saveToPdf = callback;
102
+ };
103
+ const progressCallback = (p: number) => {
104
+ if (p < 99) {
105
+ spinnerIcon.style.display = "block";
106
+ saveIcon.style.display = "none";
107
+ const progress = spinnerIcon.querySelector("[data-id]");
108
+ if (progress) {
109
+ progress.textContent = p.toString().padStart(2, "0");
110
+ }
111
+ } else {
112
+ spinnerIcon.style.display = "none";
113
+ saveIcon.style.display = "block";
114
+ }
115
+ };
116
+ const saveIcon = saveSVG(class_name);
117
+ const spinnerIcon = spinnerSVG(class_name);
118
+ spinnerIcon.style.display = "none";
119
+ const $saveBtn = hc("button", "footer-btn");
120
+ $saveBtn.appendChild(saveIcon);
121
+ $saveBtn.onclick = function () {
122
+ saveToPdf(progressCallback);
123
+ };
124
+ $saveBtn.appendChild(spinnerIcon);
125
+
126
+ // $footer.appendChild($saveBtn);
127
+
128
+ function set_readonly(readonly_: boolean) {
129
+ if (readonly !== readonly_) {
130
+ readonly = readonly_;
131
+ $footer.classList.toggle(class_readonly, readonly);
132
+ $pageNumberInput.disabled = readonly;
133
+ }
134
+ }
135
+
136
+ function set_sidebar(sidebar_: boolean) {
137
+ if (sidebar !== sidebar_) {
138
+ sidebar = sidebar_;
139
+ $btnSidebar.style.display = sidebar ? "" : "none";
140
+ }
141
+ }
142
+
143
+ function set_page_index(page_index_: number, force = false) {
144
+ if (force || page_index !== page_index_) {
145
+ page_index = page_index_;
146
+ $pageNumberInput.value = String(page_index + 1);
147
+ }
148
+ }
149
+
150
+ function set_page_count(page_count_: number) {
151
+ if (page_count !== page_count_) {
152
+ page_count = page_count_;
153
+ $totalPage.textContent = ` / ${page_count}`;
154
+ }
155
+ }
156
+
157
+ return {
158
+ $footer,
159
+ $pageNumberInput,
160
+ set_readonly,
161
+ set_sidebar,
162
+ set_page_index,
163
+ set_page_count,
164
+ on_new_page_index,
165
+ on_toggle_preview,
166
+ on_play,
167
+ on_save_to_pdf,
168
+ };
169
+ }
@@ -0,0 +1,329 @@
1
+ import type { ISlideConfig } from "@netless/slide";
2
+ import type { SlideAttributes } from "./typings";
3
+
4
+ import { Slide } from "@netless/slide";
5
+ import { SideEffectManager } from "side-effect-manager";
6
+ import { Remitter } from "remitter";
7
+
8
+ import { DefaultUrl } from "../constants";
9
+ import { append, block, hc } from "../utils";
10
+ import { fetch_slide_info, make_prefix, on_visibility_change } from "./utils";
11
+
12
+ import styles from "../style.scss?inline";
13
+ import { create_footer } from "./footer";
14
+ import { create_sidebar } from "./sidebar";
15
+
16
+ export type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
17
+
18
+ export type SlideViewerOptions = MakeOptional<
19
+ Omit<ISlideConfig & SlideAttributes, "anchor">,
20
+ "mode" | "interactive"
21
+ >;
22
+
23
+ export interface SlideViewerInfoResponse {
24
+ width: number;
25
+ height: number;
26
+ slideCount: number;
27
+ }
28
+
29
+ export interface SlideViewerEventData {
30
+ freeze: void;
31
+ unfreeze: void;
32
+ renderError: { error: Error; index: number };
33
+ prepareError: Error;
34
+ renderStart: void;
35
+ renderEnd: void;
36
+ }
37
+
38
+ export class SlideViewer {
39
+ static styles = styles;
40
+ static fetchSlideInfo = fetch_slide_info;
41
+
42
+ readonly sideEffect = new SideEffectManager();
43
+ readonly events = new Remitter<SlideViewerEventData>();
44
+
45
+ readonly $slide = hc("div", "slide");
46
+ readonly $overlay = hc("div", "overlay");
47
+ readonly sidebar = create_sidebar();
48
+ readonly footer = create_footer();
49
+ readonly $content: HTMLElement;
50
+ readonly $footer: HTMLElement;
51
+
52
+ readonly slide: Slide;
53
+
54
+ _slideTitle = "";
55
+ _getWhiteSnapshot:
56
+ | ((index: number, canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) => void)
57
+ | null = null;
58
+
59
+ private readonly _readyPromise: Promise<void>;
60
+ private readonly _infoPromise: Promise<SlideViewerInfoResponse>;
61
+ private _ready = false;
62
+ private _slideCount = 0;
63
+ private _destroyed = false;
64
+ private _isFrozen = false;
65
+ private _cachedIsFrozen = false;
66
+
67
+ constructor(options: SlideViewerOptions) {
68
+ // user interface
69
+ this.$content = this.sidebar.$content;
70
+ this.$footer = this.footer.$footer;
71
+ append(this.$content, this.$slide);
72
+ append(this.$content, this.$overlay);
73
+
74
+ // bind user actions
75
+ this.sidebar.on_new_page_index(this.onNewPageIndex);
76
+ this.footer.on_new_page_index(this.onNewPageIndex);
77
+ this.footer.on_toggle_preview(this.onTogglePreview);
78
+ this.footer.on_play(this.onPlay);
79
+ this.footer.on_save_to_pdf(this.toPdf);
80
+
81
+ // remove dom on cleanup
82
+ this.sideEffect.push(this.sidebar.destroy);
83
+ this.sideEffect.push(() => this.slide.destroy());
84
+ this.sideEffect.push(() => {
85
+ this.$content.parentElement && this.$content.remove();
86
+ this.$footer.parentElement && this.$footer.remove();
87
+ this.$slide.parentElement && this.$slide.remove();
88
+ });
89
+
90
+ // auto freeze on lost focus
91
+ this.sideEffect.push(
92
+ on_visibility_change(visible => {
93
+ if (this._destroyed) return;
94
+ if (!visible) {
95
+ this._cachedIsFrozen = this._isFrozen;
96
+ this.freeze();
97
+ } else {
98
+ if (!this._cachedIsFrozen) this.unfreeze();
99
+ }
100
+ })
101
+ );
102
+
103
+ // start loading slide
104
+ // prepare(info) -> slide.renderEnd(ready)
105
+ const [infoPromise, resolveInfo] = block<SlideViewerInfoResponse>();
106
+ this._infoPromise = infoPromise;
107
+
108
+ const fetching = SlideViewer.fetchSlideInfo(options).then(({ preview, slide }) => {
109
+ if (this._destroyed) return;
110
+
111
+ this._slideCount = slide.slideCount;
112
+
113
+ if (preview) {
114
+ const { taskId, url } = options;
115
+ const { slideCount } = slide;
116
+ const { width, height } = preview;
117
+
118
+ const prefix = make_prefix(taskId, url);
119
+ const pages = Array.from({ length: slideCount }, (_, i) => ({
120
+ width,
121
+ height,
122
+ thumbnail: `${prefix}/preview/${i + 1}.png`,
123
+ }));
124
+
125
+ this.sidebar.set_pages(pages);
126
+ this.footer.set_sidebar(true);
127
+ }
128
+
129
+ this.footer.set_page_count(slide.slideCount);
130
+ resolveInfo(slide);
131
+ });
132
+
133
+ fetching.catch(error => {
134
+ console.log("[Slide] fetch slide info error");
135
+ console.error(error);
136
+ this.events.emit("prepareError", error);
137
+ });
138
+
139
+ const [readyPromise, resolveReady] = block();
140
+ this._readyPromise = readyPromise;
141
+
142
+ this.slide = new Slide({
143
+ anchor: this.$slide,
144
+ mode: "local",
145
+ interactive: true,
146
+ enableGlobalClick: true,
147
+ useLocalCache: true,
148
+ ...options,
149
+ });
150
+ this.slide.setResource(options.taskId, options.url || DefaultUrl);
151
+
152
+ this.slide.on("renderStart", () => {
153
+ if (this._destroyed) return;
154
+ this.events.emit("renderStart");
155
+ });
156
+
157
+ this.slide.on("slideChange", page => {
158
+ if (this._destroyed) return;
159
+ this.setPage(page);
160
+ this.$overlay.style.opacity = "";
161
+
162
+ // renderStart -> renderEnd -> slideChange
163
+ // So we just treat `slideChange` as `renderEnd`
164
+ this.events.emit("renderEnd");
165
+ });
166
+
167
+ // TODO: export `once()` in the slide package.
168
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
169
+ (this.slide as any).once("renderEnd", () => {
170
+ // ready = first render end
171
+ if (!this._ready) {
172
+ setTimeout(() => {
173
+ this._ready = true;
174
+ resolveReady();
175
+ }, 1000);
176
+ }
177
+ });
178
+
179
+ // show overlay on render error
180
+ this.slide.on("renderError", ev => {
181
+ if (this._destroyed) return;
182
+ if (ev.error) {
183
+ console.error(ev.error);
184
+ this.$overlay.textContent = `Error on slide.index=${ev.index}: ${ev.error.message}`;
185
+ this.$overlay.style.opacity = "1";
186
+ this.events.emit("renderError", ev);
187
+ }
188
+ });
189
+
190
+ this.ready(() => {
191
+ if (this._isFrozen) {
192
+ this.freeze();
193
+ }
194
+ });
195
+ }
196
+
197
+ prepare(callback: (response: SlideViewerInfoResponse) => void) {
198
+ return this._infoPromise.then(callback);
199
+ }
200
+
201
+ ready(callback: () => void) {
202
+ return this._readyPromise.then(callback);
203
+ }
204
+
205
+ destroy() {
206
+ this._destroyed = true;
207
+ this.sideEffect.flushAll();
208
+ }
209
+
210
+ onPlay = () => {
211
+ this.slide.nextStep();
212
+ };
213
+
214
+ onNewPageIndex = (index: number) => {
215
+ if (this._ready && index >= 0 && index < this._slideCount) {
216
+ this.slide.renderSlide(index + 1);
217
+ } else {
218
+ this.setPage(this.slide.slideState.currentSlideIndex);
219
+ }
220
+ };
221
+
222
+ onTogglePreview = () => {
223
+ this.sidebar.set_active(!this.sidebar.get_active());
224
+ };
225
+
226
+ setReadonly(readonly: boolean) {
227
+ this.slide.setInteractive(!readonly);
228
+ this.sidebar.set_readonly(readonly);
229
+ this.footer.set_readonly(readonly);
230
+ }
231
+
232
+ setPage(page: number) {
233
+ this.sidebar.set_page_index(page - 1, true);
234
+ this.footer.set_page_index(page - 1, true);
235
+ }
236
+
237
+ freeze() {
238
+ if (this._destroyed) return;
239
+ this._isFrozen = true;
240
+ if (!this._ready) return;
241
+ this.slide.frozen();
242
+ this.events.emit("freeze");
243
+ }
244
+
245
+ unfreeze() {
246
+ if (this._destroyed) return;
247
+ this._isFrozen = false;
248
+ if (!this._ready) return;
249
+ this.slide.release();
250
+ this.events.emit("unfreeze");
251
+ }
252
+
253
+ toPdf = async (progressCallback: (progress: number) => void) => {
254
+ const MAX = 1920;
255
+ const resizeCanvas = document.createElement("canvas");
256
+ const resizeCtx = resizeCanvas.getContext("2d");
257
+ const { slideCount, width, height } = this.slide;
258
+ let pdfWidth = Math.floor(width);
259
+ let pdfHeight = Math.floor(height);
260
+ if (pdfWidth > MAX) {
261
+ pdfWidth = MAX;
262
+ pdfHeight = Math.floor((height * pdfWidth) / width);
263
+ }
264
+ if (pdfHeight > MAX) {
265
+ pdfHeight = MAX;
266
+ pdfWidth = Math.floor((width * pdfHeight) / height);
267
+ }
268
+ resizeCanvas.width = pdfWidth;
269
+ resizeCanvas.height = pdfHeight;
270
+
271
+ const whiteSnapshotCanvas = document.createElement("canvas");
272
+ whiteSnapshotCanvas.width = width;
273
+ whiteSnapshotCanvas.height = height;
274
+ const whiteCtx = whiteSnapshotCanvas.getContext("2d");
275
+ if (!whiteCtx || !this._getWhiteSnapshot || !resizeCtx) {
276
+ return null;
277
+ }
278
+
279
+ const orientation = pdfWidth > pdfHeight ? "l" : "p";
280
+ const { jsPDF } = await import("jspdf");
281
+
282
+ const pdf = new jsPDF({
283
+ format: [pdfWidth, pdfHeight],
284
+ orientation,
285
+ compress: true,
286
+ });
287
+
288
+ for (let i = 1; i <= slideCount; i++) {
289
+ const slideSnapshot = await this.slide.snapshotWithTimingEnd(i);
290
+
291
+ if (slideSnapshot) {
292
+ const img = document.createElement("img");
293
+ img.src = slideSnapshot;
294
+ await new Promise(resolve => (img.onload = resolve));
295
+ resizeCtx.drawImage(img, 0, 0, pdfWidth, pdfHeight);
296
+ }
297
+ whiteCtx.clearRect(0, 0, width, height);
298
+ this._getWhiteSnapshot(i, whiteSnapshotCanvas, whiteCtx);
299
+ try {
300
+ const whiteSnapshot = whiteSnapshotCanvas.toDataURL("image/png");
301
+ const whiteImg = document.createElement("img");
302
+ whiteImg.src = whiteSnapshot;
303
+ await new Promise(resolve => (whiteImg.onload = resolve));
304
+ resizeCtx.drawImage(whiteImg, 0, 0, pdfWidth, pdfHeight);
305
+ } catch (e) {
306
+ // ignore
307
+ }
308
+
309
+ const outputDataUrl = resizeCanvas.toDataURL("image/jpeg", 0.6);
310
+ if (i > 1) {
311
+ pdf.addPage();
312
+ }
313
+ pdf.addImage(outputDataUrl, "JPEG", 0, 0, pdfWidth, pdfHeight, "", "FAST");
314
+ resizeCtx.clearRect(0, 0, pdfWidth, pdfHeight);
315
+ progressCallback(Math.ceil((i / slideCount) * 100));
316
+ }
317
+ const dataUrl = pdf.output("arraybuffer");
318
+ const blob = new Blob([dataUrl]);
319
+ const downloadUrl = URL.createObjectURL(blob);
320
+ const element = document.createElement("a");
321
+ element.setAttribute("href", downloadUrl);
322
+ element.setAttribute("download", `${this._slideTitle || "snapshot"}.pdf`);
323
+ element.style.display = "none";
324
+ document.body.appendChild(element);
325
+ element.click();
326
+ document.body.removeChild(element);
327
+ window.postMessage({ type: "@app-slide/_download_pdf_", buf: dataUrl });
328
+ };
329
+ }
@@ -0,0 +1,140 @@
1
+ import type { ILazyLoadInstance } from "vanilla-lazyload";
2
+ import type { PreviewPage } from "./typings";
3
+
4
+ import LazyLoad from "vanilla-lazyload";
5
+ import { append, h, hc, noop, wrap_class } from "../utils";
6
+
7
+ export function create_sidebar() {
8
+ type NewPageIndexCallback = (newPageIndex: number) => void;
9
+ let new_page_index_callback: NewPageIndexCallback = noop;
10
+ const on_new_page_index = (callback: NewPageIndexCallback) => {
11
+ new_page_index_callback = callback;
12
+ };
13
+
14
+ let destroyed = false;
15
+ let readonly = false;
16
+ let active = false;
17
+ let pages: PreviewPage[] = [];
18
+ let page_index = 0;
19
+
20
+ const class_readonly = wrap_class("readonly");
21
+ const class_preview_active = wrap_class("preview-active");
22
+
23
+ const $content = hc("div", "content");
24
+
25
+ const $previewMask = hc("div", "preview-mask");
26
+ $previewMask.onclick = function on_click_preview_mask(ev) {
27
+ if (readonly) return;
28
+ if (ev.target !== $previewMask) return;
29
+ set_active(false);
30
+ };
31
+ append($content, $previewMask);
32
+
33
+ const $preview = hc("div", "preview");
34
+ $preview.classList.add(wrap_class("tele-fancy-scrollbar"));
35
+ $preview.onclick = function on_click_preview(ev) {
36
+ if (readonly) return;
37
+ const page_index = (ev.target as HTMLElement).dataset?.pageIndex;
38
+ if (page_index) {
39
+ ev.preventDefault();
40
+ ev.stopPropagation();
41
+ ev.stopImmediatePropagation();
42
+ new_page_index_callback(Number(page_index));
43
+ set_active(false);
44
+ }
45
+ };
46
+ append($content, $preview);
47
+
48
+ let lazyload: ILazyLoadInstance | undefined;
49
+
50
+ function set_readonly(readonly_: boolean) {
51
+ if (readonly !== readonly_) {
52
+ readonly = readonly_;
53
+ $content.classList.toggle(class_readonly, readonly);
54
+ }
55
+ }
56
+
57
+ function set_active(active_: boolean) {
58
+ if (active !== active_) {
59
+ active = active_;
60
+ $content.classList.toggle(class_preview_active, active);
61
+ if (active && !lazyload) {
62
+ lazyload = new LazyLoad({
63
+ container: $preview,
64
+ elements_selector: `.${wrap_class("preview-page>img")}`,
65
+ });
66
+ }
67
+ }
68
+ }
69
+
70
+ function set_pages(pages_: PreviewPage[]) {
71
+ if (pages !== pages_) {
72
+ pages = pages_;
73
+
74
+ const page_class = wrap_class("preview-page");
75
+ const page_name_class = wrap_class("preview-page-name");
76
+
77
+ pages.forEach((page, i) => {
78
+ const pageIndex = String(i);
79
+
80
+ const $page = h("a");
81
+ $page.className = page_class + " " + wrap_class(`preview-page-${i}`);
82
+ $page.href = "#";
83
+ $page.dataset.pageIndex = pageIndex;
84
+
85
+ const $name = h("span");
86
+ $name.className = page_name_class;
87
+ $name.textContent = String(i + 1);
88
+ $name.dataset.pageIndex = pageIndex;
89
+
90
+ const $img = h("img");
91
+ $img.width = page.width;
92
+ $img.height = page.height;
93
+ $img.alt = `page-${i}`;
94
+ $img.dataset.src = page.thumbnail;
95
+ $img.dataset.pageIndex = pageIndex;
96
+
97
+ append($page, $img);
98
+ append($page, $name);
99
+ append($preview, $page);
100
+ });
101
+
102
+ if (!destroyed) {
103
+ lazyload?.update();
104
+ }
105
+ }
106
+ }
107
+
108
+ function set_page_index(page_index_: number, force = false) {
109
+ if (force || page_index !== page_index_) {
110
+ page_index = page_index_;
111
+ const $page = $preview.querySelector<HTMLElement>(
112
+ `.${wrap_class(`preview-page-${page_index}`)}`
113
+ );
114
+ if ($page) {
115
+ $preview.scrollTo({ top: $page.offsetTop - 16 });
116
+ }
117
+ }
118
+ }
119
+
120
+ function destroy() {
121
+ destroyed = true;
122
+ lazyload?.destroy();
123
+ }
124
+
125
+ function get_active() {
126
+ return active;
127
+ }
128
+
129
+ return {
130
+ $content,
131
+ $preview,
132
+ set_readonly,
133
+ set_active,
134
+ get_active,
135
+ set_pages,
136
+ on_new_page_index,
137
+ set_page_index,
138
+ destroy,
139
+ };
140
+ }
@@ -0,0 +1,21 @@
1
+ export interface SlideAttributes {
2
+ taskId: string;
3
+ url?: string;
4
+ }
5
+
6
+ export interface PreviewPage {
7
+ width: number;
8
+ height: number;
9
+ thumbnail: string;
10
+ }
11
+
12
+ export interface PreviewInfo {
13
+ width: number;
14
+ height: number;
15
+ }
16
+
17
+ export interface SlideInfo {
18
+ width: number;
19
+ height: number;
20
+ slideCount: number;
21
+ }
@@ -0,0 +1,37 @@
1
+ import type { PreviewInfo, SlideAttributes, SlideInfo } from "./typings";
2
+
3
+ import { DefaultUrl } from "../constants";
4
+ import { noop, trim_slash } from "../utils";
5
+
6
+ export function make_prefix(taskId: string, url?: string) {
7
+ return `${trim_slash(url || DefaultUrl)}/${taskId}`;
8
+ }
9
+
10
+ export async function fetch_slide_info({ taskId, url }: SlideAttributes): Promise<{
11
+ preview: PreviewInfo | null;
12
+ slide: SlideInfo;
13
+ }> {
14
+ const prefix = make_prefix(taskId, url);
15
+ const imgSrc = `${prefix}/preview/1.png`;
16
+ const jsonUrl = `${prefix}/jsonOutput/slide-1.json`;
17
+
18
+ const p1 = new Promise<PreviewInfo | null>(resolve => {
19
+ const image = new Image();
20
+ image.onload = () => resolve(image);
21
+ image.onerror = () => resolve(null);
22
+ image.src = imgSrc;
23
+ });
24
+
25
+ const p2: Promise<SlideInfo> = fetch(jsonUrl).then(r => r.json());
26
+
27
+ return { preview: await p1, slide: await p2 };
28
+ }
29
+
30
+ export function on_visibility_change(callback: (visible: boolean) => void) {
31
+ if (typeof document !== "undefined") {
32
+ const listener = () => callback(document.visibilityState === "visible");
33
+ document.addEventListener("visibilitychange", listener);
34
+ return () => document.removeEventListener("visibilitychange", listener);
35
+ }
36
+ return noop;
37
+ }