@netless/fastboard 0.0.6 → 0.0.10
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/README.md +10 -18
- package/dist/index.cjs.js +4 -4
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +176 -78
- package/dist/index.es.js.map +1 -1
- package/package.json +10 -2
- package/src/WhiteboardApp.ts +91 -20
- package/src/components/PlayerControl/hooks.ts +2 -9
- package/src/components/Root.tsx +30 -11
- package/src/index.ts +1 -1
- package/src/internal/Instance.tsx +47 -19
- package/src/internal/helpers.ts +44 -0
- package/src/internal/hooks.ts +9 -0
- package/src/internal/mount-whiteboard.ts +1 -1
- package/src/react.tsx +52 -0
- package/src/style.scss +6 -0
- package/src/hooks.ts +0 -53
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netless/fastboard",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
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
|
|
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
|
}
|
package/src/WhiteboardApp.ts
CHANGED
|
@@ -1,56 +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 {
|
|
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
|
-
constructor(readonly config: WhiteboardAppConfig) {
|
|
22
|
+
public constructor(readonly config: WhiteboardAppConfig) {
|
|
14
23
|
this._instance = new Instance(config);
|
|
15
24
|
}
|
|
16
25
|
|
|
17
|
-
get room() {
|
|
26
|
+
public get room() {
|
|
18
27
|
return this._instance.room;
|
|
19
28
|
}
|
|
20
29
|
|
|
21
|
-
get manager() {
|
|
30
|
+
public get manager() {
|
|
22
31
|
return this._instance.manager;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
|
-
get sdk() {
|
|
34
|
+
public get sdk() {
|
|
26
35
|
return this._instance.sdk;
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
get i18n() {
|
|
38
|
+
public get i18n() {
|
|
30
39
|
return this._instance.i18n;
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
private _target: HTMLElement | null = null;
|
|
34
42
|
public get target(): HTMLElement | null {
|
|
35
|
-
return this.
|
|
43
|
+
return this._instance.target;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
private _collector: HTMLElement | null = null;
|
|
39
46
|
public get collector(): HTMLElement | null {
|
|
40
|
-
return this.
|
|
47
|
+
return this._instance.collector;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
public bindElement(
|
|
44
|
-
target
|
|
45
|
-
collector?: HTMLElement | null
|
|
46
|
-
) {
|
|
47
|
-
this._target = target || null;
|
|
48
|
-
this._collector = collector || null;
|
|
49
|
-
this._instance.bindElement(this._target, this._collector);
|
|
50
|
+
public bindElement(target?: HTMLElement | null) {
|
|
51
|
+
this._instance.bindElement(target || null);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
public
|
|
53
|
-
|
|
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
|
+
}
|
|
54
125
|
}
|
|
55
126
|
|
|
56
127
|
public insertCodeEditor() {
|
|
@@ -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,
|
|
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) {
|
package/src/components/Root.tsx
CHANGED
|
@@ -10,7 +10,7 @@ export interface RootProps {
|
|
|
10
10
|
instance: Instance;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export
|
|
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…</div>}
|
|
28
41
|
<div className="fastboard-view" ref={useWhiteboard} />
|
|
29
|
-
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
@@ -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
|
-
|
|
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,23 +114,26 @@ export class Instance {
|
|
|
100
114
|
this.resolveReady();
|
|
101
115
|
}
|
|
102
116
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
117
|
+
target: HTMLElement | null = null;
|
|
118
|
+
collector: HTMLElement | null = null;
|
|
106
119
|
|
|
107
|
-
|
|
108
|
-
if (this.
|
|
109
|
-
ReactDOM.unmountComponentAtNode(this.
|
|
120
|
+
bindElement(target: HTMLElement | null) {
|
|
121
|
+
if (this.target && this.target !== target) {
|
|
122
|
+
ReactDOM.unmountComponentAtNode(this.target);
|
|
110
123
|
}
|
|
111
|
-
this.
|
|
124
|
+
this.target = target;
|
|
112
125
|
this.forceUpdate();
|
|
113
126
|
}
|
|
114
127
|
|
|
115
|
-
collector: HTMLElement | null
|
|
116
|
-
|
|
117
|
-
bindElement(target: HTMLElement | null, collector: HTMLElement | null) {
|
|
118
|
-
this.target = target;
|
|
128
|
+
bindCollector(collector: HTMLElement | null) {
|
|
119
129
|
this.collector = collector;
|
|
130
|
+
if (this.manager && collector) {
|
|
131
|
+
this.manager.bindCollectorContainer(collector);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
updateLayout(layout: Layout | undefined) {
|
|
136
|
+
this.config.layout = layout;
|
|
120
137
|
this.forceUpdate();
|
|
121
138
|
}
|
|
122
139
|
|
|
@@ -150,10 +167,9 @@ export class Instance {
|
|
|
150
167
|
if (!this.manager) {
|
|
151
168
|
throw new Error(`[WhiteboardApp] mounted, but not found window manager`);
|
|
152
169
|
}
|
|
170
|
+
this.manager.bindContainer(node);
|
|
153
171
|
if (this.collector) {
|
|
154
|
-
this.manager.
|
|
155
|
-
} else {
|
|
156
|
-
this.manager.bindContainer(node);
|
|
172
|
+
this.manager.bindCollectorContainer(this.collector);
|
|
157
173
|
}
|
|
158
174
|
}
|
|
159
175
|
|
|
@@ -194,6 +210,7 @@ export class Instance {
|
|
|
194
210
|
options: {
|
|
195
211
|
scenePath: params.scenePath,
|
|
196
212
|
title: params.title,
|
|
213
|
+
scenes: params.scenes,
|
|
197
214
|
},
|
|
198
215
|
attributes: {
|
|
199
216
|
taskId: params.taskId,
|
|
@@ -233,6 +250,17 @@ export class Instance {
|
|
|
233
250
|
});
|
|
234
251
|
}
|
|
235
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
|
+
|
|
236
264
|
async changeLanguage(language: Language) {
|
|
237
265
|
try {
|
|
238
266
|
await this.i18n?.changeLanguage(language);
|
package/src/internal/helpers.ts
CHANGED
|
@@ -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
|
+
}
|
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
RoomCallbacks,
|
|
5
5
|
WhiteWebSdkConfiguration,
|
|
6
6
|
} from "white-web-sdk";
|
|
7
|
-
import type { Essentials, Language } from "./
|
|
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,52 @@
|
|
|
1
|
+
import type { WhiteboardApp } from "./index";
|
|
2
|
+
|
|
3
|
+
import React, { forwardRef, useEffect, useRef } from "react";
|
|
4
|
+
import { useLastValue } from "./internal/hooks";
|
|
5
|
+
|
|
6
|
+
// https://itnext.io/reusing-the-ref-from-forwardref-with-react-hooks-4ce9df693dd
|
|
7
|
+
function useCombinedRefs<T>(...refs: React.Ref<T>[]) {
|
|
8
|
+
const targetRef = useRef<T | null>(null);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
for (const ref of refs) {
|
|
12
|
+
if (!ref) continue;
|
|
13
|
+
|
|
14
|
+
if (typeof ref === "function") {
|
|
15
|
+
ref(targetRef.current);
|
|
16
|
+
} else {
|
|
17
|
+
(ref as typeof targetRef).current = targetRef.current;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}, [refs]);
|
|
21
|
+
|
|
22
|
+
return targetRef;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @example
|
|
27
|
+
* let app = await createWhiteboardApp(config)
|
|
28
|
+
* <Fastboard app={app} />
|
|
29
|
+
* await app.dispose()
|
|
30
|
+
*/
|
|
31
|
+
export const Fastboard = forwardRef<
|
|
32
|
+
HTMLDivElement,
|
|
33
|
+
{ app?: WhiteboardApp | null } & React.DetailedHTMLProps<
|
|
34
|
+
React.HTMLAttributes<HTMLDivElement>,
|
|
35
|
+
HTMLDivElement
|
|
36
|
+
>
|
|
37
|
+
>(({ app, ...restProps }, outerRef) => {
|
|
38
|
+
const innerRef = useRef<HTMLDivElement>(null);
|
|
39
|
+
const ref = useCombinedRefs(outerRef, innerRef);
|
|
40
|
+
const previous = useLastValue(app);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (previous && previous !== app) {
|
|
44
|
+
previous.bindElement(null);
|
|
45
|
+
}
|
|
46
|
+
if (app) {
|
|
47
|
+
app.bindElement(ref.current);
|
|
48
|
+
}
|
|
49
|
+
}, [app, previous, ref]);
|
|
50
|
+
|
|
51
|
+
return <div className="fastboard" {...restProps} ref={ref} />;
|
|
52
|
+
});
|
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,53 +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
|
-
collectorRef: (div: HTMLDivElement | null) => void
|
|
20
|
-
] & {
|
|
21
|
-
readonly app: WhiteboardApp | null;
|
|
22
|
-
readonly ref: (div: HTMLDivElement | null) => void;
|
|
23
|
-
readonly collectorRef: (div: HTMLDivElement | null) => void;
|
|
24
|
-
} {
|
|
25
|
-
const [app, setApp] = useState<WhiteboardApp | null>(null);
|
|
26
|
-
const [currentTarget, ref] = useState<HTMLDivElement | null>(null);
|
|
27
|
-
const [collector, collectorRef] = useState<HTMLDivElement | null>(null);
|
|
28
|
-
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
let isMounted = true;
|
|
31
|
-
const promise = createWhiteboardApp(config).then(app => {
|
|
32
|
-
if (isMounted) setApp(app);
|
|
33
|
-
});
|
|
34
|
-
return () => {
|
|
35
|
-
isMounted = false;
|
|
36
|
-
promise.then(() => app?.dispose());
|
|
37
|
-
};
|
|
38
|
-
// ignore config and app change
|
|
39
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
40
|
-
}, []);
|
|
41
|
-
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
if (app) {
|
|
44
|
-
app.bindElement(currentTarget, collector);
|
|
45
|
-
}
|
|
46
|
-
}, [app, collector, currentTarget]);
|
|
47
|
-
|
|
48
|
-
return Object.assign([app, ref, collectorRef] as const, {
|
|
49
|
-
app,
|
|
50
|
-
ref,
|
|
51
|
-
collectorRef,
|
|
52
|
-
});
|
|
53
|
-
}
|