@netless/fastboard-react 0.1.0

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 (163) hide show
  1. package/LICENSE.txt +21 -0
  2. package/dist/behaviors/style.d.ts +1 -0
  3. package/dist/components/Fastboard.d.ts +15 -0
  4. package/dist/components/PageControl/PageControl.d.ts +5 -0
  5. package/dist/components/PageControl/hooks.d.ts +9 -0
  6. package/dist/components/PageControl/index.d.ts +2 -0
  7. package/dist/components/PlayerControl/PlayerControl.d.ts +9 -0
  8. package/dist/components/PlayerControl/hooks.d.ts +11 -0
  9. package/dist/components/PlayerControl/icons/Loading.d.ts +3 -0
  10. package/dist/components/PlayerControl/icons/Pause.d.ts +3 -0
  11. package/dist/components/PlayerControl/icons/Play.d.ts +3 -0
  12. package/dist/components/PlayerControl/icons/index.d.ts +6 -0
  13. package/dist/components/PlayerControl/index.d.ts +2 -0
  14. package/dist/components/RedoUndo/RedoUndo.d.ts +5 -0
  15. package/dist/components/RedoUndo/hooks.d.ts +6 -0
  16. package/dist/components/RedoUndo/index.d.ts +2 -0
  17. package/dist/components/Toolbar/Content.d.ts +2 -0
  18. package/dist/components/Toolbar/Toolbar.d.ts +14 -0
  19. package/dist/components/Toolbar/components/ApplianceButtons.d.ts +7 -0
  20. package/dist/components/Toolbar/components/AppsButton.d.ts +6 -0
  21. package/dist/components/Toolbar/components/ColorBox.d.ts +2 -0
  22. package/dist/components/Toolbar/components/CutLine.d.ts +2 -0
  23. package/dist/components/Toolbar/components/Mask.d.ts +7 -0
  24. package/dist/components/Toolbar/components/PencilButton.d.ts +2 -0
  25. package/dist/components/Toolbar/components/ShapesButton.d.ts +3 -0
  26. package/dist/components/Toolbar/components/Slider.d.ts +2 -0
  27. package/dist/components/Toolbar/components/TextButton.d.ts +2 -0
  28. package/dist/components/Toolbar/components/UpDownButtons.d.ts +7 -0
  29. package/dist/components/Toolbar/const.d.ts +18 -0
  30. package/dist/components/Toolbar/hooks.d.ts +12 -0
  31. package/dist/components/Toolbar/icons/Apps.d.ts +3 -0
  32. package/dist/components/Toolbar/icons/Arrow.d.ts +3 -0
  33. package/dist/components/Toolbar/icons/Circle.d.ts +3 -0
  34. package/dist/components/Toolbar/icons/Clean.d.ts +3 -0
  35. package/dist/components/Toolbar/icons/Clicker.d.ts +3 -0
  36. package/dist/components/Toolbar/icons/Collapse.d.ts +3 -0
  37. package/dist/components/Toolbar/icons/Diamond.d.ts +3 -0
  38. package/dist/components/Toolbar/icons/Down.d.ts +3 -0
  39. package/dist/components/Toolbar/icons/Eraser.d.ts +3 -0
  40. package/dist/components/Toolbar/icons/Expand.d.ts +3 -0
  41. package/dist/components/Toolbar/icons/Line.d.ts +3 -0
  42. package/dist/components/Toolbar/icons/Pencil.d.ts +3 -0
  43. package/dist/components/Toolbar/icons/Rectangle.d.ts +3 -0
  44. package/dist/components/Toolbar/icons/Selector.d.ts +3 -0
  45. package/dist/components/Toolbar/icons/SpeechBalloon.d.ts +3 -0
  46. package/dist/components/Toolbar/icons/Star.d.ts +3 -0
  47. package/dist/components/Toolbar/icons/Text.d.ts +3 -0
  48. package/dist/components/Toolbar/icons/Triangle.d.ts +3 -0
  49. package/dist/components/Toolbar/icons/Up.d.ts +3 -0
  50. package/dist/components/Toolbar/icons/index.d.ts +22 -0
  51. package/dist/components/Toolbar/index.d.ts +2 -0
  52. package/dist/components/ZoomControl/ZoomControl.d.ts +5 -0
  53. package/dist/components/ZoomControl/hooks.d.ts +7 -0
  54. package/dist/components/ZoomControl/index.d.ts +2 -0
  55. package/dist/components/hooks.d.ts +13 -0
  56. package/dist/i18n/index.d.ts +12 -0
  57. package/dist/icons/ChevronLeft.d.ts +3 -0
  58. package/dist/icons/ChevronRight.d.ts +3 -0
  59. package/dist/icons/FilePlus.d.ts +3 -0
  60. package/dist/icons/Minus.d.ts +3 -0
  61. package/dist/icons/Plus.d.ts +3 -0
  62. package/dist/icons/Redo.d.ts +3 -0
  63. package/dist/icons/Reset.d.ts +3 -0
  64. package/dist/icons/Undo.d.ts +3 -0
  65. package/dist/icons/index.d.ts +7 -0
  66. package/dist/index.d.ts +9 -0
  67. package/dist/index.js +2116 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/index.mjs +2083 -0
  70. package/dist/index.mjs.map +1 -0
  71. package/dist/internal/helpers.d.ts +16 -0
  72. package/dist/internal/hooks.d.ts +3 -0
  73. package/dist/internal/index.d.ts +2 -0
  74. package/dist/theme.d.ts +16 -0
  75. package/dist/typings.d.ts +10 -0
  76. package/package.json +41 -0
  77. package/src/behaviors/style.ts +4 -0
  78. package/src/components/Fastboard.scss +41 -0
  79. package/src/components/Fastboard.tsx +97 -0
  80. package/src/components/PageControl/PageControl.scss +80 -0
  81. package/src/components/PageControl/PageControl.tsx +105 -0
  82. package/src/components/PageControl/hooks.ts +67 -0
  83. package/src/components/PageControl/index.ts +2 -0
  84. package/src/components/PlayerControl/PlayerControl.scss +145 -0
  85. package/src/components/PlayerControl/PlayerControl.tsx +131 -0
  86. package/src/components/PlayerControl/components/Button.tsx +45 -0
  87. package/src/components/PlayerControl/hooks.ts +88 -0
  88. package/src/components/PlayerControl/icons/Loading.tsx +13 -0
  89. package/src/components/PlayerControl/icons/Pause.tsx +13 -0
  90. package/src/components/PlayerControl/icons/Play.tsx +13 -0
  91. package/src/components/PlayerControl/icons/index.ts +10 -0
  92. package/src/components/PlayerControl/index.ts +2 -0
  93. package/src/components/RedoUndo/RedoUndo.scss +56 -0
  94. package/src/components/RedoUndo/RedoUndo.tsx +76 -0
  95. package/src/components/RedoUndo/hooks.ts +18 -0
  96. package/src/components/RedoUndo/index.ts +2 -0
  97. package/src/components/Toolbar/Content.tsx +74 -0
  98. package/src/components/Toolbar/Toolbar.scss +281 -0
  99. package/src/components/Toolbar/Toolbar.tsx +116 -0
  100. package/src/components/Toolbar/components/ApplianceButtons.tsx +108 -0
  101. package/src/components/Toolbar/components/AppsButton.tsx +101 -0
  102. package/src/components/Toolbar/components/Button.tsx +46 -0
  103. package/src/components/Toolbar/components/ColorBox.tsx +55 -0
  104. package/src/components/Toolbar/components/CutLine.tsx +8 -0
  105. package/src/components/Toolbar/components/Mask.tsx +44 -0
  106. package/src/components/Toolbar/components/PencilButton.tsx +66 -0
  107. package/src/components/Toolbar/components/ShapesButton.tsx +128 -0
  108. package/src/components/Toolbar/components/Slider.tsx +26 -0
  109. package/src/components/Toolbar/components/TextButton.tsx +62 -0
  110. package/src/components/Toolbar/components/UpDownButtons.tsx +49 -0
  111. package/src/components/Toolbar/components/assets/cocos.png +0 -0
  112. package/src/components/Toolbar/components/assets/collapsed.png +0 -0
  113. package/src/components/Toolbar/components/assets/countdown.png +0 -0
  114. package/src/components/Toolbar/components/assets/expanded.png +0 -0
  115. package/src/components/Toolbar/components/assets/geogebra.png +0 -0
  116. package/src/components/Toolbar/components/assets/vscode.png +0 -0
  117. package/src/components/Toolbar/const.ts +32 -0
  118. package/src/components/Toolbar/hooks.ts +68 -0
  119. package/src/components/Toolbar/icons/Apps.tsx +16 -0
  120. package/src/components/Toolbar/icons/Arrow.tsx +13 -0
  121. package/src/components/Toolbar/icons/Circle.tsx +13 -0
  122. package/src/components/Toolbar/icons/Clean.tsx +16 -0
  123. package/src/components/Toolbar/icons/Clicker.tsx +19 -0
  124. package/src/components/Toolbar/icons/Collapse.tsx +13 -0
  125. package/src/components/Toolbar/icons/Diamond.tsx +13 -0
  126. package/src/components/Toolbar/icons/Down.tsx +13 -0
  127. package/src/components/Toolbar/icons/Eraser.tsx +16 -0
  128. package/src/components/Toolbar/icons/Expand.tsx +13 -0
  129. package/src/components/Toolbar/icons/Line.tsx +13 -0
  130. package/src/components/Toolbar/icons/Pencil.tsx +16 -0
  131. package/src/components/Toolbar/icons/Rectangle.tsx +13 -0
  132. package/src/components/Toolbar/icons/Selector.tsx +13 -0
  133. package/src/components/Toolbar/icons/SpeechBalloon.tsx +17 -0
  134. package/src/components/Toolbar/icons/Star.tsx +17 -0
  135. package/src/components/Toolbar/icons/Text.tsx +13 -0
  136. package/src/components/Toolbar/icons/Triangle.tsx +13 -0
  137. package/src/components/Toolbar/icons/Up.tsx +13 -0
  138. package/src/components/Toolbar/icons/index.ts +42 -0
  139. package/src/components/Toolbar/index.ts +2 -0
  140. package/src/components/ZoomControl/ZoomControl.scss +80 -0
  141. package/src/components/ZoomControl/ZoomControl.tsx +94 -0
  142. package/src/components/ZoomControl/hooks.ts +52 -0
  143. package/src/components/ZoomControl/index.ts +2 -0
  144. package/src/components/hooks.ts +59 -0
  145. package/src/i18n/en.json +31 -0
  146. package/src/i18n/index.ts +29 -0
  147. package/src/i18n/zh-CN.json +32 -0
  148. package/src/icons/ChevronLeft.tsx +21 -0
  149. package/src/icons/ChevronRight.tsx +21 -0
  150. package/src/icons/FilePlus.tsx +18 -0
  151. package/src/icons/Minus.tsx +15 -0
  152. package/src/icons/Plus.tsx +15 -0
  153. package/src/icons/Redo.tsx +18 -0
  154. package/src/icons/Reset.tsx +19 -0
  155. package/src/icons/Undo.tsx +18 -0
  156. package/src/icons/index.tsx +11 -0
  157. package/src/index.ts +10 -0
  158. package/src/internal/helpers.ts +31 -0
  159. package/src/internal/hooks.ts +23 -0
  160. package/src/internal/index.ts +2 -0
  161. package/src/style.scss +29 -0
  162. package/src/theme.ts +36 -0
  163. package/src/typings.ts +15 -0
@@ -0,0 +1,105 @@
1
+ import type { CommonProps, GenericIcon } from "../../typings";
2
+
3
+ import Tippy from "@tippyjs/react";
4
+ import clsx from "clsx";
5
+ import React from "react";
6
+
7
+ import { useTranslation } from "../../i18n";
8
+ import { Icon } from "../../icons";
9
+ import { ChevronLeft } from "../../icons/ChevronLeft";
10
+ import { ChevronRight } from "../../icons/ChevronRight";
11
+ import { FilePlus } from "../../icons/FilePlus";
12
+ import { TopOffset } from "../../theme";
13
+ import { useFastboardApp, useTheme, useWritable } from "../hooks";
14
+ import { usePageControl } from "./hooks";
15
+
16
+ export const name = "fastboard-page-control";
17
+
18
+ export type PageControlProps = CommonProps & GenericIcon<"add" | "prev" | "next">;
19
+
20
+ export function PageControl({
21
+ theme,
22
+ addIcon,
23
+ addIconDisable,
24
+ prevIcon,
25
+ prevIconDisable,
26
+ nextIcon,
27
+ nextIconDisable,
28
+ }: PageControlProps) {
29
+ const app = useFastboardApp();
30
+
31
+ theme = useTheme(theme);
32
+ const { t } = useTranslation();
33
+
34
+ const writable = useWritable();
35
+ const { pageIndex, pageCount, ...actions } = usePageControl(app.room, app.manager);
36
+
37
+ const disabled = !writable;
38
+
39
+ return (
40
+ <div className={clsx(name, theme)}>
41
+ {/* <span className={clsx(`${name}-cut-line`, theme)} />{" "} */}
42
+ <Tippy
43
+ className="fastboard-tip"
44
+ content={t("prevPage")}
45
+ theme={theme}
46
+ disabled={disabled}
47
+ placement="top"
48
+ delay={[1000, 400]}
49
+ duration={300}
50
+ offset={TopOffset}
51
+ >
52
+ <button
53
+ className={clsx(`${name}-btn`, "prev", theme)}
54
+ disabled={disabled || pageIndex === 0}
55
+ onClick={actions.prevPage}
56
+ >
57
+ <Icon
58
+ fallback={<ChevronLeft theme={theme} />}
59
+ src={disabled ? prevIconDisable : prevIcon}
60
+ alt="[prev]"
61
+ />
62
+ </button>
63
+ </Tippy>
64
+ <span className={clsx(`${name}-page`, theme)}>{pageCount === 0 ? "\u2026" : pageIndex + 1}</span>
65
+ <span className={clsx(`${name}-slash`, theme)}>/</span>
66
+ <span className={clsx(`${name}-page-count`, theme)}>{pageCount}</span>
67
+ <Tippy
68
+ className="fastboard-tip"
69
+ content={t("nextPage")}
70
+ theme={theme}
71
+ disabled={disabled}
72
+ placement="top"
73
+ delay={[1000, 400]}
74
+ duration={300}
75
+ offset={TopOffset}
76
+ >
77
+ <button
78
+ className={clsx(`${name}-btn`, "next", theme)}
79
+ disabled={disabled || pageIndex === pageCount - 1}
80
+ onClick={actions.nextPage}
81
+ >
82
+ <Icon
83
+ fallback={<ChevronRight theme={theme} />}
84
+ src={disabled ? nextIconDisable : nextIcon}
85
+ alt="[next]"
86
+ />
87
+ </button>
88
+ </Tippy>
89
+ <Tippy
90
+ className="fastboard-tip"
91
+ content={t("addPage")}
92
+ theme={theme}
93
+ disabled={disabled}
94
+ placement="top"
95
+ delay={[1000, 400]}
96
+ duration={300}
97
+ offset={TopOffset}
98
+ >
99
+ <button className={clsx(`${name}-btn`, "add", theme)} disabled={disabled} onClick={actions.addPage}>
100
+ <Icon fallback={<FilePlus theme={theme} />} src={disabled ? addIconDisable : addIcon} alt="[add]" />
101
+ </button>
102
+ </Tippy>
103
+ </div>
104
+ );
105
+ }
@@ -0,0 +1,67 @@
1
+ import type { Room, RoomState } from "white-web-sdk";
2
+ import type { WindowManager } from "@netless/window-manager";
3
+ import { useCallback, useEffect, useState } from "react";
4
+
5
+ export function usePageControl(room?: Room | null, manager?: WindowManager | null) {
6
+ const [pageIndex, setPageIndex] = useState(0);
7
+ const [pageCount, setPageCount] = useState(0);
8
+
9
+ const addPage = useCallback(async () => {
10
+ if (manager && room) {
11
+ await manager.switchMainViewToWriter();
12
+ const path = room.state.sceneState.contextPath;
13
+ room.putScenes(path, [{}], pageIndex + 1);
14
+ await manager.setMainViewSceneIndex(pageIndex + 1);
15
+ } else if (!manager && room) {
16
+ const path = room.state.sceneState.contextPath;
17
+ room.putScenes(path, [{}], pageIndex + 1);
18
+ room.setSceneIndex(pageIndex + 1);
19
+ }
20
+ }, [room, manager, pageIndex]);
21
+
22
+ const prevPage = useCallback(() => {
23
+ if (manager) {
24
+ manager.setMainViewSceneIndex(pageIndex - 1);
25
+ } else if (room) {
26
+ room.pptPreviousStep();
27
+ }
28
+ }, [room, manager, pageIndex]);
29
+
30
+ const nextPage = useCallback(() => {
31
+ if (manager) {
32
+ manager.setMainViewSceneIndex(pageIndex + 1);
33
+ } else if (room) {
34
+ room.pptNextStep();
35
+ }
36
+ }, [room, manager, pageIndex]);
37
+
38
+ useEffect(() => {
39
+ if (room) {
40
+ setPageIndex(room.state.sceneState.index);
41
+ setPageCount(room.state.sceneState.scenes.length);
42
+
43
+ if (manager) {
44
+ manager.emitter.on("mainViewSceneIndexChange", setPageIndex);
45
+
46
+ return () => {
47
+ manager.emitter.off("mainViewSceneIndexChange", setPageIndex);
48
+ };
49
+ } else {
50
+ const onRoomStateChanged = (modifyState: Partial<RoomState>) => {
51
+ if (modifyState.sceneState) {
52
+ setPageIndex(modifyState.sceneState.index);
53
+ setPageCount(modifyState.sceneState.scenes.length);
54
+ }
55
+ };
56
+
57
+ room.callbacks.on("onRoomStateChanged", onRoomStateChanged);
58
+
59
+ return () => {
60
+ room.callbacks.off("onRoomStateChanged", onRoomStateChanged);
61
+ };
62
+ }
63
+ }
64
+ }, [room, manager]);
65
+
66
+ return { pageIndex, pageCount, prevPage, nextPage, addPage };
67
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./hooks";
2
+ export { PageControl, type PageControlProps } from "./PageControl";
@@ -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,131 @@
1
+ import type { Player } from "white-web-sdk";
2
+ import type { CommonProps, GenericIcon } from "../../typings";
3
+
4
+ import Tippy from "@tippyjs/react";
5
+ import clsx from "clsx";
6
+ import RcSlider from "rc-slider";
7
+ import React, { useEffect, useState } from "react";
8
+ import { PlayerPhase } from "white-web-sdk";
9
+
10
+ import { useTranslation } from "../../i18n";
11
+ import { Icon } from "../../icons";
12
+ import { themes, TopOffset } from "../../theme";
13
+ import { useTheme } from "../hooks";
14
+ import { Button } from "./components/Button";
15
+ import { usePlayerControl } from "./hooks";
16
+ import { Icons } from "./icons";
17
+
18
+ export type PlayerControlProps = {
19
+ autoHide?: boolean;
20
+ player?: Player;
21
+ } & Omit<CommonProps, "room"> &
22
+ GenericIcon<"play" | "pause" | "loading">;
23
+
24
+ export const name = "fastboard-player-control";
25
+
26
+ export function PlayerControl({ theme, autoHide = false, player: player_, ...icons }: PlayerControlProps) {
27
+ theme = useTheme(theme);
28
+ const { t } = useTranslation();
29
+
30
+ const [currentTime, setCurrentTime] = useState(0);
31
+ const player = usePlayerControl(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 = player.phase === PlayerPhase.WaitingFirstFrame || player.phase === PlayerPhase.Buffering;
45
+ const isPlaying = player.phase === PlayerPhase.Playing;
46
+
47
+ const { activeColor } = themes[theme];
48
+
49
+ return (
50
+ <div className={clsx(name, theme, { "auto-hide": autoHide })}>
51
+ <button
52
+ className={clsx(`${name}-btn`, isLoading ? "loading" : isPlaying ? "pause" : "play", theme)}
53
+ disabled={isLoading}
54
+ onClick={player.togglePlay}
55
+ >
56
+ <Icon
57
+ fallback={
58
+ isLoading ? (
59
+ <Icons.Loading theme={theme} />
60
+ ) : isPlaying ? (
61
+ <Icons.Pause theme={theme} />
62
+ ) : (
63
+ <Icons.Play theme={theme} />
64
+ )
65
+ }
66
+ src={isLoading ? icons.loadingIcon : isPlaying ? icons.pauseIcon : icons.playIcon}
67
+ alt={isLoading ? "[loading]" : isPlaying ? "[pause]" : "[play]"}
68
+ />
69
+ </button>
70
+ <span className={clsx(`${name}-slider`, { loading: isLoading }, theme)}>
71
+ <RcSlider
72
+ disabled={isLoading}
73
+ trackStyle={{ background: activeColor }}
74
+ handleStyle={{ border: `1px solid ${activeColor}` }}
75
+ value={currentTime}
76
+ onChange={setCurrentTime}
77
+ min={0}
78
+ max={player.totalTime}
79
+ step={100}
80
+ />
81
+ </span>
82
+ <span className={clsx(`${name}-current`, theme)}>{renderTime(player.currentTime)}</span>
83
+ <span className={clsx(`${name}-slash`, theme)}>/</span>
84
+ <span className={clsx(`${name}-total`, theme)}>{renderTime(player.totalTime)}</span>
85
+ <span className={`${name}-btn-interactive`}>
86
+ <Tippy
87
+ className="fastboard-tip"
88
+ content={renderSpeeds(player)}
89
+ theme={theme}
90
+ placement="top-end"
91
+ trigger="click"
92
+ offset={TopOffset}
93
+ arrow={false}
94
+ interactive
95
+ >
96
+ <Button content={t("speed")} theme={theme} disabled={isLoading}>
97
+ <span className={clsx(`${name}-speed-text`, theme)}>{player.speed}x</span>
98
+ </Button>
99
+ </Tippy>
100
+ </span>
101
+ </div>
102
+ );
103
+ }
104
+
105
+ function renderTime(ms: number) {
106
+ let seconds = ms / 1000;
107
+ const minutes = Math.floor(seconds / 60);
108
+ seconds = Math.floor(seconds) % 60;
109
+
110
+ return `${String(minutes).padStart(2, "0")}` + `:${String(seconds).padStart(2, "0")}`;
111
+ }
112
+
113
+ const Speeds = [2.0, 1.5, 1.25, 1.0, 0.75, 0.5];
114
+
115
+ function renderSpeeds({ speed: current, setSpeed }: { speed: number; setSpeed: (speed: number) => void }) {
116
+ return (
117
+ <div className={clsx(`${name}-panel`, "speed")}>
118
+ {Speeds.map(speed => (
119
+ <button
120
+ className={clsx(`${name}-btn`, "speed", {
121
+ active: speed === current,
122
+ })}
123
+ key={speed}
124
+ onClick={() => setSpeed(speed)}
125
+ >
126
+ {speed}x
127
+ </button>
128
+ ))}
129
+ </div>
130
+ );
131
+ }
@@ -0,0 +1,45 @@
1
+ import type { Placement } from "tippy.js";
2
+ import type { Theme } from "../../../typings";
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>((props, ref) => {
21
+ const { theme, content, disabled, active, onClick, interactive, placement = "top", children } = props;
22
+
23
+ return (
24
+ <Tippy
25
+ className="fastboard-tip"
26
+ content={content}
27
+ interactive={interactive}
28
+ theme={theme}
29
+ disabled={disabled}
30
+ placement={placement}
31
+ offset={TopOffset}
32
+ delay={[1000, 400]}
33
+ duration={300}
34
+ >
35
+ <button
36
+ ref={ref}
37
+ className={clsx("fastboard-player-control-btn", theme, { active })}
38
+ onClick={onClick}
39
+ disabled={disabled}
40
+ >
41
+ {children}
42
+ </button>
43
+ </Tippy>
44
+ );
45
+ });
@@ -0,0 +1,88 @@
1
+ import type { DependencyList } from "react";
2
+ import type { Player } from "white-web-sdk";
3
+
4
+ import { useCallback, useEffect, useState } from "react";
5
+ import { PlayerPhase } from "white-web-sdk";
6
+ import { useLastValue } from "../../internal/hooks";
7
+
8
+ const EMPTY_ARRAY: DependencyList = [];
9
+
10
+ function useForceUpdate() {
11
+ const [, forceUpdate_] = useState({});
12
+ // eslint-disable-next-line react-hooks/exhaustive-deps
13
+ return useCallback(() => forceUpdate_({}), EMPTY_ARRAY);
14
+ }
15
+
16
+ export function usePlayerControl(player?: Player | null) {
17
+ const togglePlay = useCallback(() => {
18
+ if (player) {
19
+ switch (player.phase) {
20
+ case PlayerPhase.WaitingFirstFrame:
21
+ case PlayerPhase.Pause:
22
+ case PlayerPhase.Ended: {
23
+ player.play();
24
+ break;
25
+ }
26
+ case PlayerPhase.Playing: {
27
+ player.pause();
28
+ break;
29
+ }
30
+ }
31
+ }
32
+ }, [player]);
33
+
34
+ const seekToProgressTime = useCallback(
35
+ (time: number) => {
36
+ if (player) {
37
+ player.seekToProgressTime(time);
38
+ }
39
+ },
40
+ [player]
41
+ );
42
+
43
+ const lastPlayer = useLastValue(player);
44
+
45
+ const forceUpdate = useForceUpdate();
46
+
47
+ const setSpeed = useCallback(
48
+ (speed: number) => {
49
+ if (player) {
50
+ player.playbackSpeed = speed;
51
+ forceUpdate();
52
+ }
53
+ },
54
+ [forceUpdate, player]
55
+ );
56
+
57
+ useEffect(() => {
58
+ if (!lastPlayer && player) {
59
+ forceUpdate();
60
+ }
61
+ }, [forceUpdate, lastPlayer, player]);
62
+
63
+ useEffect(() => {
64
+ if (player) {
65
+ player.callbacks.on("onPhaseChanged", forceUpdate);
66
+ player.callbacks.on("onProgressTimeChanged", forceUpdate);
67
+ return () => {
68
+ player.callbacks.off("onPhaseChanged", forceUpdate);
69
+ player.callbacks.off("onProgressTimeChanged", forceUpdate);
70
+ };
71
+ }
72
+ }, [forceUpdate, player]);
73
+
74
+ const phase = player ? player.phase : PlayerPhase.WaitingFirstFrame;
75
+ const currentTime = player ? player.progressTime : 0;
76
+ const totalTime = player ? player.timeDuration : 0;
77
+ const speed = player ? player.playbackSpeed : 1;
78
+
79
+ return {
80
+ phase,
81
+ currentTime,
82
+ totalTime,
83
+ speed,
84
+ setSpeed,
85
+ togglePlay,
86
+ seekToProgressTime,
87
+ };
88
+ }
@@ -0,0 +1,13 @@
1
+ import type { IconProps } from "../../../typings";
2
+
3
+ import React from "react";
4
+ import { getStroke } from "../../../theme";
5
+
6
+ export const Loading = (props: IconProps) => {
7
+ const stroke = getStroke(props);
8
+ return (
9
+ <svg viewBox="0 0 24 24">
10
+ <path d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z" fill={stroke}></path>
11
+ </svg>
12
+ );
13
+ };
@@ -0,0 +1,13 @@
1
+ import type { IconProps } from "../../../typings";
2
+
3
+ import React from "react";
4
+ import { getStroke } from "../../../theme";
5
+
6
+ export const Pause = (props: IconProps) => {
7
+ const stroke = getStroke(props);
8
+ return (
9
+ <svg viewBox="0 0 24 24">
10
+ <path d="M14 19h4V5h-4M6 19h4V5H6v14z" fill={stroke}></path>
11
+ </svg>
12
+ );
13
+ };
@@ -0,0 +1,13 @@
1
+ import type { IconProps } from "../../../typings";
2
+
3
+ import React from "react";
4
+ import { getStroke } from "../../../theme";
5
+
6
+ export const Play = (props: IconProps) => {
7
+ const stroke = getStroke(props);
8
+ return (
9
+ <svg viewBox="0 0 24 24">
10
+ <path d="M8 5.14v14l11-7l-11-7z" fill={stroke}></path>
11
+ </svg>
12
+ );
13
+ };
@@ -0,0 +1,10 @@
1
+ import { memo } from "react";
2
+ import { Loading } from "./Loading";
3
+ import { Pause } from "./Pause";
4
+ import { Play } from "./Play";
5
+
6
+ export const Icons = {
7
+ Play: memo(Play),
8
+ Pause: memo(Pause),
9
+ Loading: memo(Loading),
10
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./hooks";
2
+ export { PlayerControl, type PlayerControlProps } from "./PlayerControl";
@@ -0,0 +1,56 @@
1
+ $name: "fastboard-redo-undo";
2
+
3
+ .#{$name} {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ gap: 4px;
7
+ padding: 4px;
8
+ border-radius: 4px;
9
+ backdrop-filter: blur(2px);
10
+ -webkit-backdrop-filter: blur(2px);
11
+
12
+ &.light {
13
+ color: #333;
14
+ background-color: rgba($color: #fff, $alpha: 0.85);
15
+ border: 1px solid rgba(0, 0, 0, 0.15);
16
+ }
17
+
18
+ &.dark {
19
+ color: #ddd;
20
+ background-color: rgba($color: #333, $alpha: 0.85);
21
+ border: 1px solid rgba(0, 0, 0, 0.45);
22
+ }
23
+ }
24
+
25
+ .#{$name}-btn {
26
+ appearance: none;
27
+ cursor: pointer;
28
+ margin: 0;
29
+ border: 0;
30
+ padding: 0;
31
+ width: 24px;
32
+ height: 24px;
33
+ background-color: transparent;
34
+ border-radius: 4px;
35
+ font-size: 24px;
36
+ line-height: 1;
37
+
38
+ svg,
39
+ img {
40
+ width: 1em;
41
+ height: 1em;
42
+ }
43
+
44
+ &:disabled {
45
+ opacity: 0.5;
46
+ cursor: not-allowed;
47
+ }
48
+
49
+ &.light:not(:disabled):hover {
50
+ background-color: rgba(51, 129, 255, 0.1);
51
+ }
52
+
53
+ &.dark:not(:disabled):hover {
54
+ background-color: rgba(51, 129, 255, 0.25);
55
+ }
56
+ }