@netless/fastboard 0.0.1 → 0.0.5

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 (43) hide show
  1. package/README.md +8 -10
  2. package/dist/index.cjs.js +4 -4
  3. package/dist/index.cjs.js.map +1 -1
  4. package/dist/index.es.js +478 -168
  5. package/dist/index.es.js.map +1 -1
  6. package/dist/svelte.cjs.js +2 -0
  7. package/dist/svelte.cjs.js.map +1 -0
  8. package/dist/svelte.es.js +31 -0
  9. package/dist/svelte.es.js.map +1 -0
  10. package/dist/vue.cjs.js +2 -0
  11. package/dist/vue.cjs.js.map +1 -0
  12. package/dist/vue.es.js +42 -0
  13. package/dist/vue.es.js.map +1 -0
  14. package/package.json +25 -41
  15. package/src/WhiteboardApp.ts +39 -3
  16. package/src/behaviors/style.ts +1 -1
  17. package/src/components/PlayerControl/PlayerControl.scss +145 -0
  18. package/src/components/PlayerControl/PlayerControl.tsx +158 -0
  19. package/src/components/PlayerControl/components/Button.tsx +55 -0
  20. package/src/components/PlayerControl/hooks.ts +95 -0
  21. package/src/components/PlayerControl/icons/Loading.tsx +13 -0
  22. package/src/components/PlayerControl/icons/Pause.tsx +13 -0
  23. package/src/components/PlayerControl/icons/Play.tsx +13 -0
  24. package/src/components/PlayerControl/icons/index.ts +10 -0
  25. package/src/components/PlayerControl/index.ts +1 -0
  26. package/src/components/Root.tsx +1 -2
  27. package/src/components/Toolbar/Content.tsx +28 -15
  28. package/src/components/Toolbar/components/ColorBox.tsx +3 -3
  29. package/src/components/Toolbar/components/UpDownButtons.tsx +7 -10
  30. package/src/components/Toolbar/hooks.ts +1 -1
  31. package/src/components/ZoomControl.tsx +1 -1
  32. package/src/hooks.ts +18 -60
  33. package/src/i18n/en.json +2 -1
  34. package/src/i18n/zh-CN.json +2 -1
  35. package/src/index.ts +9 -2
  36. package/src/internal/Instance.tsx +40 -19
  37. package/src/internal/helpers.ts +15 -0
  38. package/src/internal/index.ts +1 -0
  39. package/src/internal/mount-whiteboard.ts +13 -5
  40. package/src/style.scss +1 -0
  41. package/src/svelte.ts +45 -0
  42. package/src/vue.ts +74 -0
  43. package/src/helpers/index.ts +0 -18
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});exports[Symbol.toStringTag]="Module";var a=require("svelte/store"),n=require("./index.cjs.js");require("@netless/window-manager");require("white-web-sdk");require("i18next");require("react");require("react-dom");require("clsx");require("@tippyjs/react");require("rc-slider");function o(t,i){return a.readable(null,s=>{let e=null;n.createWhiteboardApp(i).then(r=>{s(e=r)});const u=t.subscribe(r=>{r&&e&&e.bindElement(r)});return()=>{u(),e&&e.dispose()}})}exports.useFastboard=o;
2
+ //# sourceMappingURL=svelte.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svelte.cjs.js","sources":["../src/svelte.ts"],"sourcesContent":["import { readable, type Readable } from \"svelte/store\";\nimport { createWhiteboardApp } from \"./index\";\n\nimport type { WhiteboardApp, WhiteboardAppConfig } from \"./WhiteboardApp\";\n\nexport type FastBoardConfig = WhiteboardAppConfig;\n\n/**\n * @example\n * ```svelte\n * <script>\n * let ref = writable(null)\n * let app = useFastboard(ref, { sdkConfig, joinRoom })\n * if (app) {\n * app.insertDocs({...})\n * }\n * </script>\n * <div style=\"width: 100%; height: 100%\" bind:this={$ref} />\n * ```\n */\nexport function useFastboard(\n ref: Readable<HTMLElement | null>,\n config: FastBoardConfig\n): Readable<WhiteboardApp | null> {\n return readable<WhiteboardApp | null>(null, set => {\n let app_: WhiteboardApp | null = null;\n\n createWhiteboardApp(config).then(app => {\n set((app_ = app));\n });\n\n const dispose = ref.subscribe(div => {\n if (div && app_) {\n app_.bindElement(div);\n }\n });\n\n return () => {\n dispose();\n if (app_) {\n app_.dispose();\n }\n };\n });\n}\n"],"names":["readable"],"mappings":"kWAqBE,EACA,EACgC,OACzBA,YAA+B,KAAM,GAAO,IAC7C,GAA6B,2BAEb,GAAQ,KAAK,GAAO,GACjC,EAAO,UAGR,GAAU,EAAI,UAAU,GAAO,CAC/B,GAAO,KACJ,YAAY,WAId,IAAM,KAEP,KACG"}
@@ -0,0 +1,31 @@
1
+ import { readable } from "svelte/store";
2
+ import { createWhiteboardApp } from "./index.es.js";
3
+ import "@netless/window-manager";
4
+ import "white-web-sdk";
5
+ import "i18next";
6
+ import "react";
7
+ import "react-dom";
8
+ import "clsx";
9
+ import "@tippyjs/react";
10
+ import "rc-slider";
11
+ function useFastboard(ref, config) {
12
+ return readable(null, (set) => {
13
+ let app_ = null;
14
+ createWhiteboardApp(config).then((app) => {
15
+ set(app_ = app);
16
+ });
17
+ const dispose = ref.subscribe((div) => {
18
+ if (div && app_) {
19
+ app_.bindElement(div);
20
+ }
21
+ });
22
+ return () => {
23
+ dispose();
24
+ if (app_) {
25
+ app_.dispose();
26
+ }
27
+ };
28
+ });
29
+ }
30
+ export { useFastboard };
31
+ //# sourceMappingURL=svelte.es.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svelte.es.js","sources":["../src/svelte.ts"],"sourcesContent":["import { readable, type Readable } from \"svelte/store\";\nimport { createWhiteboardApp } from \"./index\";\n\nimport type { WhiteboardApp, WhiteboardAppConfig } from \"./WhiteboardApp\";\n\nexport type FastBoardConfig = WhiteboardAppConfig;\n\n/**\n * @example\n * ```svelte\n * <script>\n * let ref = writable(null)\n * let app = useFastboard(ref, { sdkConfig, joinRoom })\n * if (app) {\n * app.insertDocs({...})\n * }\n * </script>\n * <div style=\"width: 100%; height: 100%\" bind:this={$ref} />\n * ```\n */\nexport function useFastboard(\n ref: Readable<HTMLElement | null>,\n config: FastBoardConfig\n): Readable<WhiteboardApp | null> {\n return readable<WhiteboardApp | null>(null, set => {\n let app_: WhiteboardApp | null = null;\n\n createWhiteboardApp(config).then(app => {\n set((app_ = app));\n });\n\n const dispose = ref.subscribe(div => {\n if (div && app_) {\n app_.bindElement(div);\n }\n });\n\n return () => {\n dispose();\n if (app_) {\n app_.dispose();\n }\n };\n });\n}\n"],"names":[],"mappings":";;;;;;;;;;sBAqBE,KACA,QACgC;SACzB,SAA+B,MAAM,SAAO;QAC7C,OAA6B;wBAEb,QAAQ,KAAK,SAAO;UACjC,OAAO;AAAA;UAGR,UAAU,IAAI,UAAU,SAAO;UAC/B,OAAO,MAAM;aACV,YAAY;AAAA;AAAA;WAId,MAAM;;UAEP,MAAM;aACH;AAAA;AAAA;AAAA;AAAA;;"}
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});exports[Symbol.toStringTag]="Module";var t=require("vue-demi"),s=require("./index.cjs.js");require("@netless/window-manager");require("white-web-sdk");require("i18next");require("react");require("react-dom");require("clsx");require("@tippyjs/react");require("rc-slider");function n(u){var e;const r=t.unref(u);return(e=r==null?void 0:r.$el)!=null?e:r}function o(u){return t.getCurrentScope()?(t.onScopeDispose(u),!0):!1}function l(u,r){const e=t.shallowRef(null),i=t.computed(()=>n(u));return s.createWhiteboardApp(r).then(a=>{e.value=a}),t.watchEffect(()=>{i.value&&e.value&&e.value.bindElement(i.value)}),o(()=>{e.value&&e.value.dispose()}),e}exports.unrefElement=n;exports.useFastboard=l;
2
+ //# sourceMappingURL=vue.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vue.cjs.js","sources":["../src/vue.ts"],"sourcesContent":["import type { ComponentPublicInstance, Ref } from \"vue-demi\";\nimport {\n computed,\n getCurrentScope,\n onScopeDispose,\n shallowRef,\n unref,\n watchEffect,\n} from \"vue-demi\";\nimport { createWhiteboardApp } from \"./index\";\n\nimport type { WhiteboardApp, WhiteboardAppConfig } from \"./WhiteboardApp\";\n\nexport type FastBoardConfig = WhiteboardAppConfig;\n\nexport type MaybeRef<T> = T | Ref<T>;\nexport type VueInstance = ComponentPublicInstance;\nexport type MaybeElementRef = MaybeRef<\n HTMLElement | SVGElement | VueInstance | undefined | null\n>;\n\n/**\n * Get the dom element of a ref of element or Vue component instance\n */\nexport function unrefElement(elRef: MaybeElementRef) {\n const plain = unref(elRef);\n return (plain as VueInstance)?.$el ?? plain;\n}\n\nfunction tryOnScopeDispose(fn: () => void) {\n if (getCurrentScope()) {\n onScopeDispose(fn);\n return true;\n }\n return false;\n}\n\n/**\n * @example\n * ```vue\n * <script setup>\n * const el = ref(null)\n * const app = useFastboard(el, { sdkConfig, joinRoom })\n * if (app.value) {\n * app.value.insertDocs({...})\n * }\n * </script>\n * <template>\n * <div style=\"width: 100%; height: 100%\" ref=\"el\"></div>\n * </template>\n * ```\n */\nexport function useFastboard(el: MaybeElementRef, config: FastBoardConfig) {\n const app = shallowRef<WhiteboardApp | null>(null);\n const target = computed(() => unrefElement(el));\n\n createWhiteboardApp(config).then(app_ => {\n app.value = app_;\n });\n\n watchEffect(() => {\n if (target.value && app.value) {\n app.value.bindElement(target.value);\n }\n });\n\n tryOnScopeDispose(() => {\n if (app.value) {\n app.value.dispose();\n }\n });\n\n return app;\n}\n"],"names":["unref","getCurrentScope","shallowRef","computed"],"mappings":"8VAwB6B,EAAwB,YAC7C,GAAQA,QAAM,SACZ,oBAAuB,MAAvB,OAA8B,EAGxC,WAA2B,EAAgB,OACrCC,uCACa,GACR,IAEF,cAkBoB,EAAqB,EAAyB,MACnE,GAAMC,aAAiC,MACvC,EAASC,WAAS,IAAM,EAAa,iCAEvB,GAAQ,KAAK,GAAQ,GACnC,MAAQ,kBAGF,IAAM,CACZ,EAAO,OAAS,EAAI,SAClB,MAAM,YAAY,EAAO,WAIf,IAAM,CAClB,EAAI,SACF,MAAM,YAIP"}
package/dist/vue.es.js ADDED
@@ -0,0 +1,42 @@
1
+ import { unref, shallowRef, computed, watchEffect, getCurrentScope, onScopeDispose } from "vue-demi";
2
+ import { createWhiteboardApp } from "./index.es.js";
3
+ import "@netless/window-manager";
4
+ import "white-web-sdk";
5
+ import "i18next";
6
+ import "react";
7
+ import "react-dom";
8
+ import "clsx";
9
+ import "@tippyjs/react";
10
+ import "rc-slider";
11
+ function unrefElement(elRef) {
12
+ var _a;
13
+ const plain = unref(elRef);
14
+ return (_a = plain == null ? void 0 : plain.$el) != null ? _a : plain;
15
+ }
16
+ function tryOnScopeDispose(fn) {
17
+ if (getCurrentScope()) {
18
+ onScopeDispose(fn);
19
+ return true;
20
+ }
21
+ return false;
22
+ }
23
+ function useFastboard(el, config) {
24
+ const app = shallowRef(null);
25
+ const target = computed(() => unrefElement(el));
26
+ createWhiteboardApp(config).then((app_) => {
27
+ app.value = app_;
28
+ });
29
+ watchEffect(() => {
30
+ if (target.value && app.value) {
31
+ app.value.bindElement(target.value);
32
+ }
33
+ });
34
+ tryOnScopeDispose(() => {
35
+ if (app.value) {
36
+ app.value.dispose();
37
+ }
38
+ });
39
+ return app;
40
+ }
41
+ export { unrefElement, useFastboard };
42
+ //# sourceMappingURL=vue.es.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vue.es.js","sources":["../src/vue.ts"],"sourcesContent":["import type { ComponentPublicInstance, Ref } from \"vue-demi\";\nimport {\n computed,\n getCurrentScope,\n onScopeDispose,\n shallowRef,\n unref,\n watchEffect,\n} from \"vue-demi\";\nimport { createWhiteboardApp } from \"./index\";\n\nimport type { WhiteboardApp, WhiteboardAppConfig } from \"./WhiteboardApp\";\n\nexport type FastBoardConfig = WhiteboardAppConfig;\n\nexport type MaybeRef<T> = T | Ref<T>;\nexport type VueInstance = ComponentPublicInstance;\nexport type MaybeElementRef = MaybeRef<\n HTMLElement | SVGElement | VueInstance | undefined | null\n>;\n\n/**\n * Get the dom element of a ref of element or Vue component instance\n */\nexport function unrefElement(elRef: MaybeElementRef) {\n const plain = unref(elRef);\n return (plain as VueInstance)?.$el ?? plain;\n}\n\nfunction tryOnScopeDispose(fn: () => void) {\n if (getCurrentScope()) {\n onScopeDispose(fn);\n return true;\n }\n return false;\n}\n\n/**\n * @example\n * ```vue\n * <script setup>\n * const el = ref(null)\n * const app = useFastboard(el, { sdkConfig, joinRoom })\n * if (app.value) {\n * app.value.insertDocs({...})\n * }\n * </script>\n * <template>\n * <div style=\"width: 100%; height: 100%\" ref=\"el\"></div>\n * </template>\n * ```\n */\nexport function useFastboard(el: MaybeElementRef, config: FastBoardConfig) {\n const app = shallowRef<WhiteboardApp | null>(null);\n const target = computed(() => unrefElement(el));\n\n createWhiteboardApp(config).then(app_ => {\n app.value = app_;\n });\n\n watchEffect(() => {\n if (target.value && app.value) {\n app.value.bindElement(target.value);\n }\n });\n\n tryOnScopeDispose(() => {\n if (app.value) {\n app.value.dispose();\n }\n });\n\n return app;\n}\n"],"names":[],"mappings":";;;;;;;;;;sBAwB6B,OAAwB;;QAC7C,QAAQ,MAAM;SACZ,qCAAuB,QAAvB,YAA8B;AAAA;AAGxC,2BAA2B,IAAgB;MACrC,mBAAmB;mBACN;WACR;AAAA;SAEF;AAAA;sBAkBoB,IAAqB,QAAyB;QACnE,MAAM,WAAiC;QACvC,SAAS,SAAS,MAAM,aAAa;sBAEvB,QAAQ,KAAK,UAAQ;QACnC,QAAQ;AAAA;cAGF,MAAM;QACZ,OAAO,SAAS,IAAI,OAAO;UACzB,MAAM,YAAY,OAAO;AAAA;AAAA;oBAIf,MAAM;QAClB,IAAI,OAAO;UACT,MAAM;AAAA;AAAA;SAIP;AAAA;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netless/fastboard",
3
- "version": "0.0.1",
3
+ "version": "0.0.5",
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",
@@ -14,6 +14,14 @@
14
14
  "default": "./dist/index.es.js",
15
15
  "types": "./src/index.ts"
16
16
  },
17
+ "./vue": {
18
+ "node": {
19
+ "import": "./dist/vue.es.js",
20
+ "require": "./dist/vue.cjs.js"
21
+ },
22
+ "default": "./dist/vue.es.js",
23
+ "types": "./src/vue.ts"
24
+ },
17
25
  "./package.json": "./package.json"
18
26
  },
19
27
  "files": [
@@ -37,8 +45,11 @@
37
45
  "@netless/app-monaco": "*",
38
46
  "@netless/app-slide": "*",
39
47
  "@netless/window-manager": "*",
48
+ "@vue/composition-api": "*",
40
49
  "react": "*",
41
50
  "react-dom": "*",
51
+ "svelte": "*",
52
+ "vue": "*",
42
53
  "white-web-sdk": "*"
43
54
  },
44
55
  "peerDependenciesMeta": {
@@ -53,51 +64,24 @@
53
64
  },
54
65
  "@netless/app-geogebra": {
55
66
  "optional": true
67
+ },
68
+ "@vue/composition-api": {
69
+ "optional": true
70
+ },
71
+ "svelte": {
72
+ "optional": true
73
+ },
74
+ "vue": {
75
+ "optional": true
56
76
  }
57
77
  },
58
78
  "dependencies": {
59
79
  "@tippyjs/react": "^4.2.6",
60
80
  "clsx": "^1.1.1",
61
- "i18next": "^21.6.3",
81
+ "i18next": "^21.6.5",
62
82
  "rc-slider": "^9.7.5",
63
- "tippy.js": "^6.3.7"
64
- },
65
- "devDependencies": {
66
- "@hyrious/esbuild-dev": "^0.7.1",
67
- "@hyrious/rimraf": "^0.1.0",
68
- "@netless/app-countdown": "^0.0.1",
69
- "@netless/app-geogebra": "^0.0.2",
70
- "@netless/app-monaco": "^0.1.12",
71
- "@netless/app-slide": "^0.0.52",
72
- "@netless/window-manager": "^0.3.16",
73
- "@types/node": "^17.0.4",
74
- "@types/react": "^17.0.38",
75
- "@types/react-dom": "^17.0.11",
76
- "@typescript-eslint/eslint-plugin": "^5.8.0",
77
- "@typescript-eslint/parser": "^5.8.0",
78
- "@vitejs/plugin-react": "^1.1.3",
79
- "esbuild": "^0.14.8",
80
- "eslint": "^8.5.0",
81
- "eslint-define-config": "^1.2.0",
82
- "eslint-plugin-react-hooks": "^4.3.0",
83
- "prettier": "2.5.1",
84
- "react": "^17.0.2",
85
- "react-dom": "^17.0.2",
86
- "rollup": "^2.61.1",
87
- "rollup-plugin-visualizer": "^5.5.2",
88
- "sass": "^1.45.1",
89
- "type-fest": "^2.8.0",
90
- "typescript": "^4.5.4",
91
- "vite": "^2.7.6",
92
- "white-web-sdk": "^2.15.13"
93
- },
94
- "scripts": {
95
- "preinstall": "npx only-allow pnpm",
96
- "format": "prettier --write .",
97
- "lint": "eslint --ext .ts,.tsx .",
98
- "dev": "vite dev --host",
99
- "build": "vite build",
100
- "build:dev": "vite build --mode development"
83
+ "tippy.js": "^6.3.7",
84
+ "vue-demi": "^0.12.1"
101
85
  },
102
- "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\n```\n\n```bash\nyarn add @netless/fastboard\n```\n\n```bash\npnpm add @netless/fastboard\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 = 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```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"
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"
103
87
  }
@@ -10,18 +10,54 @@ export type { WhiteboardAppConfig, InsertDocsParams };
10
10
  export class WhiteboardApp {
11
11
  private readonly _instance: Instance;
12
12
 
13
+ private _target: HTMLElement | null = null;
14
+ public get target(): HTMLElement | null {
15
+ return this._target;
16
+ }
17
+
13
18
  constructor(readonly config: WhiteboardAppConfig) {
14
19
  this._instance = new Instance(config);
15
20
  }
16
21
 
22
+ get room() {
23
+ return this._instance.room;
24
+ }
25
+
26
+ get manager() {
27
+ return this._instance.manager;
28
+ }
29
+
30
+ get sdk() {
31
+ return this._instance.sdk;
32
+ }
33
+
34
+ get i18n() {
35
+ return this._instance.i18n;
36
+ }
37
+
38
+ public bindElement(target?: HTMLElement | null) {
39
+ this._target = target || null;
40
+ this._instance.target = target || null;
41
+ }
42
+
17
43
  public insertDocs(params: InsertDocsParams) {
18
44
  return this._instance.insertDocs(params);
19
45
  }
20
46
 
47
+ public insertCodeEditor() {
48
+ return this._instance.insertCodeEditor();
49
+ }
50
+
51
+ public insertGeoGebra() {
52
+ return this._instance.insertGeoGebra();
53
+ }
54
+
55
+ public insertCountdown() {
56
+ return this._instance.insertCountdown();
57
+ }
58
+
21
59
  public changeLanguage(language: Language) {
22
- return this._instance.changeLanguage(language)?.finally(() => {
23
- this._instance.forceUpdate();
24
- });
60
+ return this._instance.changeLanguage(language);
25
61
  }
26
62
 
27
63
  public dispose() {
@@ -1,4 +1,4 @@
1
- import { applyStyles } from "../helpers";
1
+ import { applyStyles } from "../internal";
2
2
  import style from "../style.scss?inline";
3
3
 
4
4
  const newEl = applyStyles(style);
@@ -0,0 +1,145 @@
1
+ $name: "fastboard-player-control";
2
+
3
+ .#{$name} {
4
+ width: 100%;
5
+ display: inline-flex;
6
+ align-items: center;
7
+ gap: 4px;
8
+ padding: 4px;
9
+ border-radius: 4px;
10
+ backdrop-filter: blur(2px);
11
+ -webkit-backdrop-filter: blur(2px);
12
+
13
+ &.auto-hide {
14
+ opacity: 0;
15
+ transition: opacity 0.2s;
16
+
17
+ &:hover {
18
+ opacity: 1;
19
+ }
20
+ }
21
+
22
+ .rc-slider-disabled {
23
+ background: transparent;
24
+ opacity: 0.5;
25
+ }
26
+
27
+ .rc-slider-rail,
28
+ .rc-slider-track {
29
+ height: 2px;
30
+ }
31
+
32
+ .tippy-content {
33
+ padding: 8px;
34
+ }
35
+ .tippy-box {
36
+ border: 1px solid rgba(0, 0, 0, 0.15);
37
+ background-color: rgba($color: #333, $alpha: 0.95);
38
+ backdrop-filter: blur(2px);
39
+ -webkit-backdrop-filter: blur(2px);
40
+ }
41
+ .tippy-box[data-theme~="light"] {
42
+ background-color: rgba($color: #fff, $alpha: 0.95);
43
+ box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.25);
44
+ }
45
+
46
+ &.light {
47
+ color: #333;
48
+ background-color: rgba($color: #fff, $alpha: 0.85);
49
+ border: 1px solid rgba(0, 0, 0, 0.15);
50
+ }
51
+
52
+ &.dark {
53
+ color: #ddd;
54
+ background-color: rgba($color: #333, $alpha: 0.85);
55
+ border: 1px solid rgba(0, 0, 0, 0.45);
56
+ }
57
+ }
58
+
59
+ .#{$name}-btn {
60
+ appearance: none;
61
+ cursor: pointer;
62
+ margin: 0;
63
+ border: 0;
64
+ padding: 0;
65
+ min-width: 24px;
66
+ height: 24px;
67
+ background-color: transparent;
68
+ border-radius: 4px;
69
+ font-size: 24px;
70
+ line-height: 1;
71
+ display: inline-flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+
75
+ svg,
76
+ img {
77
+ width: 1em;
78
+ height: 1em;
79
+ }
80
+
81
+ &:disabled {
82
+ opacity: 0.5;
83
+ cursor: not-allowed;
84
+ }
85
+
86
+ &.light:not(:disabled):hover {
87
+ background-color: rgba(51, 129, 255, 0.1);
88
+ }
89
+
90
+ &.dark:not(:disabled):hover {
91
+ background-color: rgba(51, 129, 255, 0.25);
92
+ }
93
+
94
+ &.loading {
95
+ animation: fastboard-player-control-rotate 0.5s linear infinite;
96
+ }
97
+
98
+ @keyframes fastboard-player-control-rotate {
99
+ 100% {
100
+ transform: rotate(360deg);
101
+ }
102
+ }
103
+ }
104
+
105
+ .#{$name}-panel {
106
+ padding: 0;
107
+ display: flex;
108
+ flex-flow: column nowrap;
109
+ align-items: stretch;
110
+ gap: 4px;
111
+
112
+ .#{$name}-btn {
113
+ width: initial;
114
+ height: initial;
115
+ user-select: none;
116
+ font-size: 12px;
117
+ padding: 4px;
118
+ justify-content: flex-end;
119
+
120
+ &.active {
121
+ color: #3381ff;
122
+ }
123
+ }
124
+ }
125
+
126
+ .#{$name}-slider {
127
+ width: 100%;
128
+ padding: 0 7px;
129
+
130
+ &.loading {
131
+ cursor: not-allowed;
132
+ }
133
+ }
134
+
135
+ .#{$name}-slash {
136
+ opacity: 0.6;
137
+ }
138
+
139
+ .#{$name}-current,
140
+ .#{$name}-slash,
141
+ .#{$name}-total,
142
+ .#{$name}-speed-text {
143
+ font-size: 12px;
144
+ font-variant-numeric: tabular-nums;
145
+ }
@@ -0,0 +1,158 @@
1
+ import type { Player } from "white-web-sdk";
2
+ import type { CommonProps, GenericIcon } from "../../types";
3
+
4
+ import clsx from "clsx";
5
+ import React, { useEffect, useState } from "react";
6
+ import RcSlider from "rc-slider";
7
+ import { PlayerPhase } from "white-web-sdk";
8
+ import { usePlayer } from "./hooks";
9
+ import { Icon } from "../../icons";
10
+ import { themes, TopOffset } from "../../theme";
11
+ import { Icons } from "./icons";
12
+ import Tippy from "@tippyjs/react";
13
+ import { Button } from "./components/Button";
14
+
15
+ export type PlayerControlProps = {
16
+ autoHide?: boolean;
17
+ player?: Player;
18
+ } & Omit<CommonProps, "room"> &
19
+ GenericIcon<"play" | "pause" | "loading">;
20
+
21
+ export const name = "fastboard-player-control";
22
+
23
+ export function PlayerControl({
24
+ autoHide = false,
25
+ player: player_,
26
+ theme = "light",
27
+ i18n,
28
+ ...icons
29
+ }: PlayerControlProps) {
30
+ const [currentTime, setCurrentTime] = useState(0);
31
+ const player = usePlayer(player_);
32
+
33
+ useEffect(() => {
34
+ setCurrentTime(player.currentTime);
35
+ }, [player.currentTime]);
36
+
37
+ useEffect(() => {
38
+ if (player.currentTime !== currentTime) {
39
+ player.seekToProgressTime(currentTime);
40
+ }
41
+ // eslint-disable-next-line react-hooks/exhaustive-deps
42
+ }, [currentTime]);
43
+
44
+ const isLoading =
45
+ player.phase === PlayerPhase.WaitingFirstFrame ||
46
+ player.phase === PlayerPhase.Buffering;
47
+ const isPlaying = player.phase === PlayerPhase.Playing;
48
+
49
+ const { activeColor } = themes[theme];
50
+
51
+ return (
52
+ <div className={clsx(name, theme, { "auto-hide": autoHide })}>
53
+ <button
54
+ className={clsx(
55
+ `${name}-btn`,
56
+ isLoading ? "loading" : isPlaying ? "pause" : "play",
57
+ theme
58
+ )}
59
+ disabled={isLoading}
60
+ onClick={player.togglePlay}
61
+ >
62
+ <Icon
63
+ fallback={
64
+ isLoading ? (
65
+ <Icons.Loading theme={theme} />
66
+ ) : isPlaying ? (
67
+ <Icons.Pause theme={theme} />
68
+ ) : (
69
+ <Icons.Play theme={theme} />
70
+ )
71
+ }
72
+ src={
73
+ isLoading
74
+ ? icons.loadingIcon
75
+ : isPlaying
76
+ ? icons.pauseIcon
77
+ : icons.playIcon
78
+ }
79
+ alt={isLoading ? "[loading]" : isPlaying ? "[pause]" : "[play]"}
80
+ />
81
+ </button>
82
+ <span className={clsx(`${name}-slider`, { loading: isLoading }, theme)}>
83
+ <RcSlider
84
+ disabled={isLoading}
85
+ trackStyle={{ background: activeColor }}
86
+ handleStyle={{ border: `1px solid ${activeColor}` }}
87
+ value={currentTime}
88
+ onChange={setCurrentTime}
89
+ min={0}
90
+ max={player.totalTime}
91
+ step={100}
92
+ />
93
+ </span>
94
+ <span className={clsx(`${name}-current`, theme)}>
95
+ {renderTime(player.currentTime)}
96
+ </span>
97
+ <span className={clsx(`${name}-slash`, theme)}>/</span>
98
+ <span className={clsx(`${name}-total`, theme)}>
99
+ {renderTime(player.totalTime)}
100
+ </span>
101
+ <span className={`${name}-btn-interactive`}>
102
+ <Tippy
103
+ className="fastboard-tip"
104
+ content={renderSpeeds(player)}
105
+ theme={theme}
106
+ placement="top-end"
107
+ trigger="click"
108
+ offset={TopOffset}
109
+ arrow={false}
110
+ interactive
111
+ >
112
+ <Button content={i18n?.t("speed")} theme={theme} disabled={isLoading}>
113
+ <span className={clsx(`${name}-speed-text`, theme)}>
114
+ {player.speed}x
115
+ </span>
116
+ </Button>
117
+ </Tippy>
118
+ </span>
119
+ </div>
120
+ );
121
+ }
122
+
123
+ function renderTime(ms: number) {
124
+ let seconds = ms / 1000;
125
+ const minutes = Math.floor(seconds / 60);
126
+ seconds = Math.floor(seconds) % 60;
127
+
128
+ return (
129
+ `${String(minutes).padStart(2, "0")}` +
130
+ `:${String(seconds).padStart(2, "0")}`
131
+ );
132
+ }
133
+
134
+ const Speeds = [2.0, 1.5, 1.25, 1.0, 0.75, 0.5];
135
+
136
+ function renderSpeeds({
137
+ speed: current,
138
+ setSpeed,
139
+ }: {
140
+ speed: number;
141
+ setSpeed: (speed: number) => void;
142
+ }) {
143
+ return (
144
+ <div className={clsx(`${name}-panel`, "speed")}>
145
+ {Speeds.map(speed => (
146
+ <button
147
+ className={clsx(`${name}-btn`, "speed", {
148
+ active: speed === current,
149
+ })}
150
+ key={speed}
151
+ onClick={() => setSpeed(speed)}
152
+ >
153
+ {speed}x
154
+ </button>
155
+ ))}
156
+ </div>
157
+ );
158
+ }
@@ -0,0 +1,55 @@
1
+ import type { Placement } from "tippy.js";
2
+ import type { Theme } from "../../../types";
3
+
4
+ import clsx from "clsx";
5
+ import React, { forwardRef, type PropsWithChildren } from "react";
6
+ import Tippy from "@tippyjs/react";
7
+
8
+ import { TopOffset } from "../../../theme";
9
+
10
+ type ButtonProps = PropsWithChildren<{
11
+ theme: Theme;
12
+ content: React.ReactNode;
13
+ disabled?: boolean;
14
+ active?: boolean;
15
+ onClick?: () => void;
16
+ interactive?: boolean;
17
+ placement?: Placement;
18
+ }>;
19
+
20
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
21
+ (props, ref) => {
22
+ const {
23
+ theme,
24
+ content,
25
+ disabled,
26
+ active,
27
+ onClick,
28
+ interactive,
29
+ placement = "top",
30
+ children,
31
+ } = props;
32
+
33
+ return (
34
+ <Tippy
35
+ className="fastboard-tip"
36
+ content={content}
37
+ interactive={interactive}
38
+ theme={theme}
39
+ disabled={disabled}
40
+ placement={placement}
41
+ offset={TopOffset}
42
+ duration={300}
43
+ >
44
+ <button
45
+ ref={ref}
46
+ className={clsx("fastboard-player-control-btn", theme, { active })}
47
+ onClick={onClick}
48
+ disabled={disabled}
49
+ >
50
+ {children}
51
+ </button>
52
+ </Tippy>
53
+ );
54
+ }
55
+ );