Skip to content

Commit 984a53d

Browse files
authored
feat(classroom): add presets (#1526)
1 parent b5dc868 commit 984a53d

File tree

15 files changed

+268
-6
lines changed

15 files changed

+268
-6
lines changed
Loading

desktop/renderer-app/src/components/Whiteboard.tsx

+20
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import React, { useCallback, useContext, useEffect, useState } from "react";
66
import { Fastboard, Language } from "@netless/fastboard-react";
77
import {
88
DarkModeContext,
9+
PresetsModal,
910
RaiseHand,
1011
SaveAnnotationModal,
1112
SaveAnnotationModalProps,
@@ -20,6 +21,7 @@ import { isSupportedFileExt } from "../utils/drag-and-drop";
2021
import { isSupportedImageType, onDropImage } from "../utils/drag-and-drop/image";
2122
import { ClassRoomStore } from "../stores/class-room-store";
2223
import { refreshApps } from "../utils/toolbar-apps";
24+
import { PRESETS } from "../constants/presets";
2325

2426
export interface WhiteboardProps {
2527
whiteboardStore: WhiteboardStore;
@@ -46,6 +48,7 @@ export const Whiteboard = observer<WhiteboardProps>(function Whiteboard({
4648
const [saveAnnotationImages, setSaveAnnotationImages] = useState<
4749
SaveAnnotationModalProps["images"]
4850
>([]);
51+
const [presetsVisible, showPresets] = useState(false);
4952

5053
const isReconnecting = phase === RoomPhase.Reconnecting;
5154

@@ -135,6 +138,9 @@ export const Whiteboard = observer<WhiteboardProps>(function Whiteboard({
135138
onSaveAnnotation: () => {
136139
showSaveAnnotation(true);
137140
},
141+
onPresets: () => {
142+
showPresets(true);
143+
},
138144
});
139145
}, [t]);
140146

@@ -156,6 +162,14 @@ export const Whiteboard = observer<WhiteboardProps>(function Whiteboard({
156162
}
157163
}, []);
158164

165+
const insertPresetImage = useCallback(
166+
(fileURL: string) => {
167+
whiteboardStore.insertImage({ fileURL });
168+
showPresets(false);
169+
},
170+
[whiteboardStore],
171+
);
172+
159173
const onDragOver = useCallback(
160174
(event: React.DragEvent<HTMLDivElement>) => {
161175
event.preventDefault();
@@ -219,6 +233,12 @@ export const Whiteboard = observer<WhiteboardProps>(function Whiteboard({
219233
visible={saveAnnotationVisible}
220234
onClose={() => showSaveAnnotation(false)}
221235
/>
236+
<PresetsModal
237+
images={PRESETS}
238+
visible={presetsVisible}
239+
onClick={insertPresetImage}
240+
onClose={() => showPresets(false)}
241+
/>
222242
</>
223243
);
224244
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { PresetImage } from "flat-components";
2+
3+
export const PRESETS: PresetImage[] = [
4+
{
5+
src: "https://flat-storage.oss-cn-hangzhou.aliyuncs.com/flat-resources/presets/math_1.png",
6+
i18nKey: "presets.math_1",
7+
},
8+
{
9+
src: "https://flat-storage.oss-cn-hangzhou.aliyuncs.com/flat-resources/presets/english_1.jpeg",
10+
i18nKey: "presets.english_1",
11+
},
12+
{
13+
src: "https://flat-storage.oss-cn-hangzhou.aliyuncs.com/flat-resources/presets/chinese_1.png",
14+
i18nKey: "presets.chinese_1",
15+
},
16+
];

desktop/renderer-app/src/stores/whiteboard-store.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ export class WhiteboardStore {
457457
}
458458
};
459459

460-
public insertImage = async (file: CloudStorageFile): Promise<void> => {
460+
public insertImage = async (file: Pick<CloudStorageFile, "fileURL">): Promise<void> => {
461461
const room = this.room;
462462
if (!room) {
463463
return;

desktop/renderer-app/src/utils/toolbar-apps.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import monacoSVG from "../assets/image/tool-monaco.svg";
22
import geogebraSVG from "../assets/image/tool-geogebra.svg";
33
import countdownSVG from "../assets/image/tool-countdown.svg";
44
import saveSVG from "../assets/image/tool-save.svg";
5+
import presetsSVG from "../assets/image/tool-presets.svg";
56

67
import { TFunction } from "react-i18next";
78
import { apps, FastboardApp } from "@netless/fastboard-react";
@@ -11,9 +12,10 @@ import { i18n } from "./i18n";
1112
export interface RefreshAppsParams {
1213
t: TFunction;
1314
onSaveAnnotation?: (app: FastboardApp) => void;
15+
onPresets?: (app: FastboardApp) => void;
1416
}
1517

16-
export const refreshApps = ({ t, onSaveAnnotation }: RefreshAppsParams): void => {
18+
export const refreshApps = ({ t, onSaveAnnotation, onPresets }: RefreshAppsParams): void => {
1719
apps.clear();
1820
apps.push(
1921
{
@@ -40,6 +42,12 @@ export const refreshApps = ({ t, onSaveAnnotation }: RefreshAppsParams): void =>
4042
label: t("tool.save"),
4143
onClick: onSaveAnnotation || noop,
4244
},
45+
{
46+
kind: "Presets",
47+
icon: presetsSVG,
48+
label: t("tool.presets"),
49+
onClick: onPresets || noop,
50+
},
4351
);
4452
};
4553

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import "./style.less";
2+
3+
import React from "react";
4+
import { Modal } from "antd";
5+
import { useTranslation } from "react-i18next";
6+
7+
import { SVGPlus } from "../FlatIcons";
8+
9+
export interface PresetImage {
10+
src: string;
11+
i18nKey: string;
12+
}
13+
14+
export interface PresetsModalProps {
15+
visible: boolean;
16+
images: PresetImage[];
17+
onClick: (image: string) => void;
18+
onClose: () => void;
19+
}
20+
21+
export const PresetsModal: React.FC<PresetsModalProps> = ({
22+
visible,
23+
images,
24+
onClick,
25+
onClose,
26+
}) => {
27+
const { t } = useTranslation();
28+
29+
return (
30+
<Modal
31+
closable
32+
destroyOnClose
33+
footer={null}
34+
title={t("presets.title")}
35+
visible={visible}
36+
width={640}
37+
wrapClassName="presets-modal-container"
38+
onCancel={onClose}
39+
>
40+
{images.map(({ src, i18nKey }) => (
41+
<div className="presets-modal-item">
42+
<button
43+
key={i18nKey}
44+
className="presets-modal-btn"
45+
title={t(i18nKey)}
46+
onClick={() => onClick(src)}
47+
>
48+
<img alt={i18nKey} src={src} title={t(i18nKey)} />
49+
<div className="presets-modal-mask">
50+
<SVGPlus />
51+
</div>
52+
</button>
53+
<div className="presets-modal-text">{t(i18nKey)}</div>
54+
</div>
55+
))}
56+
</Modal>
57+
);
58+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
.presets-modal-container {
2+
.ant-modal-title {
3+
text-align: center;
4+
}
5+
.ant-modal-body {
6+
display: grid;
7+
grid-template-columns: repeat(3, 1fr);
8+
justify-items: center;
9+
gap: 54px 48px;
10+
padding: 16px 54px 48px;
11+
height: 480px - 54px;
12+
overflow-y: auto;
13+
}
14+
}
15+
16+
.presets-modal-item {
17+
text-align: center;
18+
width: 140px;
19+
}
20+
21+
.presets-modal-btn {
22+
appearance: none;
23+
background-color: transparent;
24+
padding: 0;
25+
position: relative;
26+
width: 100%;
27+
height: 100px;
28+
border: 1px solid var(--grey-3);
29+
border-radius: 4px;
30+
overflow: hidden;
31+
transition: border 0.1s linear;
32+
cursor: pointer;
33+
user-select: none;
34+
-webkit-user-select: none;
35+
36+
img {
37+
width: 100%;
38+
height: 100%;
39+
object-fit: cover;
40+
}
41+
}
42+
43+
.presets-modal-mask {
44+
position: absolute;
45+
top: 0;
46+
left: 0;
47+
right: 0;
48+
bottom: 0;
49+
background-color: var(--grey-12);
50+
transition: opacity 0.2s ease;
51+
opacity: 0;
52+
z-index: 9;
53+
display: flex;
54+
align-items: center;
55+
justify-content: center;
56+
57+
svg {
58+
width: 48px;
59+
height: 48px;
60+
}
61+
}
62+
63+
.presets-modal-text {
64+
color: var(--text);
65+
font-size: 14px;
66+
line-height: 1.75;
67+
max-width: 100%;
68+
white-space: nowrap;
69+
overflow: hidden;
70+
text-overflow: ellipsis;
71+
}
72+
73+
.flat-color-scheme-dark {
74+
.presets-modal-btn {
75+
border-color: var(--grey-8);
76+
}
77+
.presets-modal-mask {
78+
background-color: rgba(0, 0, 0, 0.8);
79+
}
80+
}
81+
82+
.presets-modal-btn:hover {
83+
border-color: var(--primary);
84+
.presets-modal-mask {
85+
opacity: 0.5;
86+
}
87+
}
88+
89+
.presets-modal-btn:active {
90+
border-color: var(--primary-strong);
91+
.presets-modal-mask {
92+
opacity: 0.6;
93+
transition: none;
94+
}
95+
.presets-modal-text {
96+
opacity: 1;
97+
transition: none;
98+
}
99+
}

packages/flat-components/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export * from "./components/PeriodicRoomPage";
1717
export * from "./components/RemoveHistoryRoomModal";
1818
export * from "./components/RemoveRoomModal";
1919
export * from "./components/SaveAnnotationModal";
20+
export * from "./components/PresetsModal";
2021
export * from "./components/RoomDetailPage";
2122
export * from "./components/RoomStatusElement";
2223
export * from "./containers/CloudStorageContainer";

packages/flat-i18n/locales/en.json

+8-1
Original file line numberDiff line numberDiff line change
@@ -441,10 +441,17 @@
441441
"monaco": "Code Editor",
442442
"geogebra": "GeoGebra",
443443
"countdown": "Countdown",
444-
"save": "Save Annotation"
444+
"save": "Save Annotation",
445+
"presets": "Presets"
445446
},
446447
"annotation": {
447448
"save-action": "Save Annotation",
448449
"save-failed": "Save annotation failed"
450+
},
451+
"presets": {
452+
"title": "Presets",
453+
"math_1": "Coordinate Axis",
454+
"english_1": "Four Lines and Three Grids",
455+
"chinese_1": "Tin Word Format"
449456
}
450457
}

packages/flat-i18n/locales/zh-CN.json

+8-1
Original file line numberDiff line numberDiff line change
@@ -441,10 +441,17 @@
441441
"monaco": "编辑器",
442442
"geogebra": "几何代数",
443443
"countdown": "计时器",
444-
"save": "保存板书"
444+
"save": "保存板书",
445+
"presets": "素材库"
445446
},
446447
"annotation": {
447448
"save-action": "保存板书",
448449
"save-failed": "板书保存失败"
450+
},
451+
"presets": {
452+
"title": "资源素材库",
453+
"math_1": "坐标系",
454+
"english_1": "四线三格",
455+
"chinese_1": "田字格"
449456
}
450457
}
Loading

web/flat-web/src/components/Whiteboard.tsx

+20
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Fastboard, Language } from "@netless/fastboard-react";
77
import { RoomPhase } from "white-web-sdk";
88
import {
99
DarkModeContext,
10+
PresetsModal,
1011
RaiseHand,
1112
SaveAnnotationModal,
1213
SaveAnnotationModalProps,
@@ -20,6 +21,7 @@ import { isSupportedFileExt } from "../utils/drag-and-drop";
2021
import { isSupportedImageType, onDropImage } from "../utils/drag-and-drop/image";
2122
import { ClassRoomStore } from "../stores/class-room-store";
2223
import { refreshApps } from "../utils/toolbar-apps";
24+
import { PRESETS } from "../constants/presets";
2325

2426
export interface WhiteboardProps {
2527
whiteboardStore: WhiteboardStore;
@@ -46,6 +48,7 @@ export const Whiteboard = observer<WhiteboardProps>(function Whiteboard({
4648
const [saveAnnotationImages, setSaveAnnotationImages] = useState<
4749
SaveAnnotationModalProps["images"]
4850
>([]);
51+
const [presetsVisible, showPresets] = useState(false);
4952

5053
const isReconnecting = phase === RoomPhase.Reconnecting;
5154

@@ -76,6 +79,9 @@ export const Whiteboard = observer<WhiteboardProps>(function Whiteboard({
7679
onSaveAnnotation: () => {
7780
showSaveAnnotation(true);
7881
},
82+
onPresets: () => {
83+
showPresets(true);
84+
},
7985
});
8086
}, [t]);
8187

@@ -102,6 +108,14 @@ export const Whiteboard = observer<WhiteboardProps>(function Whiteboard({
102108
}
103109
}, []);
104110

111+
const insertPresetImage = useCallback(
112+
(fileURL: string) => {
113+
whiteboardStore.insertImage({ fileURL });
114+
showPresets(false);
115+
},
116+
[whiteboardStore],
117+
);
118+
105119
const onDragOver = useCallback(
106120
(event: React.DragEvent<HTMLDivElement>) => {
107121
event.preventDefault();
@@ -220,6 +234,12 @@ export const Whiteboard = observer<WhiteboardProps>(function Whiteboard({
220234
visible={saveAnnotationVisible}
221235
onClose={() => showSaveAnnotation(false)}
222236
/>
237+
<PresetsModal
238+
images={PRESETS}
239+
visible={presetsVisible}
240+
onClick={insertPresetImage}
241+
onClose={() => showPresets(false)}
242+
/>
223243
</>
224244
);
225245
});

0 commit comments

Comments
 (0)