@netless/app-slide 0.3.0-canary.11 → 0.3.0-canary.13

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/src/connect.ts ADDED
@@ -0,0 +1,116 @@
1
+ import type { AppContext, Storage, WhiteBoardView } from "@netless/window-manager";
2
+ import type { SideEffectManager } from "side-effect-manager";
3
+ import type { SlideViewer } from "./SlideViewer";
4
+ import type { Attributes } from "./typings";
5
+
6
+ import { get } from "./utils";
7
+
8
+ export interface ConnectParams {
9
+ context: AppContext;
10
+ storage: Storage<Attributes>;
11
+ viewer: SlideViewer;
12
+ sideEffect: SideEffectManager;
13
+ logger: { info: (msg: string) => void };
14
+ }
15
+
16
+ export function connect({ context, storage, viewer, sideEffect, logger }: ConnectParams) {
17
+ const log = logger.info.bind(logger);
18
+
19
+ let whiteboard: WhiteBoardView | null = null;
20
+
21
+ viewer.prepare(({ width, height, slideCount }) => {
22
+ if (context.destroyed) return;
23
+ width && context.box.setStageRatio(height / width);
24
+ whiteboard = context.createWhiteBoardView({ size: slideCount });
25
+ whiteboard.setBaseRect({ width, height });
26
+ kick_start();
27
+ });
28
+
29
+ const { slide } = viewer;
30
+
31
+ function dump_state(state: unknown = storage.state.state) {
32
+ return JSON.stringify(state);
33
+ }
34
+
35
+ function kick_start() {
36
+ if (storage.state.state) {
37
+ log("[Slide] restore " + dump_state());
38
+ slide.setSlideState(storage.state.state);
39
+ } else if (context.isAddApp) {
40
+ log("[Slide] kick start");
41
+ slide.renderSlide(1);
42
+ } else {
43
+ log("[Slide] wait for restore");
44
+ const timeoutID = sideEffect.add(() => {
45
+ const timer = setInterval(() => {
46
+ if (!storage.state.state) {
47
+ log("[Slide] timeout");
48
+ slide.renderSlide(1);
49
+ }
50
+ }, 15 * 1000);
51
+ return () => clearInterval(timer);
52
+ });
53
+ const disposerID = sideEffect.push(
54
+ storage.on("stateChanged", () => {
55
+ if (storage.state.state) {
56
+ log("[Slide] restore " + dump_state());
57
+ slide.setSlideState(storage.state.state);
58
+ sideEffect.flush(disposerID);
59
+ sideEffect.flush(timeoutID);
60
+ }
61
+ })
62
+ );
63
+ }
64
+ }
65
+
66
+ // setup sync event
67
+ slide.on("stateChange", () => {
68
+ if (context.isWritable) {
69
+ // log("[Slide] save state " + dump_state(slide.slideState));
70
+ storage.setState({ state: slide.slideState });
71
+ }
72
+ });
73
+ slide.on("syncDispatch", payload => {
74
+ if (context.isWritable) {
75
+ log("[Slide] dispatch " + dump_state(get(payload, "uuid")));
76
+ context.dispatchMagixEvent("syncDispatch", payload);
77
+ }
78
+ });
79
+ sideEffect.push(
80
+ context.addMagixEventListener(
81
+ "syncDispatch",
82
+ ({ payload }) => {
83
+ log("[Slide] receive " + dump_state(get(payload, "uuid")));
84
+ slide.emit("syncReceive", payload);
85
+ },
86
+ { fireSelfEventAfterCommit: true }
87
+ )
88
+ );
89
+
90
+ // save state
91
+ slide.on("slideChange", page => {
92
+ if (context.isWritable && whiteboard) {
93
+ log("[Slide] page to " + page);
94
+ whiteboard.jumpPage(page - 1);
95
+ }
96
+ });
97
+
98
+ // handle ui events
99
+ viewer.setReadonly(!context.isWritable);
100
+ sideEffect.push(
101
+ context.emitter.on("writableChange", w => {
102
+ viewer.setReadonly(!w);
103
+ })
104
+ );
105
+
106
+ sideEffect.addEventListener(window, "keydown", ev => {
107
+ if (context.box.focus) {
108
+ if (ev.key === "ArrowUp" || ev.key === "ArrowLeft") {
109
+ viewer.slide.prevStep();
110
+ }
111
+ if (ev.key === "ArrowRight" || ev.key === "ArrowDown") {
112
+ viewer.slide.nextStep();
113
+ }
114
+ }
115
+ });
116
+ }
package/src/constants.ts CHANGED
@@ -1,3 +1,3 @@
1
- export const namespace = "netless-app-slide";
1
+ export const class_name = "netless-app-slide";
2
2
 
3
3
  export const DefaultUrl = "https://convertcdn.netless.link/dynamicConvert";
@@ -0,0 +1,5 @@
1
+ export { sidebarSVG } from "./sidebar";
2
+ export { arrowLeftSVG } from "./arrow-left";
3
+ export { playSVG } from "./play";
4
+ export { pauseSVG } from "./pause";
5
+ export { arrowRightSVG } from "./arrow-right";
package/src/index.ts CHANGED
@@ -1,17 +1,21 @@
1
- import type { AppContext, NetlessApp, Player, WhiteBoardView } from "@netless/window-manager";
2
- import type { AddHooks, AppOptions, Attributes, MagixEvents, SlideState } from "./typings";
1
+ import type { NetlessApp } from "@netless/window-manager";
3
2
  import type { SlideViewerOptions } from "./SlideViewer";
3
+ import type { AddHooks, AppOptions, Attributes, MagixEvents, SlideState } from "./typings";
4
4
 
5
5
  export type { SlideState, AddHooks, AppOptions, Attributes, MagixEvents, SlideViewerOptions };
6
6
 
7
- import ColorString from "color-string";
8
- import { SideEffectManager } from "side-effect-manager";
9
7
  import { Slide } from "@netless/slide";
10
- import { SlideViewer } from "./SlideViewer";
11
- import { refrigerator, addHooks } from "./Refrigerator";
8
+ import { SideEffectManager } from "side-effect-manager";
9
+
12
10
  import { DefaultUrl } from "./constants";
11
+ import { addHooks, refrigerator } from "./Refrigerator";
12
+ import { SlideViewer } from "./SlideViewer";
13
+ import { Logger } from "./Logger";
14
+ import { connect } from "./connect";
15
+ import { previewSlide } from "./preview";
16
+ import { make_bg_color, make_timestamp } from "./utils";
13
17
 
14
- export { Slide, SlideViewer, refrigerator, addHooks, DefaultUrl };
18
+ export { Slide, SlideViewer, refrigerator, addHooks, DefaultUrl, previewSlide };
15
19
 
16
20
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
21
  export const usePlugin = /* @__PURE__ */ Slide.usePlugin.bind(Slide) as (plugin: any) => void;
@@ -21,16 +25,19 @@ export const version = __APP_VERSION__;
21
25
  const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, void> = {
22
26
  kind: "Slide",
23
27
  setup(context) {
24
- console.log("[Slide] setup @ " + __APP_VERSION__);
28
+ console.log("[Slide] setup @ " + __APP_VERSION__ + " (" + context.appId + ")");
25
29
 
26
- if (!context.storage.state.taskId) {
30
+ const logger = new Logger(context);
31
+ logger.info("[Slide] setup @ " + __APP_VERSION__);
32
+
33
+ if (!context.attributes.taskId) {
27
34
  throw new Error("[Slide] no taskId");
28
35
  }
29
36
 
37
+ const storage = context.createStorage("slide", context.attributes);
38
+
30
39
  const appOptions = context.getAppOptions() || {};
31
- const options = appOptions.renderOptions || {};
32
- const logger = make_logger(context, appOptions.debug);
33
- logger.info("[Slide] setup @ " + __APP_VERSION__);
40
+ const renderOptions = appOptions.renderOptions || {};
34
41
 
35
42
  const sideEffect = new SideEffectManager();
36
43
 
@@ -38,18 +45,17 @@ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, void> = {
38
45
  const viewer = new SlideViewer({
39
46
  interactive: true,
40
47
  mode: "interactive",
41
- resize: true,
42
48
  enableGlobalClick: true,
43
49
  timestamp: make_timestamp(context),
44
50
  logger: appOptions.logger || logger,
45
51
  renderOptions: {
46
- minFPS: options.minFPS || 25,
47
- maxFPS: options.maxFPS || 30,
48
- autoFPS: options.autoFPS ?? true,
49
- autoResolution: options.autoResolution ?? true,
50
- resolution: options.resolution,
51
- maxResolutionLevel: options.maxResolutionLevel,
52
- transactionBgColor: options.transactionBgColor || make_bg_color(context.box.$content),
52
+ minFPS: renderOptions.minFPS || 25,
53
+ maxFPS: renderOptions.maxFPS || 30,
54
+ autoFPS: renderOptions.autoFPS ?? true,
55
+ autoResolution: renderOptions.autoResolution ?? true,
56
+ resolution: renderOptions.resolution,
57
+ maxResolutionLevel: renderOptions.maxResolutionLevel,
58
+ transactionBgColor: renderOptions.transactionBgColor || make_bg_color(context.box.$content),
53
59
  },
54
60
  rtcAudio: appOptions.rtcAudio,
55
61
  useLocalCache: appOptions.useLocalCache,
@@ -57,33 +63,47 @@ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, void> = {
57
63
  loaderDelegate: appOptions.loaderDelegate,
58
64
  navigatorDelegate: appOptions.navigatorDelegate,
59
65
  fixedFrameSize: appOptions.fixedFrameSize,
60
- taskId: context.storage.state.taskId,
61
- url: context.storage.state.url,
66
+ taskId: storage.state.taskId,
67
+ url: storage.state.url,
62
68
  });
63
69
 
64
70
  // For debugging.
65
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
- (viewer as any).context = context;
71
+ Object.assign(viewer, { context, logger });
67
72
 
68
- sideEffect.addDisposer(refrigerator.set(context.appId, viewer));
73
+ sideEffect.push(refrigerator.set(context.appId, viewer));
69
74
 
70
75
  if (import.meta.env.DEV) {
71
76
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
77
  (window as any).slideViewer = viewer;
73
78
  }
74
79
 
80
+ // Log more
81
+ sideEffect.push(viewer.events.on("freeze", () => logger.info("[Slide] freeze")));
82
+ sideEffect.push(viewer.events.on("unfreeze", () => logger.info("[Slide] unfreeze")));
83
+ sideEffect.push(
84
+ viewer.events.on("prepareError", error =>
85
+ logger.info("[Slide] fetch slide info failed: " + error.message)
86
+ )
87
+ );
88
+ sideEffect.push(
89
+ viewer.events.on("renderError", ({ error, index }) =>
90
+ logger.info("[Slide] render failed " + `[${index}]: ${error.message}`)
91
+ )
92
+ );
93
+
75
94
  // Mount UI
76
95
  context.box.mountStyles(SlideViewer.styles);
77
96
  context.box.mountStage(viewer.$slide);
78
97
  context.box.mountFooter(viewer.$footer);
79
98
  context.box.mountContent(viewer.$content);
80
- sideEffect.addDisposer(() => viewer.destroy());
99
+ sideEffect.push(() => viewer.destroy());
81
100
 
82
101
  // Connect UI with Netless App
83
- connect({ context, viewer, sideEffect, logger });
102
+ connect({ context, storage, viewer, sideEffect, logger });
84
103
 
85
- onUnmount(context, () => {
104
+ context.emitter.on("destroy", () => {
86
105
  logger.info("[Slide] destroy");
106
+ logger.destroy();
87
107
  sideEffect.flushAll();
88
108
  });
89
109
 
@@ -92,186 +112,3 @@ const SlideApp: NetlessApp<Attributes, MagixEvents, AppOptions, void> = {
92
112
  };
93
113
 
94
114
  export default SlideApp;
95
-
96
- export function previewSlide(options: SlideViewerOptions & { container: HTMLElement }) {
97
- const { container, ...slideOptions } = options;
98
- const wrapper = document.createElement("div");
99
- wrapper.className = "netless-app-slide-preview-wrapper";
100
- const viewer = new SlideViewer(slideOptions);
101
- wrapper.appendChild(document.createElement("style")).textContent = SlideViewer.styles;
102
- wrapper.appendChild(viewer.$content);
103
- wrapper.appendChild(viewer.$footer);
104
- container.appendChild(wrapper);
105
- viewer.prepare(() => viewer.slide.renderSlide(1));
106
- return viewer;
107
- }
108
-
109
- if (import.meta.env.DEV) {
110
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
111
- (window as any).previewSlide = previewSlide;
112
- }
113
-
114
- function make_timestamp(context: AppContext): () => number {
115
- const room = context.room;
116
- const player = context.displayer as Player;
117
- if (room) {
118
- return () => room.calibrationTimestamp;
119
- } else if (player) {
120
- return () => player.beginTimestamp + player.progressTime;
121
- } else {
122
- return () => Date.now();
123
- }
124
- }
125
-
126
- function make_logger(context: AppContext, debug?: boolean): { info: (msg: string) => void } {
127
- void debug; // not used for now
128
- if (import.meta.env.DEV) {
129
- return { info: console.debug.bind(console) };
130
- }
131
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
132
- return (context.displayer as any).logger;
133
- }
134
-
135
- function make_bg_color(el: HTMLElement): string {
136
- try {
137
- let bg = window.getComputedStyle(el).backgroundColor;
138
- if (bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
139
- bg = hex(bg);
140
- console.log("[Slide] guess bg color:", bg);
141
- return bg;
142
- }
143
- if (el.parentElement) {
144
- return make_bg_color(el.parentElement);
145
- }
146
- } catch {
147
- // ignored
148
- }
149
- return "#ffffff";
150
- }
151
-
152
- // https://github.com/Qix-/color-convert/blob/8dfdbbc6b46fa6a305bf394d942cc1b08e23fca5/conversions.js#L616
153
- function hex(color: string): string {
154
- const result = ColorString.get(color);
155
- if (result && result.model === "rgb") {
156
- const args = result.value;
157
-
158
- const integer =
159
- ((Math.round(args[0]) & 0xff) << 16) +
160
- ((Math.round(args[1]) & 0xff) << 8) +
161
- (Math.round(args[2]) & 0xff);
162
-
163
- const string = integer.toString(16);
164
- return "#" + "000000".substring(string.length) + string;
165
- } else {
166
- return color;
167
- }
168
- }
169
-
170
- function onUnmount(context: AppContext, callback: () => void) {
171
- context.emitter.on("destroy", callback);
172
- }
173
-
174
- interface ConnectParams {
175
- context: AppContext;
176
- viewer: SlideViewer;
177
- sideEffect: SideEffectManager;
178
- logger: { info: (msg: string) => void };
179
- }
180
-
181
- function connect({ context, viewer, sideEffect, logger }: ConnectParams) {
182
- const log = import.meta.env.DEV ? console.log.bind(console) : logger.info.bind(logger);
183
-
184
- let whiteboard: WhiteBoardView | null = null;
185
-
186
- viewer.prepare(({ width, height, slideCount }) => {
187
- if (context.destroyed) return;
188
- width && context.box.setStageRatio(height / width);
189
- whiteboard = context.createWhiteBoardView({ size: slideCount });
190
- whiteboard.setBaseRect({ width, height });
191
- kick_start();
192
- });
193
-
194
- const { slide } = viewer;
195
-
196
- function kick_start() {
197
- if (context.storage.state.state) {
198
- log("[Slide] restore", { ...context.storage.state.state });
199
- slide.setSlideState(context.storage.state.state);
200
- } else if (context.isAddApp) {
201
- log("[Slide] kick start");
202
- slide.renderSlide(1);
203
- } else {
204
- log("[Slide] wait for restore");
205
- const timeoutID = sideEffect.add(() => {
206
- const timer = setInterval(() => {
207
- if (!context.storage.state.state) {
208
- log("[Slide] timeout");
209
- slide.renderSlide(1);
210
- }
211
- }, 15 * 1000);
212
- return () => clearInterval(timer);
213
- });
214
- const disposerID = sideEffect.addDisposer(
215
- context.storage.addStateChangedListener(() => {
216
- if (context.storage.state.state) {
217
- log("[Slide] restore", { ...context.storage.state.state });
218
- slide.setSlideState(context.storage.state.state);
219
- sideEffect.flush(disposerID);
220
- sideEffect.flush(timeoutID);
221
- }
222
- })
223
- );
224
- }
225
- }
226
-
227
- // setup sync event
228
- slide.on("stateChange", () => {
229
- if (context.isWritable) {
230
- log("[Slide] save state", { ...slide.slideState });
231
- context.storage.setState({ state: slide.slideState });
232
- }
233
- });
234
- slide.on("syncDispatch", payload => {
235
- if (context.isWritable) {
236
- log("[Slide] dispatch", { ...payload });
237
- context.dispatchMagixEvent("syncDispatch", payload);
238
- }
239
- });
240
- sideEffect.addDisposer(
241
- context.addMagixEventListener(
242
- "syncDispatch",
243
- ({ payload }) => {
244
- log("[Slide] receive", { ...payload });
245
- slide.emit("syncReceive", payload);
246
- },
247
- { fireSelfEventAfterCommit: true }
248
- )
249
- );
250
-
251
- // save state
252
- slide.on("slideChange", page => {
253
- if (context.isWritable && whiteboard) {
254
- log("[Slide] page to", page);
255
- whiteboard.jumpPage(page - 1);
256
- }
257
- });
258
-
259
- // handle ui events
260
- viewer.setReadonly(!context.isWritable);
261
- sideEffect.addDisposer(
262
- context.emitter.on("writableChange", w => {
263
- viewer.setReadonly(!w);
264
- })
265
- );
266
-
267
- sideEffect.addEventListener(window, "keydown", ev => {
268
- if (context.box.focus) {
269
- if (ev.key === "ArrowUp" || ev.key === "ArrowLeft") {
270
- viewer.slide.prevStep();
271
- }
272
- if (ev.key === "ArrowRight" || ev.key === "ArrowDown") {
273
- viewer.slide.nextStep();
274
- }
275
- }
276
- });
277
- }
package/src/preview.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { SlideViewerOptions } from "./SlideViewer";
2
+
3
+ import { SlideViewer } from "./SlideViewer";
4
+
5
+ export function previewSlide(options: SlideViewerOptions & { container: HTMLElement }) {
6
+ const { container, ...slideOptions } = options;
7
+ const wrapper = document.createElement("div");
8
+ wrapper.className = "netless-app-slide-preview-wrapper";
9
+ const viewer = new SlideViewer(slideOptions);
10
+ wrapper.appendChild(document.createElement("style")).textContent = SlideViewer.styles;
11
+ wrapper.appendChild(viewer.$content);
12
+ wrapper.appendChild(viewer.$footer);
13
+ container.appendChild(wrapper);
14
+ viewer.prepare(() => viewer.slide.renderSlide(1));
15
+ return viewer;
16
+ }
17
+
18
+ if (import.meta.env.DEV) {
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ (window as any).previewSlide = previewSlide;
21
+ }
package/src/typings.ts CHANGED
@@ -29,9 +29,7 @@ export interface AppOptions
29
29
  | "navigatorDelegate"
30
30
  | "fixedFrameSize"
31
31
  | "logger"
32
- > {
33
- debug?: boolean;
34
- }
32
+ > {}
35
33
 
36
34
  export interface ILogger {
37
35
  info?(msg: string): void;
package/src/utils.ts CHANGED
@@ -1,3 +1,93 @@
1
+ import type { AppContext, Player } from "@netless/window-manager";
2
+
3
+ import ColorString from "color-string";
4
+ import { class_name } from "./constants";
5
+
1
6
  export function noop() {
2
7
  return;
3
8
  }
9
+
10
+ export function h<K extends keyof HTMLElementTagNameMap>(tag: K) {
11
+ return document.createElement(tag);
12
+ }
13
+
14
+ export function hc<K extends keyof HTMLElementTagNameMap>(tag: K, name: string) {
15
+ const el = h(tag);
16
+ set_class(el, name);
17
+ return el;
18
+ }
19
+
20
+ export function append(parent: HTMLElement, child: HTMLElement) {
21
+ return parent.appendChild(child);
22
+ }
23
+
24
+ export function wrap_class(name: string) {
25
+ return `${class_name}-${name}`;
26
+ }
27
+
28
+ export function set_class(el: HTMLElement, name: string) {
29
+ el.className = wrap_class(name);
30
+ }
31
+
32
+ export function trim_slash(s: string) {
33
+ return s.endsWith("/") ? s.slice(0, -1) : s;
34
+ }
35
+
36
+ export function get(o: unknown, k: string): unknown {
37
+ if (typeof o === "object" && o !== null) return (o as Record<string, unknown>)[k];
38
+ }
39
+
40
+ export function block<T = void>(): [p: Promise<T>, resolve: (t: T) => void] {
41
+ let resolve!: (t: T) => void;
42
+ const p = new Promise<T>(r => {
43
+ resolve = r;
44
+ });
45
+ return [p, resolve];
46
+ }
47
+
48
+ export function make_timestamp(context: AppContext): () => number {
49
+ const room = context.room;
50
+ const player = context.displayer as Player;
51
+ if (room) {
52
+ return () => room.calibrationTimestamp;
53
+ } else if (player) {
54
+ return () => player.beginTimestamp + player.progressTime;
55
+ } else {
56
+ return () => Date.now();
57
+ }
58
+ }
59
+
60
+ export function make_bg_color(el: HTMLElement): string {
61
+ try {
62
+ let bg = window.getComputedStyle(el).backgroundColor;
63
+ if (bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") {
64
+ bg = hex(bg);
65
+ console.log("[Slide] guess bg color:", bg);
66
+ return bg;
67
+ }
68
+ if (el.parentElement) {
69
+ return make_bg_color(el.parentElement);
70
+ }
71
+ } catch {
72
+ // ignored
73
+ }
74
+ return "#ffffff";
75
+ }
76
+
77
+ // https://github.com/Qix-/color-convert/blob/8dfdbbc6b46fa6a305bf394d942cc1b08e23fca5/conversions.js#L616
78
+ export function hex(color: string): string {
79
+ const result = ColorString.get(color);
80
+ if (result && result.model === "rgb") {
81
+ const args = result.value;
82
+
83
+ const integer =
84
+ ((Math.round(args[0]) & 0xff) << 16) +
85
+ ((Math.round(args[1]) & 0xff) << 8) +
86
+ (Math.round(args[2]) & 0xff);
87
+
88
+ const string = integer.toString(16);
89
+ return "#" + "000000".substring(string.length) + string;
90
+ } else {
91
+ return color;
92
+ }
93
+ }