@netless/fastboard 0.0.7 → 0.0.11

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 (49) hide show
  1. package/README.md +21 -19
  2. package/dist/index.cjs.js +4 -4
  3. package/dist/index.cjs.js.map +1 -1
  4. package/dist/index.es.js +678 -410
  5. package/dist/index.es.js.map +1 -1
  6. package/dist/svelte.cjs.js +1 -1
  7. package/dist/svelte.cjs.js.map +1 -1
  8. package/dist/svelte.es.js +1 -0
  9. package/dist/svelte.es.js.map +1 -1
  10. package/dist/vue.cjs.js +1 -1
  11. package/dist/vue.cjs.js.map +1 -1
  12. package/dist/vue.es.js +1 -0
  13. package/dist/vue.es.js.map +1 -1
  14. package/package.json +11 -2
  15. package/src/WhiteboardApp.ts +91 -20
  16. package/src/components/{PageControl.scss → PageControl/PageControl.scss} +0 -0
  17. package/src/components/PageControl/PageControl.tsx +110 -0
  18. package/src/components/PageControl/hooks.ts +70 -0
  19. package/src/components/PageControl/index.ts +2 -0
  20. package/src/components/PlayerControl/PlayerControl.tsx +7 -8
  21. package/src/components/PlayerControl/hooks.ts +3 -10
  22. package/src/components/PlayerControl/index.ts +1 -0
  23. package/src/components/{RedoUndo.scss → RedoUndo/RedoUndo.scss} +0 -0
  24. package/src/components/{RedoUndo.tsx → RedoUndo/RedoUndo.tsx} +13 -29
  25. package/src/components/RedoUndo/hooks.ts +50 -0
  26. package/src/components/RedoUndo/index.ts +2 -0
  27. package/src/components/Root.tsx +10 -6
  28. package/src/components/Toolbar/Content.tsx +4 -3
  29. package/src/components/Toolbar/Toolbar.scss +35 -1
  30. package/src/components/Toolbar/Toolbar.tsx +78 -28
  31. package/src/components/Toolbar/components/Mask.tsx +44 -0
  32. package/src/components/Toolbar/components/assets/collapsed.png +0 -0
  33. package/src/components/Toolbar/components/assets/expanded.png +0 -0
  34. package/src/components/Toolbar/hooks.ts +28 -29
  35. package/src/components/Toolbar/index.ts +1 -0
  36. package/src/components/{ZoomControl.scss → ZoomControl/ZoomControl.scss} +0 -0
  37. package/src/components/ZoomControl/ZoomControl.tsx +109 -0
  38. package/src/components/ZoomControl/hooks.ts +111 -0
  39. package/src/components/ZoomControl/index.ts +2 -0
  40. package/src/components/hooks.ts +80 -0
  41. package/src/index.ts +20 -5
  42. package/src/internal/Instance.tsx +31 -7
  43. package/src/internal/helpers.ts +44 -0
  44. package/src/internal/hooks.ts +9 -0
  45. package/src/react.tsx +52 -0
  46. package/src/style.scss +9 -3
  47. package/src/components/PageControl.tsx +0 -181
  48. package/src/components/ZoomControl.tsx +0 -221
  49. package/src/hooks.ts +0 -53
@@ -0,0 +1,80 @@
1
+ import type { Room, WindowManager } from "@netless/window-manager";
2
+ import { BuiltinApps } from "@netless/window-manager";
3
+ import { useEffect, useState } from "react";
4
+
5
+ export function useWritable(room?: Room | null) {
6
+ const [writable, setWritable] = useState(false);
7
+
8
+ useEffect(() => {
9
+ if (room) {
10
+ setWritable(room.isWritable);
11
+ room.isWritable && (room.disableSerialization = false);
12
+
13
+ const updateWritable = () => setWritable(room.isWritable);
14
+ room.callbacks.on("onEnableWriteNowChanged", updateWritable);
15
+
16
+ return () => {
17
+ room.callbacks.off("onEnableWriteNowChanged", updateWritable);
18
+ };
19
+ }
20
+ }, [room]);
21
+
22
+ return writable;
23
+ }
24
+
25
+ export type BoxState = "normal" | "minimized" | "maximized";
26
+
27
+ export function useBoxState(manager?: WindowManager | null) {
28
+ const [boxState, setBoxState] = useState<BoxState | undefined>();
29
+
30
+ useEffect(() => {
31
+ if (manager) {
32
+ setBoxState(manager.boxState);
33
+
34
+ manager.emitter.on("boxStateChange", setBoxState);
35
+
36
+ return () => {
37
+ manager.emitter.off("boxStateChange", setBoxState);
38
+ };
39
+ }
40
+ }, [manager]);
41
+
42
+ return boxState;
43
+ }
44
+
45
+ export function useFocusedApp(manager?: WindowManager | null) {
46
+ const [focused, setFocused] = useState<string | undefined>();
47
+
48
+ useEffect(() => {
49
+ if (manager) {
50
+ setFocused(manager.focused);
51
+
52
+ manager.emitter.on("focusedChange", setFocused);
53
+
54
+ return () => {
55
+ manager.emitter.off("focusedChange", setFocused);
56
+ };
57
+ }
58
+ }, [manager]);
59
+
60
+ return focused;
61
+ }
62
+
63
+ export function useMaximized(manager?: WindowManager | null) {
64
+ return useBoxState(manager) === "maximized";
65
+ }
66
+
67
+ export function useHideControls(manager?: WindowManager | null) {
68
+ const maximized = useMaximized(manager);
69
+ const focusedApp = useFocusedApp(manager);
70
+
71
+ if (maximized) {
72
+ if (Object.values(BuiltinApps).some(kind => focusedApp?.includes(kind))) {
73
+ return "toolbar-only";
74
+ } else {
75
+ return true;
76
+ }
77
+ }
78
+
79
+ return false;
80
+ }
package/src/index.ts CHANGED
@@ -6,16 +6,31 @@ import "./behaviors/style";
6
6
  import { WhiteboardApp, type WhiteboardAppConfig } from "./WhiteboardApp";
7
7
 
8
8
  export { version } from "../package.json";
9
- export { PageControl, type PageControlProps } from "./components/PageControl";
10
- export { RedoUndo, type RedoUndoProps } from "./components/RedoUndo";
11
- export { Toolbar, type ToolbarProps } from "./components/Toolbar";
12
- export { ZoomControl, type ZoomControlProps } from "./components/ZoomControl";
9
+ export {
10
+ PageControl,
11
+ usePageControl,
12
+ type PageControlProps,
13
+ } from "./components/PageControl";
14
+ export {
15
+ RedoUndo,
16
+ useRedoUndo,
17
+ type RedoUndoProps,
18
+ } from "./components/RedoUndo";
19
+ export { Toolbar, useToolbar, type ToolbarProps } from "./components/Toolbar";
20
+ export {
21
+ ZoomControl,
22
+ useZoomControl,
23
+ type ZoomControlProps,
24
+ } from "./components/ZoomControl";
13
25
  export {
14
26
  PlayerControl,
27
+ usePlayerControl,
15
28
  type PlayerControlProps,
16
29
  } from "./components/PlayerControl";
30
+ export {};
31
+
17
32
  export * from "./WhiteboardApp";
18
- export * from "./hooks";
33
+ export * from "./react";
19
34
 
20
35
  export const register = WindowManager.register.bind(WindowManager);
21
36
 
@@ -6,6 +6,7 @@ import type { i18n } from "i18next";
6
6
 
7
7
  import React, { createContext, useContext } from "react";
8
8
  import ReactDOM from "react-dom";
9
+ import { BuiltinApps } from "@netless/window-manager";
9
10
 
10
11
  import { Root } from "../components/Root";
11
12
  import { mountWhiteboard } from "./mount-whiteboard";
@@ -18,6 +19,11 @@ export interface AcceptParams {
18
19
  readonly i18n: i18n;
19
20
  }
20
21
 
22
+ export interface InsertMediaParams {
23
+ title: string;
24
+ src: string;
25
+ }
26
+
21
27
  export interface InsertDocsStatic {
22
28
  readonly fileType: "pdf" | "ppt";
23
29
  readonly scenePath: string;
@@ -31,6 +37,7 @@ export interface InsertDocsDynamic {
31
37
  readonly taskId: string;
32
38
  readonly title?: string;
33
39
  readonly url?: string;
40
+ readonly scenes?: SceneDefinition[];
34
41
  }
35
42
 
36
43
  export type InsertDocsParams = InsertDocsStatic | InsertDocsDynamic;
@@ -110,16 +117,22 @@ export class Instance {
110
117
  target: HTMLElement | null = null;
111
118
  collector: HTMLElement | null = null;
112
119
 
113
- bindElement(target: HTMLElement | null, collector: HTMLElement | null) {
114
- if (this.target && target) {
120
+ bindElement(target: HTMLElement | null) {
121
+ if (this.target && this.target !== target) {
115
122
  ReactDOM.unmountComponentAtNode(this.target);
116
123
  }
117
124
  this.target = target;
118
- this.collector = collector;
119
125
  this.forceUpdate();
120
126
  }
121
127
 
122
- updateLayout(layout: Layout) {
128
+ bindCollector(collector: HTMLElement | null) {
129
+ this.collector = collector;
130
+ if (this.manager && collector) {
131
+ this.manager.bindCollectorContainer(collector);
132
+ }
133
+ }
134
+
135
+ updateLayout(layout: Layout | undefined) {
123
136
  this.config.layout = layout;
124
137
  this.forceUpdate();
125
138
  }
@@ -154,10 +167,9 @@ export class Instance {
154
167
  if (!this.manager) {
155
168
  throw new Error(`[WhiteboardApp] mounted, but not found window manager`);
156
169
  }
170
+ this.manager.bindContainer(node);
157
171
  if (this.collector) {
158
- this.manager.bindContainer(node, this.collector);
159
- } else {
160
- this.manager.bindContainer(node);
172
+ this.manager.bindCollectorContainer(this.collector);
161
173
  }
162
174
  }
163
175
 
@@ -198,6 +210,7 @@ export class Instance {
198
210
  options: {
199
211
  scenePath: params.scenePath,
200
212
  title: params.title,
213
+ scenes: params.scenes,
201
214
  },
202
215
  attributes: {
203
216
  taskId: params.taskId,
@@ -237,6 +250,17 @@ export class Instance {
237
250
  });
238
251
  }
239
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
+
240
264
  async changeLanguage(language: Language) {
241
265
  try {
242
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
+ }
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
@@ -3,12 +3,18 @@
3
3
  @import "tippy.js/themes/light.css";
4
4
  @import "rc-slider/assets/index.css";
5
5
  @import "./components/Root.scss";
6
- @import "./components/RedoUndo.scss";
7
- @import "./components/PageControl.scss";
8
- @import "./components/ZoomControl.scss";
6
+ @import "./components/RedoUndo/RedoUndo.scss";
7
+ @import "./components/PageControl/PageControl.scss";
8
+ @import "./components/ZoomControl/ZoomControl.scss";
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);
@@ -1,181 +0,0 @@
1
- import type { RoomState, ViewVisionMode } from "white-web-sdk";
2
- import type { CommonProps, GenericIcon } from "../types";
3
-
4
- import clsx from "clsx";
5
- import React, { useCallback, useEffect, useState } from "react";
6
- import Tippy from "@tippyjs/react";
7
-
8
- import { TopOffset } from "../theme";
9
- import { Icon } from "../icons";
10
- import { FilePlus } from "../icons/FilePlus";
11
- import { ChevronLeft } from "../icons/ChevronLeft";
12
- import { ChevronRight } from "../icons/ChevronRight";
13
-
14
- export const name = "fastboard-page-control";
15
-
16
- export type PageControlProps = CommonProps &
17
- GenericIcon<"add" | "prev" | "next">;
18
-
19
- export function PageControl({
20
- room,
21
- manager,
22
- theme = "light",
23
- addIcon,
24
- addIconDisable,
25
- prevIcon,
26
- prevIconDisable,
27
- nextIcon,
28
- nextIconDisable,
29
- i18n,
30
- }: PageControlProps) {
31
- const [writable, setWritable] = useState(false);
32
- const [pageIndex, setPageIndex] = useState(0);
33
- const [pageCount, setPageCount] = useState(0);
34
-
35
- const addPage = useCallback(async () => {
36
- if (manager && room) {
37
- await manager.switchMainViewToWriter();
38
- const path = room.state.sceneState.contextPath;
39
- room.putScenes(path, [{}], pageIndex + 1);
40
- await manager.setMainViewSceneIndex(pageIndex + 1);
41
- } else if (!manager && room) {
42
- const path = room.state.sceneState.contextPath;
43
- room.putScenes(path, [{}], pageIndex + 1);
44
- room.setSceneIndex(pageIndex + 1);
45
- }
46
- }, [room, manager, pageIndex]);
47
-
48
- const prevPage = useCallback(() => {
49
- if (room?.isWritable) {
50
- if (manager) {
51
- manager.setMainViewSceneIndex(pageIndex - 1);
52
- } else {
53
- room.pptPreviousStep();
54
- }
55
- }
56
- }, [room, manager, pageIndex]);
57
-
58
- const nextPage = useCallback(() => {
59
- if (room?.isWritable) {
60
- if (manager) {
61
- manager.setMainViewSceneIndex(pageIndex + 1);
62
- } else {
63
- room.pptNextStep();
64
- }
65
- }
66
- }, [room, manager, pageIndex]);
67
-
68
- useEffect(() => {
69
- if (room) {
70
- setWritable(room.isWritable);
71
- setPageIndex(room.state.sceneState.index);
72
- setPageCount(room.state.sceneState.scenes.length);
73
- }
74
-
75
- const onRoomStateChanged = (modifyState: Partial<RoomState>) => {
76
- if (modifyState.sceneState) {
77
- setPageIndex(modifyState.sceneState.index);
78
- setPageCount(modifyState.sceneState.scenes.length);
79
- }
80
- };
81
-
82
- const onMainViewModeChanged = (mode: number) => {
83
- if (room && mode === (0 as ViewVisionMode.Writable)) {
84
- setPageIndex(room.state.sceneState.index);
85
- setPageCount(room.state.sceneState.scenes.length);
86
- }
87
- };
88
-
89
- const updateWritable = () => setWritable(room?.isWritable || false);
90
-
91
- if (room) {
92
- room.callbacks.on("onEnableWriteNowChanged", updateWritable);
93
- room.callbacks.on("onRoomStateChanged", onRoomStateChanged);
94
- manager?.callbacks.on("mainViewModeChange", onMainViewModeChanged);
95
- }
96
-
97
- return () => {
98
- if (room) {
99
- room.callbacks.off("onEnableWriteNowChanged", updateWritable);
100
- room.callbacks.off("onRoomStateChanged", onRoomStateChanged);
101
- manager?.callbacks.off("mainViewModeChange", onMainViewModeChanged);
102
- }
103
- };
104
- }, [room, manager]);
105
-
106
- const disabled = !writable;
107
-
108
- return (
109
- <div className={clsx(name, theme)}>
110
- {/* <span className={clsx(`${name}-cut-line`, theme)} />{" "} */}
111
- <Tippy
112
- className="fastboard-tip"
113
- content={i18n?.t("prevPage")}
114
- theme={theme}
115
- disabled={disabled}
116
- placement="top"
117
- duration={300}
118
- offset={TopOffset}
119
- >
120
- <button
121
- className={clsx(`${name}-btn`, "prev", theme)}
122
- disabled={disabled || pageIndex === 0}
123
- onClick={prevPage}
124
- >
125
- <Icon
126
- fallback={<ChevronLeft theme={theme} />}
127
- src={disabled ? prevIconDisable : prevIcon}
128
- alt="[prev]"
129
- />
130
- </button>
131
- </Tippy>
132
- <span className={clsx(`${name}-page`, theme)}>
133
- {pageCount === 0 ? "\u2026" : pageIndex + 1}
134
- </span>
135
- <span className={clsx(`${name}-slash`, theme)}>/</span>
136
- <span className={clsx(`${name}-page-count`, theme)}>{pageCount}</span>
137
- <Tippy
138
- className="fastboard-tip"
139
- content={i18n?.t("nextPage")}
140
- theme={theme}
141
- disabled={disabled}
142
- placement="top"
143
- duration={300}
144
- offset={TopOffset}
145
- >
146
- <button
147
- className={clsx(`${name}-btn`, "next", theme)}
148
- disabled={disabled || pageIndex === pageCount - 1}
149
- onClick={nextPage}
150
- >
151
- <Icon
152
- fallback={<ChevronRight theme={theme} />}
153
- src={disabled ? nextIconDisable : nextIcon}
154
- alt="[next]"
155
- />
156
- </button>
157
- </Tippy>
158
- <Tippy
159
- className="fastboard-tip"
160
- content={i18n?.t("addPage")}
161
- theme={theme}
162
- disabled={disabled}
163
- placement="top"
164
- duration={300}
165
- offset={TopOffset}
166
- >
167
- <button
168
- className={clsx(`${name}-btn`, "add", theme)}
169
- disabled={disabled}
170
- onClick={addPage}
171
- >
172
- <Icon
173
- fallback={<FilePlus theme={theme} />}
174
- src={disabled ? addIconDisable : addIcon}
175
- alt="[add]"
176
- />
177
- </button>
178
- </Tippy>
179
- </div>
180
- );
181
- }