@netless/fastboard 0.0.5 → 0.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netless/fastboard",
3
- "version": "0.0.5",
3
+ "version": "0.0.9",
4
4
  "description": "An open sourced sdk based on white-web-sdk.",
5
5
  "main": "./dist/index.cjs.js",
6
6
  "module": "./dist/index.es.js",
@@ -22,6 +22,14 @@
22
22
  "default": "./dist/vue.es.js",
23
23
  "types": "./src/vue.ts"
24
24
  },
25
+ "./svelte": {
26
+ "node": {
27
+ "import": "./dist/svelte.es.js",
28
+ "require": "./dist/svelte.cjs.js"
29
+ },
30
+ "default": "./dist/svelte.es.js",
31
+ "types": "./src/svelte.ts"
32
+ },
25
33
  "./package.json": "./package.json"
26
34
  },
27
35
  "files": [
@@ -83,5 +91,5 @@
83
91
  "tippy.js": "^6.3.7",
84
92
  "vue-demi": "^0.12.1"
85
93
  },
86
- "readme": "# @netless/fastboard\n\nA whiteboard starter, based on [white-web-sdk](https://www.npmjs.com/package/white-web-sdk), [@netless/window-manager](https://www.npmjs.com/package/@netless/window-manager)\nand [netless-app](https://github.com/netless-io/netless-app).\n\n## 目录\n\n- [安装](#install)\n- [使用](#usage)\n- [进阶](./docs/advanced.md)\n- [开发](#开发)\n\n<h2 id=\"install\">安装</h2>\n\n```bash\nnpm add @netless/fastboard @netless/window-manager white-web-sdk react react-dom\n```\n\n<h2 id=\"usage\">使用</h2>\n\n<h3 id=\"mount-whiteboard\">挂载白板</h3>\n\n> 原生 `javascript`\n\n```js\nimport { createWhiteboardApp } from \"@netless/fastboard\";\n\nlet whiteboard = await createWhiteboardApp({\n target: document.getElementById(\"whiteboard\"),\n // [1]\n sdkConfig: {\n appIdentifier: \"whiteboard-appid\",\n },\n // [2]\n joinRoom: {\n uid: \"unique_id_for_each_client\",\n uuid: \"room-uuid\",\n roomToken: \"NETLESSROOM_...\",\n },\n // [3]\n managerConfig: {\n cursor: true,\n },\n});\n```\n\n> 使用 `React`\n\n```typescript\nimport { useFastboard, FastBoardConfig } from \"@netless/fastboard\";\nimport ReactDOM from \"react-dom\";\n\nconst config: FastBoardConfig = {\n sdkConfig: {\n appIdentifier: \"whiteboard-appid\",\n },\n joinRoom: {\n uid: \"unique_id_for_each_client\",\n uuid: \"room-uuid\",\n roomToken: \"NETLESSROOM_...\",\n },\n};\n\nfunction App() {\n const [app, ref] = useFastboard(config);\n return <div ref={ref} />;\n}\nReactDOM.render(<App />, document.getElementById(\"root\"));\n```\n\n[1] 关于 SDK 更多配置请看 [构造 WhiteWebSDK](https://developer.netless.link/javascript-zh/home/construct-white-web-sdk)\n\n[2] 加入房间更多配置请看 [构造 Room 与 Player 对象](https://developer.netless.link/javascript-zh/home/construct-room-and-player)\n\n[3] 配置 `WindowManager` 请看 [WindowManager](https://github.com/netless-io/window-manager#mount)\n\n### 使用 APPS\n\n**注意:** 需要先安装对应的 APP\n\n```bash\nnpm add @netless/app-slide\n```\n\n```typescript\n// 插入动态 PPTX 至白板\nconst appId = await whiteboard.insertDocs({\n fileType: \"pptx\",\n params: {\n scenePath: `/ppt/${uuid}`, // [1]\n title: \"a.pptx\",\n taskId: \"1234567...\", // [2]\n url: \"https://convertcdn.netless.link/dynamicConvert\", // [3]\n },\n});\n\n// 插入 PDF/静态 PPT 至白板\nconst appId = await whiteboard.insertDocs({\n fileType: \"pdf\", // or ppt\n options: {\n scenePath: `/pdf${uuid}`,\n title: \"a.pdf\", // 可选\n scenes: [], // SceneDefinition[] 静态/动态 Scene 数据\n },\n});\n\n// 插入音频/视频至白板\nconst appId = await whiteboard.manager.addApp({\n kind: \"MediaPlayer\",\n options: {\n title: \"test.mp3\", // 可选\n },\n attributes: {\n src: \"xxxx\", // 音视频 url\n },\n});\n```\n\n更多 `app` 请看 [netless-app](#https://github.com/netless-io/netless-app)\n\n## Develop\n\n```bash\npnpm i\n# upgrade dependencies\npnpm up -Li\n# build and see bundle size\npnpm build\nopen node_modules/.visualizer/stats.html\n```\n\n## License\n\nMIT @ [netless](https://github.com/netless-io)\n"
94
+ "readme": "# @netless/fastboard\n\nA whiteboard starter, based on [white-web-sdk](https://www.npmjs.com/package/white-web-sdk), [@netless/window-manager](https://www.npmjs.com/package/@netless/window-manager)\nand [netless-app](https://github.com/netless-io/netless-app).\n\n## 目录\n\n- [安装](#install)\n- [使用](#usage)\n- [进阶](./docs/advanced.md)\n- [开发](#开发)\n\n<h2 id=\"install\">安装</h2>\n\n```bash\nnpm add @netless/fastboard @netless/window-manager white-web-sdk react react-dom\n```\n\n<h2 id=\"usage\">使用</h2>\n\n<h3 id=\"mount-whiteboard\">挂载白板</h3>\n\n> 原生 `javascript`\n\n```js\nimport { createWhiteboardApp } from \"@netless/fastboard\";\n\nlet app = await createWhiteboardApp({\n target: document.getElementById(\"whiteboard\"),\n // [1]\n sdkConfig: {\n appIdentifier: \"whiteboard-appid\",\n },\n // [2]\n joinRoom: {\n uid: \"unique_id_for_each_client\",\n uuid: \"room-uuid\",\n roomToken: \"NETLESSROOM_...\",\n },\n // [3]\n managerConfig: {\n cursor: true,\n },\n});\n```\n\n> 使用 `React`\n\n```jsx\nimport { createWhiteboardApp, Fastboard } from \"@netless/fastboard\";\nimport ReactDOM from \"react-dom\";\n\nlet app = await createWhiteboardApp({\n /* ... */\n});\n\nfunction App() {\n return <Fastboard app={app} />;\n}\nReactDOM.render(<App />, document.getElementById(\"root\"));\n```\n\n[1] 关于 SDK 更多配置请看 [构造 WhiteWebSDK](https://developer.netless.link/javascript-zh/home/construct-white-web-sdk)\n\n[2] 加入房间更多配置请看 [构造 Room 与 Player 对象](https://developer.netless.link/javascript-zh/home/construct-room-and-player)\n\n[3] 配置 `WindowManager` 请看 [WindowManager](https://github.com/netless-io/window-manager#mount)\n\n### 使用 APPS\n\n**注意:** 需要先安装对应的 APP\n\n```bash\nnpm add @netless/app-slide\n```\n\n```typescript\n// 插入动态 PPTX 至白板\nconst appId = await app.insertDocs({\n fileType: \"pptx\",\n params: {\n scenePath: `/ppt/${uuid}`, // [1]\n title: \"a.pptx\",\n taskId: \"1234567...\", // [2]\n url: \"https://convertcdn.netless.link/dynamicConvert\", // [3]\n },\n});\n\n// 插入 PDF/静态 PPT 至白板\nconst appId = await app.insertDocs({\n fileType: \"pdf\", // or ppt\n options: {\n scenePath: `/pdf${uuid}`,\n title: \"a.pdf\", // 可选\n scenes: [], // SceneDefinition[] 静态/动态 Scene 数据\n },\n});\n\n// 插入音频/视频至白板\nconst appId = await app.manager.addApp({\n kind: \"MediaPlayer\",\n options: {\n title: \"test.mp3\", // 可选\n },\n attributes: {\n src: \"xxxx\", // 音视频 url\n },\n});\n```\n\n更多 `app` 请看 [netless-app](#https://github.com/netless-io/netless-app)\n\n## Develop\n\n```bash\npnpm i\n# upgrade dependencies\npnpm up -Li\n# build and see bundle size\npnpm build\nopen node_modules/.visualizer/stats.html\n```\n\n## License\n\nMIT @ [netless](https://github.com/netless-io)\n"
87
95
  }
@@ -1,47 +1,127 @@
1
+ import type { ConversionResponse, SceneDefinition } from "white-web-sdk";
1
2
  import type {
2
3
  InsertDocsParams,
4
+ InsertMediaParams,
3
5
  Language,
6
+ Layout,
4
7
  WhiteboardAppConfig,
5
8
  } from "./internal";
6
- import { Instance } from "./internal";
9
+ import { genUID, Instance, makeSlideParams } from "./internal";
7
10
 
8
- export type { WhiteboardAppConfig, InsertDocsParams };
11
+ export type {
12
+ Language,
13
+ Layout,
14
+ WhiteboardAppConfig,
15
+ InsertMediaParams,
16
+ InsertDocsParams,
17
+ };
9
18
 
10
19
  export class WhiteboardApp {
11
20
  private readonly _instance: Instance;
12
21
 
13
- private _target: HTMLElement | null = null;
14
- public get target(): HTMLElement | null {
15
- return this._target;
16
- }
17
-
18
- constructor(readonly config: WhiteboardAppConfig) {
22
+ public constructor(readonly config: WhiteboardAppConfig) {
19
23
  this._instance = new Instance(config);
20
24
  }
21
25
 
22
- get room() {
26
+ public get room() {
23
27
  return this._instance.room;
24
28
  }
25
29
 
26
- get manager() {
30
+ public get manager() {
27
31
  return this._instance.manager;
28
32
  }
29
33
 
30
- get sdk() {
34
+ public get sdk() {
31
35
  return this._instance.sdk;
32
36
  }
33
37
 
34
- get i18n() {
38
+ public get i18n() {
35
39
  return this._instance.i18n;
36
40
  }
37
41
 
42
+ public get target(): HTMLElement | null {
43
+ return this._instance.target;
44
+ }
45
+
46
+ public get collector(): HTMLElement | null {
47
+ return this._instance.collector;
48
+ }
49
+
38
50
  public bindElement(target?: HTMLElement | null) {
39
- this._target = target || null;
40
- this._instance.target = target || null;
51
+ this._instance.bindElement(target || null);
41
52
  }
42
53
 
43
- public insertDocs(params: InsertDocsParams) {
44
- return this._instance.insertDocs(params);
54
+ public bindCollector(collector?: HTMLElement | null) {
55
+ this._instance.bindCollector(collector || null);
56
+ }
57
+
58
+ public get layout() {
59
+ return this._instance.config.layout;
60
+ }
61
+
62
+ public updateLayout(layout?: Layout | undefined) {
63
+ this._instance.updateLayout(layout);
64
+ }
65
+
66
+ public insertMedia(params: InsertMediaParams) {
67
+ this._instance.insertMedia(params);
68
+ }
69
+
70
+ /**
71
+ * Insert PDF/PPTX from conversion result.
72
+ * @param status https://developer.netless.link/server-en/home/server-conversion#get-query-task-conversion-progress
73
+ */
74
+ public insertDocs(
75
+ filename: string,
76
+ status: ConversionResponse
77
+ ): Promise<string | undefined>;
78
+
79
+ /**
80
+ * Manual way.
81
+ */
82
+ public insertDocs(params: InsertDocsParams): Promise<string | undefined>;
83
+
84
+ public insertDocs(
85
+ arg1: string | InsertDocsParams,
86
+ arg2?: ConversionResponse
87
+ ) {
88
+ if (typeof arg1 === "object" && "fileType" in arg1) {
89
+ return this._instance.insertDocs(arg1);
90
+ } else if (arg2 && arg2.status !== "Finished") {
91
+ throw new Error("[WhiteboardApp] cannot insert a converting doc");
92
+ } else if (arg2 && arg2.progress) {
93
+ const scenes: SceneDefinition[] = arg2.progress.convertedFileList.map(
94
+ (f, i) => ({
95
+ name: String(i + 1),
96
+ ppt: {
97
+ src: f.conversionFileUrl,
98
+ width: f.width,
99
+ height: f.height,
100
+ previewURL: f.preview,
101
+ },
102
+ })
103
+ );
104
+ const uid = genUID();
105
+ const scenePath = `/${arg2.uuid}/${uid}`;
106
+ const { scenesWithoutPPT, taskId, url } = makeSlideParams(scenes);
107
+ if (taskId && url) {
108
+ return this._instance.insertDocs({
109
+ fileType: "pptx",
110
+ scenePath,
111
+ taskId,
112
+ title: arg1,
113
+ url,
114
+ scenes: scenesWithoutPPT,
115
+ });
116
+ } else {
117
+ return this._instance.insertDocs({
118
+ fileType: "pdf",
119
+ scenePath,
120
+ scenes,
121
+ title: arg1,
122
+ });
123
+ }
124
+ }
45
125
  }
46
126
 
47
127
  public insertCodeEditor() {
@@ -36,14 +36,14 @@ export function PageControl({
36
36
  if (manager && room) {
37
37
  await manager.switchMainViewToWriter();
38
38
  const path = room.state.sceneState.contextPath;
39
- room.putScenes(path, [{}], pageCount);
40
- await manager.setMainViewSceneIndex(pageIndex);
39
+ room.putScenes(path, [{}], pageIndex + 1);
40
+ await manager.setMainViewSceneIndex(pageIndex + 1);
41
41
  } else if (!manager && room) {
42
42
  const path = room.state.sceneState.contextPath;
43
- room.putScenes(path, [{}], pageCount);
44
- room.setSceneIndex(pageIndex);
43
+ room.putScenes(path, [{}], pageIndex + 1);
44
+ room.setSceneIndex(pageIndex + 1);
45
45
  }
46
- }, [room, manager, pageCount, pageIndex]);
46
+ }, [room, manager, pageIndex]);
47
47
 
48
48
  const prevPage = useCallback(() => {
49
49
  if (room?.isWritable) {
@@ -1,8 +1,9 @@
1
1
  import type { DependencyList } from "react";
2
2
  import type { Player } from "white-web-sdk";
3
3
 
4
- import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { useCallback, useEffect, useState } from "react";
5
5
  import { PlayerPhase } from "white-web-sdk";
6
+ import { useLastValue } from "../../internal/hooks";
6
7
 
7
8
  const EMPTY_ARRAY: DependencyList = [];
8
9
 
@@ -12,14 +13,6 @@ function useForceUpdate() {
12
13
  return useCallback(() => forceUpdate_({}), EMPTY_ARRAY);
13
14
  }
14
15
 
15
- function useLastValue<T>(value: T) {
16
- const ref = useRef<T>(value);
17
- useEffect(() => {
18
- ref.current = value;
19
- }, [value]);
20
- return ref.current;
21
- }
22
-
23
16
  export function usePlayer(player?: Player | null) {
24
17
  const togglePlay = useCallback(() => {
25
18
  if (player) {
@@ -10,7 +10,7 @@ export interface RootProps {
10
10
  instance: Instance;
11
11
  }
12
12
 
13
- export default function Root({ instance: app }: RootProps) {
13
+ export function Root({ instance: app }: RootProps) {
14
14
  const [mux] = useState(() => new Lock());
15
15
 
16
16
  const useWhiteboard = useCallback(
@@ -21,21 +21,40 @@ export default function Root({ instance: app }: RootProps) {
21
21
  [app, mux]
22
22
  );
23
23
 
24
+ const {
25
+ Toolbar: toolbar = true,
26
+ RedoUndo: redo_undo = true,
27
+ ZoomControl: zoom_control = true,
28
+ PageControl: page_control = true,
29
+ } = app.config.layout || {};
30
+
31
+ const props = {
32
+ room: app.room,
33
+ manager: app.manager,
34
+ i18n: app.i18n,
35
+ };
36
+
24
37
  return (
25
38
  <Instance.Context.Provider value={app}>
26
39
  <div className="fastboard-root">
27
40
  {!app.room && <div className="fastboard-loading">Loading&hellip;</div>}
28
41
  <div className="fastboard-view" ref={useWhiteboard} />
29
- <div className="fastboard-left">
30
- <Toolbar room={app.room} manager={app.manager} i18n={app.i18n} />
31
- </div>
32
- <div className="fastboard-bottom-left">
33
- <RedoUndo room={app.room} manager={app.manager} i18n={app.i18n} />
34
- <ZoomControl room={app.room} manager={app.manager} i18n={app.i18n} />
35
- </div>
36
- <div className="fastboard-bottom-right">
37
- <PageControl room={app.room} manager={app.manager} i18n={app.i18n} />
38
- </div>
42
+ {toolbar && (
43
+ <div className="fastboard-left">
44
+ <Toolbar {...props} />
45
+ </div>
46
+ )}
47
+ {(redo_undo || zoom_control) && (
48
+ <div className="fastboard-bottom-left">
49
+ {redo_undo && <RedoUndo {...props} />}
50
+ {zoom_control && <ZoomControl {...props} />}
51
+ </div>
52
+ )}
53
+ {page_control && (
54
+ <div className="fastboard-bottom-right">
55
+ <PageControl {...props} />
56
+ </div>
57
+ )}
39
58
  </div>
40
59
  </Instance.Context.Provider>
41
60
  );
package/src/index.ts CHANGED
@@ -15,7 +15,7 @@ export {
15
15
  type PlayerControlProps,
16
16
  } from "./components/PlayerControl";
17
17
  export * from "./WhiteboardApp";
18
- export * from "./hooks";
18
+ export * from "./react";
19
19
 
20
20
  export const register = WindowManager.register.bind(WindowManager);
21
21
 
@@ -1,3 +1,4 @@
1
+ import type { Mutable } from "type-fest";
1
2
  import type { WindowManager } from "@netless/window-manager";
2
3
  import type { Room, SceneDefinition, WhiteWebSdk } from "white-web-sdk";
3
4
  import type { JoinRoom, ManagerConfig, SdkConfig } from "./mount-whiteboard";
@@ -5,8 +6,9 @@ import type { i18n } from "i18next";
5
6
 
6
7
  import React, { createContext, useContext } from "react";
7
8
  import ReactDOM from "react-dom";
9
+ import { BuiltinApps } from "@netless/window-manager";
8
10
 
9
- import Root from "../components/Root";
11
+ import { Root } from "../components/Root";
10
12
  import { mountWhiteboard } from "./mount-whiteboard";
11
13
  import { noop } from "./helpers";
12
14
 
@@ -17,6 +19,11 @@ export interface AcceptParams {
17
19
  readonly i18n: i18n;
18
20
  }
19
21
 
22
+ export interface InsertMediaParams {
23
+ title: string;
24
+ src: string;
25
+ }
26
+
20
27
  export interface InsertDocsStatic {
21
28
  readonly fileType: "pdf" | "ppt";
22
29
  readonly scenePath: string;
@@ -30,16 +37,25 @@ export interface InsertDocsDynamic {
30
37
  readonly taskId: string;
31
38
  readonly title?: string;
32
39
  readonly url?: string;
40
+ readonly scenes?: SceneDefinition[];
33
41
  }
34
42
 
35
43
  export type InsertDocsParams = InsertDocsStatic | InsertDocsDynamic;
36
44
 
37
45
  export type Language = "zh-CN" | "en-US";
38
46
 
47
+ export interface Layout {
48
+ Toolbar?: boolean;
49
+ PageControl?: boolean;
50
+ RedoUndo?: boolean;
51
+ ZoomControl?: boolean;
52
+ }
53
+
39
54
  export interface WhiteboardAppConfig {
40
55
  readonly sdkConfig: SdkConfig;
41
56
  readonly joinRoom: JoinRoom;
42
57
  readonly managerConfig?: Omit<ManagerConfig, "container">;
58
+ readonly layout?: Layout;
43
59
  readonly toolbar?: {
44
60
  apps?: {
45
61
  enable?: boolean;
@@ -60,7 +76,7 @@ export interface Essentials {
60
76
  export class Instance {
61
77
  static readonly Context = createContext<Instance | null>(null);
62
78
 
63
- readonly config: WhiteboardAppConfig;
79
+ config: Mutable<WhiteboardAppConfig>;
64
80
 
65
81
  sdk: WhiteWebSdk | null = null;
66
82
  room: Room | null = null;
@@ -82,13 +98,11 @@ export class Instance {
82
98
  }
83
99
 
84
100
  constructor(config: WhiteboardAppConfig) {
85
- this.config = config;
101
+ this.config = { ...config };
86
102
  this.refreshReadyPromise();
87
103
  this.initialize();
88
104
  }
89
105
 
90
- private _target: HTMLElement | null = null;
91
-
92
106
  async initialize() {
93
107
  const essentials = await mountWhiteboard(
94
108
  this.config.sdkConfig,
@@ -100,15 +114,26 @@ export class Instance {
100
114
  this.resolveReady();
101
115
  }
102
116
 
103
- get target(): HTMLElement | null {
104
- return this._target;
117
+ target: HTMLElement | null = null;
118
+ collector: HTMLElement | null = null;
119
+
120
+ bindElement(target: HTMLElement | null) {
121
+ if (this.target && this.target !== target) {
122
+ ReactDOM.unmountComponentAtNode(this.target);
123
+ }
124
+ this.target = target;
125
+ this.forceUpdate();
105
126
  }
106
127
 
107
- set target(value: HTMLElement | null) {
108
- if (this._target && value) {
109
- ReactDOM.unmountComponentAtNode(this._target);
128
+ bindCollector(collector: HTMLElement | null) {
129
+ this.collector = collector;
130
+ if (this.manager && collector) {
131
+ this.manager.bindCollectorContainer(collector);
110
132
  }
111
- this._target = value;
133
+ }
134
+
135
+ updateLayout(layout: Layout | undefined) {
136
+ this.config.layout = layout;
112
137
  this.forceUpdate();
113
138
  }
114
139
 
@@ -139,8 +164,12 @@ export class Instance {
139
164
 
140
165
  async mount(node: HTMLElement) {
141
166
  await this.readyPromise;
142
- if (this.manager) {
143
- this.manager.bindContainer(node);
167
+ if (!this.manager) {
168
+ throw new Error(`[WhiteboardApp] mounted, but not found window manager`);
169
+ }
170
+ this.manager.bindContainer(node);
171
+ if (this.collector) {
172
+ this.manager.bindCollectorContainer(this.collector);
144
173
  }
145
174
  }
146
175
 
@@ -181,6 +210,7 @@ export class Instance {
181
210
  options: {
182
211
  scenePath: params.scenePath,
183
212
  title: params.title,
213
+ scenes: params.scenes,
184
214
  },
185
215
  attributes: {
186
216
  taskId: params.taskId,
@@ -220,6 +250,17 @@ export class Instance {
220
250
  });
221
251
  }
222
252
 
253
+ insertMedia({ title, src }: InsertMediaParams) {
254
+ if (!this.manager) {
255
+ throw new Error(`[WhiteboardApp] cannot insert app before mounted`);
256
+ }
257
+ return this.manager.addApp({
258
+ kind: BuiltinApps.MediaPlayer,
259
+ options: { title },
260
+ attributes: { src },
261
+ });
262
+ }
263
+
223
264
  async changeLanguage(language: Language) {
224
265
  try {
225
266
  await this.i18n?.changeLanguage(language);
@@ -1,3 +1,5 @@
1
+ import type { SceneDefinition } from "white-web-sdk";
2
+
1
3
  export function noop() {
2
4
  return;
3
5
  }
@@ -40,3 +42,45 @@ export class Lock {
40
42
  }
41
43
  };
42
44
  }
45
+
46
+ // Copy from https://github.com/crimx/side-effect-manager/blob/main/src/gen-uid.ts
47
+ const SOUP =
48
+ "!#%()*+,-./:;=?@[]^_`{|}~" +
49
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
50
+ const SOUP_LEN = SOUP.length;
51
+ const ID_LEN = 20;
52
+ const reusedIdCarrier = Array(ID_LEN);
53
+
54
+ export const genUID = (): string => {
55
+ for (let i = 0; i < ID_LEN; i++) {
56
+ reusedIdCarrier[i] = SOUP.charAt(Math.random() * SOUP_LEN);
57
+ }
58
+ return reusedIdCarrier.join("");
59
+ };
60
+
61
+ export function makeSlideParams(scenes: SceneDefinition[]) {
62
+ const scenesWithoutPPT: SceneDefinition[] = [];
63
+ let taskId = "";
64
+ let url = "";
65
+
66
+ // e.g. "ppt(x)://cdn/prefix/dynamicConvert/{taskId}/1.slide"
67
+ const pptSrcRE = /^pptx?(?<prefix>:\/\/\S+?dynamicConvert)\/(?<taskId>\w+)\//;
68
+
69
+ for (const { name, ppt } of scenes) {
70
+ // make sure scenesWithoutPPT.length === scenes.length
71
+ scenesWithoutPPT.push({ name });
72
+
73
+ if (!ppt || !ppt.src.startsWith("ppt")) {
74
+ continue;
75
+ }
76
+ const match = pptSrcRE.exec(ppt.src);
77
+ if (!match || !match.groups) {
78
+ continue;
79
+ }
80
+ taskId = match.groups.taskId;
81
+ url = "https" + match.groups.prefix;
82
+ break;
83
+ }
84
+
85
+ return { scenesWithoutPPT, taskId, url };
86
+ }
@@ -0,0 +1,9 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ export function useLastValue<T>(value: T) {
4
+ const ref = useRef<T>(value);
5
+ useEffect(() => {
6
+ ref.current = value;
7
+ }, [value]);
8
+ return ref.current;
9
+ }
@@ -4,7 +4,7 @@ import type {
4
4
  RoomCallbacks,
5
5
  WhiteWebSdkConfiguration,
6
6
  } from "white-web-sdk";
7
- import type { Essentials, Language } from "./instance";
7
+ import type { Essentials, Language } from "./Instance";
8
8
 
9
9
  import { WindowManager } from "@netless/window-manager";
10
10
  import { DefaultHotKeys, WhiteWebSdk } from "white-web-sdk";
package/src/react.tsx ADDED
@@ -0,0 +1,26 @@
1
+ import type { WhiteboardApp } from "./index";
2
+
3
+ import React, { useEffect, useRef } from "react";
4
+ import { useLastValue } from "./internal/hooks";
5
+
6
+ /**
7
+ * @example
8
+ * let app = await createWhiteboardApp(config)
9
+ * <Fastboard app={app} />
10
+ * await app.dispose()
11
+ */
12
+ export function Fastboard({ app }: { app?: WhiteboardApp | null }) {
13
+ const ref = useRef<HTMLDivElement>(null);
14
+ const previous = useLastValue(app);
15
+
16
+ useEffect(() => {
17
+ if (previous && previous !== app) {
18
+ previous.bindElement(null);
19
+ }
20
+ if (app) {
21
+ app.bindElement(ref.current);
22
+ }
23
+ }, [app, previous]);
24
+
25
+ return <div className="fastboard" ref={ref} />;
26
+ }
package/src/style.scss CHANGED
@@ -9,6 +9,12 @@
9
9
  @import "./components/Toolbar/Toolbar.scss";
10
10
  @import "./components/PlayerControl/PlayerControl.scss";
11
11
 
12
+ .fastboard {
13
+ width: 100%;
14
+ height: 100%;
15
+ position: relative;
16
+ }
17
+
12
18
  .tippy-box.fastboard-tip {
13
19
  color: #eee;
14
20
  background-color: rgba($color: #000000, $alpha: 0.95);
package/src/hooks.ts DELETED
@@ -1,46 +0,0 @@
1
- import type { WhiteboardApp, WhiteboardAppConfig } from "./WhiteboardApp";
2
-
3
- import { useEffect, useState } from "react";
4
- import { createWhiteboardApp } from "./index";
5
-
6
- export type FastBoardConfig = WhiteboardAppConfig;
7
-
8
- /**
9
- * @example
10
- * const [app, ref] = useFastboard({ sdkConfig, joinRoom })
11
- * if (app) {
12
- * app.insertDocs({...})
13
- * }
14
- * return <div style={{ width: '100%', height: '100%' }} ref={ref} />
15
- */
16
- export function useFastboard(config: FastBoardConfig): readonly [
17
- app: WhiteboardApp | null,
18
- ref: (div: HTMLDivElement | null) => void
19
- ] & {
20
- readonly app: WhiteboardApp | null;
21
- readonly ref: (div: HTMLDivElement | null) => void;
22
- } {
23
- const [app, setApp] = useState<WhiteboardApp | null>(null);
24
- const [currentTarget, ref] = useState<HTMLDivElement | null>(null);
25
-
26
- useEffect(() => {
27
- let isMounted = true;
28
- const promise = createWhiteboardApp(config).then(app => {
29
- if (isMounted) setApp(app);
30
- });
31
- return () => {
32
- isMounted = false;
33
- promise.then(() => app?.dispose());
34
- };
35
- // ignore config and app change
36
- // eslint-disable-next-line react-hooks/exhaustive-deps
37
- }, []);
38
-
39
- useEffect(() => {
40
- if (app) {
41
- app.bindElement(currentTarget);
42
- }
43
- }, [app, currentTarget]);
44
-
45
- return Object.assign([app, ref] as const, { app, ref });
46
- }