@netless/app-slide 0.0.17 → 0.0.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.
package/src/index.ts CHANGED
@@ -1,181 +1,90 @@
1
- import type { Event, Player, RoomState } from "white-web-sdk";
2
- import type { Slide, SyncEvent } from "@netless/slide";
3
1
  import type { NetlessApp } from "@netless/window-manager";
4
- import type { SlideController } from "./utils/slide";
2
+ import type { RoomState } from "white-web-sdk";
3
+ import type { MountSlideOptions } from "./SlideDocsViewer";
4
+ import type { Attributes } from "./typings";
5
5
 
6
- import { SLIDE_EVENTS } from "@netless/slide";
7
- import { ensureAttributes } from "@netless/app-shared";
8
6
  import { SideEffectManager } from "side-effect-manager";
7
+ import { ensureAttributes } from "@netless/app-shared";
8
+ import {
9
+ DefaultUrl,
10
+ EmptyAttributes,
11
+ syncSceneWithSlide,
12
+ SlideController,
13
+ } from "./SlideController";
9
14
  import { SlideDocsViewer } from "./SlideDocsViewer";
10
- import { createSlideController, syncSceneWithSlide } from "./utils/slide";
11
- import { isObj } from "./utils/helpers";
12
15
  import styles from "./style.scss?inline";
13
16
 
14
- export type SlideState = Slide["slideState"];
17
+ export type { Attributes };
15
18
 
16
- export interface Attributes {
17
- /** convert task id */
18
- taskId: string;
19
- /** base url of converted resources */
20
- url: string;
21
- /** internal state of slide, do not change */
22
- state: SlideState | null;
23
- }
19
+ export { DefaultUrl };
24
20
 
25
21
  const SlideApp: NetlessApp<Attributes> = {
26
22
  kind: "Slide",
27
- async setup(context) {
28
- const box = context.getBox();
29
- const view = context.getView();
30
- const room = context.getRoom();
31
- const displayer = context.getDisplayer();
32
- const attrs = ensureAttributes(context, {
33
- taskId: "",
34
- url: "",
35
- state: null,
36
- });
23
+ setup(context) {
24
+ if (context.getIsWritable()) {
25
+ ensureAttributes(context, EmptyAttributes);
26
+ }
37
27
 
38
- if (!attrs.taskId) {
39
- throw new Error(`[Slide] no taskId`);
28
+ const attributes = context.getAttributes();
29
+ if (!attributes?.taskId) {
30
+ throw new Error("[Slide] no taskId");
40
31
  }
41
32
 
33
+ const view = context.getView();
42
34
  if (!view) {
43
- throw new Error(`[Slide] no view, please set scenePath on addApp()`);
35
+ throw new Error("[Slide] no view, please set scenePath on addApp()");
44
36
  }
37
+ view.disableCameraTransform = true;
45
38
 
46
- const debug = import.meta.env.DEV || context.getAppOptions()?.debug;
47
- const fixElectron = !!context.getAppOptions()?.fixElectron;
48
- const resolution = context.getAppOptions()?.resolution;
49
-
39
+ const box = context.getBox();
50
40
  box.mountStyles(styles);
51
41
 
52
- const sideEffect = new SideEffectManager();
53
- // 因为 view 存在,init scene path 必定存在
42
+ // must exist because of view
54
43
  const baseScenePath = context.getInitScenePath() as string;
55
- const channel = `channel-${context.appId}`;
56
44
 
57
- let theController: SlideController | undefined;
58
-
59
- const timestamp = () => {
60
- if (room) {
61
- return room.calibrationTimestamp;
62
- } else {
63
- return (displayer as Player).beginTimestamp + (displayer as Player).progressTime;
64
- }
65
- };
45
+ let docsViewer: SlideDocsViewer | null = null;
66
46
 
67
47
  const onPageChanged = (page: number) => {
68
- console.log("[Slide] page to", page);
69
- if (context.getIsWritable() && room && theController) {
70
- syncSceneWithSlide(room, context, theController.slide, baseScenePath);
48
+ const room = context.getRoom();
49
+ if (docsViewer && docsViewer.slideController && room && context.getIsWritable()) {
50
+ syncSceneWithSlide(room, context, docsViewer.slideController.slide, baseScenePath);
71
51
  docsViewer.viewer.setPageIndex(page - 1);
72
52
  }
73
53
  };
74
54
 
75
- const onTransitionStart = () => {
76
- docsViewer.viewer.setPlaying();
77
- };
78
-
79
- const onTransitionEnd = () => {
80
- docsViewer.viewer.setPaused();
81
- };
82
-
83
- const onDispatchSyncEvent = (event: SyncEvent) => {
84
- if (debug) {
85
- console.log("[Slide] dispatch", event);
86
- }
87
- if (context.getIsWritable() && theController && room) {
88
- room.dispatchMagixEvent(channel, { type: SLIDE_EVENTS.syncDispatch, payload: event });
89
- }
90
- };
91
-
92
- const onStateChange = (slideState: SlideState) => {
93
- if (context.getIsWritable()) {
94
- context.updateAttributes(["state"], slideState);
95
- }
96
- };
97
-
98
- sideEffect.add(() => {
99
- let first = !context.isAddApp;
100
- const magixEventListener = async (ev: Event) => {
101
- if (debug) {
102
- console.log(
103
- "[Slide] receive",
104
- ev.payload.payload,
105
- "(",
106
- !!theController,
107
- ev.event === channel,
108
- ev.payload.type === SLIDE_EVENTS.syncDispatch,
109
- ")"
110
- );
111
- }
112
- if (theController && ev.event === channel && isObj(ev.payload)) {
113
- const { type, payload } = ev.payload;
114
- if (type === SLIDE_EVENTS.syncDispatch) {
115
- if (first && attrs.state) {
116
- first = false;
117
- theController.setState(attrs.state);
118
- }
119
- theController.receiveSyncEvent(payload);
120
- }
121
- }
122
- };
123
- displayer.addMagixEventListener(channel, magixEventListener, {
124
- fireSelfEventAfterCommit: true,
125
- });
126
- return () => displayer.removeMagixEventListener(channel, magixEventListener);
127
- });
128
-
129
- const mountSlideController = async (anchor: HTMLDivElement) => {
130
- theController = createSlideController(
131
- anchor,
132
- attrs.taskId,
133
- attrs.url || "https://convertcdn.netless.link/dynamicConvert",
134
- debug,
135
- JSON.parse(JSON.stringify(attrs.state)),
136
- displayer.state.sceneState.index + 1,
55
+ const mountSlideController = (options: MountSlideOptions): SlideController => {
56
+ const slideController = new SlideController({
57
+ context,
58
+ ...options,
137
59
  onPageChanged,
138
- onTransitionStart,
139
- onTransitionEnd,
140
- onDispatchSyncEvent,
141
- onStateChange,
142
- timestamp,
143
- fixElectron,
144
- resolution
145
- );
146
- theController.initialize();
147
- await theController.ready;
148
- return theController;
60
+ });
61
+ if (import.meta.env.DEV) {
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ (window as any).slideController = slideController;
64
+ }
65
+ slideController.readyPromise.then(options.onReady);
66
+ return slideController;
149
67
  };
150
68
 
151
- const docsViewer = new SlideDocsViewer({
69
+ docsViewer = new SlideDocsViewer({
152
70
  box,
153
71
  view,
154
72
  mountSlideController,
155
73
  mountWhiteboard: dom => context.mountView(dom),
156
74
  });
157
75
 
158
- try {
159
- await docsViewer.mount();
160
- view.disableCameraTransform = true;
161
- } catch (err) {
162
- console.warn(err);
163
- console.log("[Slide]: destroy by error");
164
- sideEffect.flushAll();
165
- docsViewer.destroy();
166
- return;
167
- }
168
-
169
76
  if (import.meta.env.DEV) {
170
77
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
171
78
  (window as any).slideDoc = docsViewer;
172
79
  }
173
80
 
81
+ const room = context.getRoom();
82
+ const sideEffect = new SideEffectManager();
174
83
  if (room) {
175
84
  docsViewer.toggleClickThrough(room.state.memberState.currentApplianceName);
176
85
  sideEffect.add(() => {
177
86
  const onRoomStateChanged = (e: Partial<RoomState>) => {
178
- if (e.memberState) {
87
+ if (e.memberState && docsViewer) {
179
88
  docsViewer.toggleClickThrough(e.memberState.currentApplianceName);
180
89
  }
181
90
  };
@@ -184,15 +93,16 @@ const SlideApp: NetlessApp<Attributes> = {
184
93
  });
185
94
  }
186
95
 
187
- context.emitter.on("writableChange", () => {
188
- docsViewer.setReadonly(!context.getIsWritable());
189
- });
190
-
191
96
  context.emitter.on("destroy", () => {
192
97
  console.log("[Slide]: destroy");
193
98
  sideEffect.flushAll();
194
- docsViewer.destroy();
99
+ if (docsViewer) {
100
+ docsViewer.destroy();
101
+ docsViewer = null;
102
+ }
195
103
  });
104
+
105
+ docsViewer.mount();
196
106
  },
197
107
  };
198
108
 
package/src/typings.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { Slide, SLIDE_EVENTS, SyncEvent } from "@netless/slide";
2
+
3
+ export type SlideState = Slide["slideState"];
4
+
5
+ export interface Attributes {
6
+ /** convert task id */
7
+ taskId: string;
8
+ /** base url of converted resources */
9
+ url: string;
10
+ /** internal state of slide, do not change */
11
+ state: SlideState | null;
12
+ }
13
+
14
+ export type MagixPayload = {
15
+ type: typeof SLIDE_EVENTS.syncDispatch;
16
+ payload: SyncEvent;
17
+ };
@@ -20,3 +20,7 @@ export function isObj(obj: unknown) {
20
20
  export function wait(ms: number) {
21
21
  return new Promise<void>(resolve => setTimeout(resolve, ms));
22
22
  }
23
+
24
+ export function deepClone<T>(obj: T): T {
25
+ return JSON.parse(JSON.stringify(obj));
26
+ }
@@ -1,47 +0,0 @@
1
- import type { SyncEvent } from "@netless/slide";
2
- import type { Room } from "white-web-sdk";
3
- import type { AppContext } from "@netless/window-manager";
4
- import type { DocsViewerPage } from "../DocsViewer";
5
- import type { Attributes, SlideState } from "../index";
6
- import { Slide } from "@netless/slide";
7
- /**
8
- * 因为 slide 的 `renderSlide()` 比较耗时,而切 scene 很快,这里让 scene 跟随 slide 的状态。
9
- *
10
- * 同时,UI 也应该和 slide 对齐。考虑到快速点击下一页的情况:slide 无法立刻切到下一页。我们应该记住当前的
11
- * "目标页",slide 的 `renderEnd` 没触发前不再继续调用 `renderSlide()`。
12
- */
13
- export declare function syncSceneWithSlide(room: Room, context: AppContext<Attributes>, slide: Slide, baseScenePath: string): void;
14
- export declare function createDocsViewerPages(slide: Slide): DocsViewerPage[];
15
- /**
16
- * slide 依赖事件回溯 (收到广播事件) 来进行同步进度,每次 `renderSlide()` 等操作都不会实际触发更改,
17
- * 而是发出一条广播事件,等收到事件后,使用 `slide.emit(SLIDE_EVENTS.syncReceive)` 来实际触发。
18
- *
19
- * controller 要保证每个事件都正确反馈给 slide。
20
- */
21
- export declare class SlideController {
22
- readonly slide: Slide;
23
- readonly onPageChanged: (page: number) => void;
24
- readonly onTransitionStart: () => void;
25
- readonly onTransitionEnd: () => void;
26
- readonly onDispatchSyncEvent: (event: SyncEvent) => void;
27
- readonly onStateChange: (state: SlideState) => void;
28
- private readonly initialState;
29
- private readonly initialPage;
30
- constructor(slide: Slide, onPageChanged: (page: number) => void, onTransitionStart: () => void, onTransitionEnd: () => void, onDispatchSyncEvent: (event: SyncEvent) => void, onStateChange: (state: SlideState) => void, initialState: SlideState | null, initialPage: number);
31
- private resolveReady;
32
- private reject;
33
- readonly ready: Promise<this>;
34
- private onRenderError;
35
- initialize(): void;
36
- private pollCount;
37
- private pollReadyState;
38
- receiveSyncEvent(event: SyncEvent): void;
39
- setState(state: SlideState): void;
40
- isReady(): boolean;
41
- private targetingPage;
42
- private resetTargetingPage;
43
- jumpToPage(page: number): void;
44
- destroy(): void;
45
- }
46
- export declare function createSlideController(anchor: HTMLDivElement, taskId: string, url: string, controller: boolean, initialState: Slide["slideState"] | null, initialPage: number, onPageChanged: (page: number) => void, onTransitionStart: () => void, onTransitionEnd: () => void, onDispatchSyncEvent: (event: SyncEvent) => void, onStateChange: (state: Slide["slideState"]) => void, timestamp: () => number, _fixElectron: boolean, // eslint-disable-line @typescript-eslint/no-unused-vars
47
- resolution?: number): SlideController;
package/playground.ts DELETED
@@ -1,31 +0,0 @@
1
- import type { PlaygroundConfigs } from "../playground/typings";
2
- import type { Attributes } from "./src";
3
-
4
- const options: PlaygroundConfigs<Attributes> = [
5
- {
6
- kind: "Slide",
7
- src: () => import("./src"),
8
- options: {
9
- title: "ByeBye GoodBye",
10
- scenePath: `/Slide/5f36bc1041c811ecba2783d2bae94ad0`,
11
- },
12
- attributes: {
13
- taskId: "5f36bc1041c811ecba2783d2bae94ad0",
14
- url: "https://convertcdn.netless.group/dynamicConvert",
15
- },
16
- },
17
- {
18
- kind: "Slide",
19
- src: () => import("./src"),
20
- options: {
21
- title: "大凉山区",
22
- scenePath: `/Slide/8887363041c811ecba2783d2bae94ad0`,
23
- },
24
- attributes: {
25
- taskId: "8887363041c811ecba2783d2bae94ad0",
26
- url: "https://convertcdn.netless.group/dynamicConvert",
27
- },
28
- },
29
- ];
30
-
31
- export default options;
@@ -1,203 +0,0 @@
1
- import type { SyncEvent } from "@netless/slide";
2
- import type { Room, ScenePathType } from "white-web-sdk";
3
- import type { AppContext } from "@netless/window-manager";
4
- import type { DocsViewerPage } from "../DocsViewer";
5
- import type { Attributes, SlideState } from "../index";
6
-
7
- import { SLIDE_EVENTS, Slide } from "@netless/slide";
8
- import { clamp } from "./helpers";
9
-
10
- /**
11
- * 因为 slide 的 `renderSlide()` 比较耗时,而切 scene 很快,这里让 scene 跟随 slide 的状态。
12
- *
13
- * 同时,UI 也应该和 slide 对齐。考虑到快速点击下一页的情况:slide 无法立刻切到下一页。我们应该记住当前的
14
- * "目标页",slide 的 `renderEnd` 没触发前不再继续调用 `renderSlide()`。
15
- */
16
- export function syncSceneWithSlide(
17
- room: Room,
18
- context: AppContext<Attributes>,
19
- slide: Slide,
20
- baseScenePath: string
21
- ) {
22
- const page = slide.slideState.currentSlideIndex;
23
- // 如果不存在 currentSlideIndex,可能没初始化完,此时什么都不做
24
- if (page == null) return;
25
- // 如果 room 不可写,什么也不做
26
- if (!context.getIsWritable()) return;
27
-
28
- const scenePath = [baseScenePath, page].join("/");
29
- // 这里用 === none 而不是 !== page,是因为这个接口有 bug,最新的 sdk 才修掉
30
- if (room.scenePathType(scenePath) === ("none" as ScenePathType.None)) {
31
- // 如果不存在对应 scene,重新创建一遍
32
- room.removeScenes(baseScenePath);
33
- const count = slide.slideCount;
34
- const scenes: { name: string }[] = [];
35
- for (let i = 1; i <= count; ++i) scenes.push({ name: `${i}` });
36
- room.putScenes(baseScenePath, scenes);
37
- }
38
-
39
- context.setScenePath(scenePath);
40
- }
41
-
42
- export function createDocsViewerPages(slide: Slide): DocsViewerPage[] {
43
- const { width, height, slideCount, slideState } = slide;
44
- const { taskId, url } = slideState;
45
- const pages: DocsViewerPage[] = [];
46
- for (let i = 1; i <= slideCount; ++i) {
47
- pages.push({ width, height, thumbnail: `${url}/${taskId}/preview/${i}.png`, src: "ppt" });
48
- }
49
- return pages;
50
- }
51
-
52
- /**
53
- * slide 依赖事件回溯 (收到广播事件) 来进行同步进度,每次 `renderSlide()` 等操作都不会实际触发更改,
54
- * 而是发出一条广播事件,等收到事件后,使用 `slide.emit(SLIDE_EVENTS.syncReceive)` 来实际触发。
55
- *
56
- * controller 要保证每个事件都正确反馈给 slide。
57
- */
58
- export class SlideController {
59
- constructor(
60
- public readonly slide: Slide,
61
- readonly onPageChanged: (page: number) => void,
62
- readonly onTransitionStart: () => void,
63
- readonly onTransitionEnd: () => void,
64
- readonly onDispatchSyncEvent: (event: SyncEvent) => void,
65
- readonly onStateChange: (state: SlideState) => void,
66
- private readonly initialState: SlideState | null,
67
- private readonly initialPage: number
68
- ) {
69
- slide.on(SLIDE_EVENTS.slideChange, this.resetTargetingPage);
70
- slide.on(SLIDE_EVENTS.slideChange, onPageChanged);
71
-
72
- slide.on(SLIDE_EVENTS.renderStart, onTransitionStart);
73
- slide.on(SLIDE_EVENTS.renderEnd, onTransitionEnd);
74
- // slide.on(SLIDE_EVENTS.animateStart, onTransitionStart);
75
- // slide.on(SLIDE_EVENTS.animateEnd, onTransitionEnd);
76
- slide.on(SLIDE_EVENTS.mainSeqStepStart, onTransitionStart);
77
- slide.on(SLIDE_EVENTS.mainSeqStepEnd, onTransitionEnd);
78
-
79
- slide.on(SLIDE_EVENTS.syncDispatch, onDispatchSyncEvent);
80
-
81
- slide.on(SLIDE_EVENTS.renderError, this.onRenderError);
82
- slide.on(SLIDE_EVENTS.renderError, onTransitionEnd);
83
-
84
- slide.on(SLIDE_EVENTS.stateChange, onStateChange);
85
-
86
- this.pollReadyState();
87
- }
88
-
89
- private resolveReady!: (slideController: this) => void;
90
- private reject!: (error: Error) => void;
91
-
92
- readonly ready = new Promise<this>((resolve, reject) => {
93
- this.resolveReady = resolve;
94
- this.reject = reject;
95
- });
96
-
97
- private onRenderError = ({ error }: { error: Error }) => {
98
- console.warn("[Slide] render error", error);
99
- };
100
-
101
- initialize() {
102
- if (this.initialState) {
103
- console.log("[Slide] init with state", this.initialState);
104
- this.slide.setSlideState(this.initialState);
105
- } else {
106
- this.slide.renderSlide(this.initialPage);
107
- }
108
- }
109
-
110
- private pollCount = 0;
111
-
112
- private pollReadyState = () => {
113
- if (this.isReady()) {
114
- this.resolveReady(this);
115
- } else if (this.pollCount < 60) {
116
- this.pollCount++;
117
- setTimeout(this.pollReadyState, 500);
118
- } else {
119
- this.reject(new Error("slide init time out (30s)"));
120
- }
121
- };
122
-
123
- receiveSyncEvent(event: SyncEvent) {
124
- this.slide.emit(SLIDE_EVENTS.syncReceive, event);
125
- }
126
-
127
- setState(state: SlideState) {
128
- this.slide.setSlideState(state);
129
- }
130
-
131
- isReady() {
132
- return this.slide.slideCount > 0;
133
- }
134
-
135
- private targetingPage = -1;
136
-
137
- private resetTargetingPage = () => {
138
- this.targetingPage = -1;
139
- };
140
-
141
- jumpToPage(page: number) {
142
- if (!this.isReady()) return;
143
- page = clamp(page, 1, this.slide.slideCount);
144
- if (page === this.targetingPage) return;
145
- this.targetingPage = page;
146
- this.slide.renderSlide(page);
147
- }
148
-
149
- destroy() {
150
- this.slide.destroy();
151
- }
152
- }
153
-
154
- export function createSlideController(
155
- anchor: HTMLDivElement,
156
- taskId: string,
157
- url: string,
158
- controller: boolean,
159
- initialState: Slide["slideState"] | null,
160
- initialPage: number,
161
- onPageChanged: (page: number) => void,
162
- onTransitionStart: () => void,
163
- onTransitionEnd: () => void,
164
- onDispatchSyncEvent: (event: SyncEvent) => void,
165
- onStateChange: (state: Slide["slideState"]) => void,
166
- timestamp: () => number,
167
- _fixElectron: boolean, // eslint-disable-line @typescript-eslint/no-unused-vars
168
- resolution?: number
169
- ) {
170
- const slide = new Slide({
171
- anchor,
172
- interactive: true,
173
- mode: "interactive",
174
- resize: true,
175
- controller,
176
- renderOptions: {
177
- minFPS: 25,
178
- maxFPS: 30,
179
- autoFPS: true,
180
- autoResolution: true,
181
- resolution,
182
- },
183
- timestamp,
184
- });
185
-
186
- if (import.meta.env.DEV) {
187
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
188
- (window as any).slide = slide;
189
- }
190
-
191
- slide.setResource(taskId, url);
192
-
193
- return new SlideController(
194
- slide,
195
- onPageChanged,
196
- onTransitionStart,
197
- onTransitionEnd,
198
- onDispatchSyncEvent,
199
- onStateChange,
200
- initialState,
201
- initialPage
202
- );
203
- }
package/tsconfig.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "include": ["src"],
4
- "compilerOptions": {
5
- "noEmit": false
6
- }
7
- }
package/vite.config.ts DELETED
@@ -1,3 +0,0 @@
1
- import { createViteConfig } from "../../scripts/create-vite-config";
2
-
3
- export default createViteConfig();