@capytale/meta-player 0.0.3 → 0.1.1
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/package.json +1 -1
- package/src/App.tsx +12 -15
- package/src/MetaPlayer.tsx +1 -12
- package/src/app/store.ts +2 -0
- package/src/app.module.scss +1 -0
- package/src/demo.tsx +10 -8
- package/src/external/prime.ts +5 -0
- package/src/features/activityData/IsDirtySetter.tsx +0 -6
- package/src/features/activityData/MetaPlayerOptionsSetter.tsx +41 -0
- package/src/features/activityData/hooks.ts +9 -0
- package/src/features/activityJS/Saver.tsx +0 -1
- package/src/features/activitySettings/ActivitySettingsSetter.tsx +26 -0
- package/src/features/activitySettings/activitySettingsSlice.ts +43 -0
- package/src/features/activitySettings/hooks.ts +6 -0
- package/src/features/activitySettings/index.tsx +32 -0
- package/src/features/activitySettings/style.module.scss +4 -0
- package/src/features/activitySettings/types.ts +88 -0
- package/src/features/activitySettings/ui.tsx +393 -0
- package/src/features/navbar/ActivityMenu.tsx +30 -2
- package/src/features/navbar/CapytaleMenu.tsx +142 -124
- package/src/features/navbar/GradingNav.tsx +0 -1
- package/src/features/navbar/SidebarContent.tsx +38 -11
- package/src/features/navbar/style.module.scss +5 -14
- package/src/features/pedago/index.tsx +0 -2
- package/src/index.css +49 -0
- package/src/utils/equality.ts +32 -0
- package/src/features/activityData/OptionSetter.tsx +0 -35
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActivitySettings,
|
|
3
|
+
ActivitySettingsFormSection,
|
|
4
|
+
ActivitySettingsMultipleOptionsSection,
|
|
5
|
+
ActivitySettingsRadioOptionsSection,
|
|
6
|
+
} from "./types";
|
|
7
|
+
|
|
8
|
+
import styles from "./style.module.scss";
|
|
9
|
+
import { Slider } from "primereact/slider";
|
|
10
|
+
import { InputNumber } from "primereact/inputnumber";
|
|
11
|
+
import { InputText } from "primereact/inputtext";
|
|
12
|
+
import { ColorPicker } from "primereact/colorpicker";
|
|
13
|
+
import { InputTextarea } from "primereact/inputtextarea";
|
|
14
|
+
import { Checkbox } from "primereact/checkbox";
|
|
15
|
+
import { InputSwitch } from "primereact/inputswitch";
|
|
16
|
+
import { Dropdown } from "primereact/dropdown";
|
|
17
|
+
import React from "react";
|
|
18
|
+
import { RadioButton } from "primereact/radiobutton";
|
|
19
|
+
import { useAppDispatch } from "../../app/hooks";
|
|
20
|
+
import { updateSettings } from "./activitySettingsSlice";
|
|
21
|
+
|
|
22
|
+
type ActivitySettingsFormDisplayProps = {
|
|
23
|
+
form: ActivitySettingsFormSection;
|
|
24
|
+
sectionId: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function ActivitySettingsFormDisplay({
|
|
28
|
+
form,
|
|
29
|
+
sectionId,
|
|
30
|
+
}: ActivitySettingsFormDisplayProps) {
|
|
31
|
+
const dispatch = useAppDispatch();
|
|
32
|
+
const selectOnChange = (fieldName: string, value: string) => {
|
|
33
|
+
dispatch(
|
|
34
|
+
updateSettings((oldSettings: ActivitySettings | undefined) => {
|
|
35
|
+
if (!oldSettings) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const oldSection = oldSettings[
|
|
39
|
+
sectionId
|
|
40
|
+
] as ActivitySettingsFormSection;
|
|
41
|
+
const newSettings = {
|
|
42
|
+
...oldSettings,
|
|
43
|
+
[sectionId]: {
|
|
44
|
+
...oldSection,
|
|
45
|
+
type: "form",
|
|
46
|
+
fields: {
|
|
47
|
+
...oldSection.fields,
|
|
48
|
+
[fieldName]: {
|
|
49
|
+
...oldSection.fields[fieldName],
|
|
50
|
+
selectedOptionName: value,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
return newSettings as ActivitySettings;
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
const onChange = (fieldName: string, value: string | number | boolean) => {
|
|
60
|
+
dispatch(
|
|
61
|
+
updateSettings((oldSettings: ActivitySettings | undefined) => {
|
|
62
|
+
if (!oldSettings) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const oldSection = oldSettings[
|
|
66
|
+
sectionId
|
|
67
|
+
] as ActivitySettingsFormSection;
|
|
68
|
+
const newSettings = {
|
|
69
|
+
...oldSettings,
|
|
70
|
+
[sectionId]: {
|
|
71
|
+
...oldSection,
|
|
72
|
+
type: "form",
|
|
73
|
+
fields: {
|
|
74
|
+
...oldSection.fields,
|
|
75
|
+
[fieldName]: {
|
|
76
|
+
...oldSection.fields[fieldName],
|
|
77
|
+
value,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
return newSettings as ActivitySettings;
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
return (
|
|
87
|
+
<div className="mb-3">
|
|
88
|
+
{Object.keys(form.fields).map((name, _) => {
|
|
89
|
+
const field = form.fields[name];
|
|
90
|
+
switch (field.type) {
|
|
91
|
+
case "range":
|
|
92
|
+
return (
|
|
93
|
+
<div className={styles.formGroup}>
|
|
94
|
+
<label
|
|
95
|
+
className={styles.formLabel}
|
|
96
|
+
htmlFor={sectionId + "-" + name}
|
|
97
|
+
>
|
|
98
|
+
Range
|
|
99
|
+
</label>
|
|
100
|
+
<InputNumber
|
|
101
|
+
min={field.min}
|
|
102
|
+
max={field.max}
|
|
103
|
+
step={field.step}
|
|
104
|
+
value={field.value}
|
|
105
|
+
onChange={(e) => onChange(name, e.value as number)}
|
|
106
|
+
/>
|
|
107
|
+
<Slider
|
|
108
|
+
id={sectionId + "-" + name}
|
|
109
|
+
min={field.min}
|
|
110
|
+
max={field.max}
|
|
111
|
+
step={field.step}
|
|
112
|
+
value={field.value}
|
|
113
|
+
onChange={(e) => onChange(name, e.value as number)}
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
case "input":
|
|
118
|
+
return (
|
|
119
|
+
<div className={styles.formGroup}>
|
|
120
|
+
<label
|
|
121
|
+
className={styles.formLabel}
|
|
122
|
+
htmlFor={sectionId + "-" + name}
|
|
123
|
+
>
|
|
124
|
+
{field.label}
|
|
125
|
+
</label>
|
|
126
|
+
{field.inputType === "text" && (
|
|
127
|
+
<InputText
|
|
128
|
+
id={sectionId + "-" + name}
|
|
129
|
+
value={field.value as string}
|
|
130
|
+
onChange={(e) => onChange(name, e.target.value)}
|
|
131
|
+
/>
|
|
132
|
+
)}
|
|
133
|
+
{field.inputType === "number" && (
|
|
134
|
+
<InputNumber
|
|
135
|
+
inputId={sectionId + "-" + name}
|
|
136
|
+
value={field.value as number}
|
|
137
|
+
onChange={(e) => e.value != null && onChange(name, e.value)}
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
140
|
+
{field.inputType === "color" && (
|
|
141
|
+
<ColorPicker
|
|
142
|
+
inputId={sectionId + "-" + name}
|
|
143
|
+
value={field.value as string}
|
|
144
|
+
onChange={(e) =>
|
|
145
|
+
e.value && onChange(name, e.value as string)
|
|
146
|
+
}
|
|
147
|
+
/>
|
|
148
|
+
)}
|
|
149
|
+
{field.inputType !== "color" &&
|
|
150
|
+
field.inputType !== "number" &&
|
|
151
|
+
field.inputType !== "text" && (
|
|
152
|
+
<p>Type d'input {field.inputType} pas implémenté.</p>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
case "textarea":
|
|
157
|
+
return (
|
|
158
|
+
<div className={styles.formGroup}>
|
|
159
|
+
<label
|
|
160
|
+
className={styles.formLabel}
|
|
161
|
+
htmlFor={sectionId + "-" + name}
|
|
162
|
+
>
|
|
163
|
+
{field.label}
|
|
164
|
+
</label>
|
|
165
|
+
<InputTextarea
|
|
166
|
+
id={sectionId + "-" + name}
|
|
167
|
+
value={field.value}
|
|
168
|
+
onChange={(e) => onChange(name, e.target.value)}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
case "checkbox":
|
|
173
|
+
return (
|
|
174
|
+
<CheckboxGroup
|
|
175
|
+
inputId={sectionId + "-" + name}
|
|
176
|
+
checked={field.value}
|
|
177
|
+
onChange={(value) => onChange(name, value)}
|
|
178
|
+
label={field.label}
|
|
179
|
+
/>
|
|
180
|
+
);
|
|
181
|
+
case "switch":
|
|
182
|
+
return (
|
|
183
|
+
<SwitchGroup
|
|
184
|
+
inputId={sectionId + "-" + name}
|
|
185
|
+
checked={field.value}
|
|
186
|
+
onChange={(value) => onChange(name, value)}
|
|
187
|
+
label={field.label}
|
|
188
|
+
/>
|
|
189
|
+
);
|
|
190
|
+
case "select":
|
|
191
|
+
return (
|
|
192
|
+
<div className={styles.formGroup}>
|
|
193
|
+
<label
|
|
194
|
+
className={styles.formLabel}
|
|
195
|
+
htmlFor={sectionId + "-" + name}
|
|
196
|
+
>
|
|
197
|
+
{field.label}
|
|
198
|
+
</label>
|
|
199
|
+
<Dropdown
|
|
200
|
+
inputId={sectionId + "-" + name}
|
|
201
|
+
value={field.selectedOptionName}
|
|
202
|
+
onChange={(e) => selectOnChange(name, e.value)}
|
|
203
|
+
options={field.options}
|
|
204
|
+
optionLabel="label"
|
|
205
|
+
optionValue="name"
|
|
206
|
+
/>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
})}
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
type ActivitySettingsOptionsDisplayProps = {
|
|
216
|
+
options:
|
|
217
|
+
| ActivitySettingsMultipleOptionsSection
|
|
218
|
+
| ActivitySettingsRadioOptionsSection;
|
|
219
|
+
sectionId: string;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export function ActivitySettingsOptionsDisplay({
|
|
223
|
+
options,
|
|
224
|
+
sectionId,
|
|
225
|
+
}: ActivitySettingsOptionsDisplayProps) {
|
|
226
|
+
return (
|
|
227
|
+
<div>
|
|
228
|
+
{options.type === "radio" ? (
|
|
229
|
+
<ActivitySettingsRadioDisplay options={options} sectionId={sectionId} />
|
|
230
|
+
) : (
|
|
231
|
+
<ActivitySettingsCheckboxesDisplay
|
|
232
|
+
options={options}
|
|
233
|
+
sectionId={sectionId}
|
|
234
|
+
/>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
type ActivitySettingsCheckboxesDisplayProps = {
|
|
241
|
+
options: ActivitySettingsMultipleOptionsSection;
|
|
242
|
+
sectionId: string;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
function ActivitySettingsCheckboxesDisplay({
|
|
246
|
+
options,
|
|
247
|
+
sectionId,
|
|
248
|
+
}: ActivitySettingsCheckboxesDisplayProps) {
|
|
249
|
+
const dispatch = useAppDispatch();
|
|
250
|
+
const onChange = (optionName: string, checked: boolean) => {
|
|
251
|
+
dispatch(
|
|
252
|
+
updateSettings((oldSettings: ActivitySettings | undefined) => {
|
|
253
|
+
if (!oldSettings) {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
const oldSection = oldSettings[
|
|
257
|
+
sectionId
|
|
258
|
+
] as ActivitySettingsMultipleOptionsSection;
|
|
259
|
+
const newSettings = {
|
|
260
|
+
...oldSettings,
|
|
261
|
+
[sectionId]: {
|
|
262
|
+
...oldSection,
|
|
263
|
+
selectedOptionNames: checked
|
|
264
|
+
? [...oldSection.selectedOptionNames, optionName]
|
|
265
|
+
: oldSection.selectedOptionNames.filter((n) => n !== optionName),
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
return newSettings as ActivitySettings;
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
};
|
|
272
|
+
return (
|
|
273
|
+
<div className="sidebarRadioButtons">
|
|
274
|
+
{options.options.map((option) => (
|
|
275
|
+
<CheckboxGroup
|
|
276
|
+
key={option.name}
|
|
277
|
+
inputId={sectionId + "-" + option.name}
|
|
278
|
+
checked={options.selectedOptionNames.includes(option.name)}
|
|
279
|
+
onChange={(value) => onChange(option.name, value)}
|
|
280
|
+
label={option.label}
|
|
281
|
+
/>
|
|
282
|
+
))}
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
type ActivitySettingsRadioDisplayProps = {
|
|
288
|
+
options: ActivitySettingsRadioOptionsSection;
|
|
289
|
+
sectionId: string;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
function ActivitySettingsRadioDisplay({
|
|
293
|
+
options,
|
|
294
|
+
sectionId,
|
|
295
|
+
}: ActivitySettingsRadioDisplayProps) {
|
|
296
|
+
const dispatch = useAppDispatch();
|
|
297
|
+
const onChange = (optionName: string, checked: boolean) => {
|
|
298
|
+
if (!checked) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
dispatch(
|
|
302
|
+
updateSettings((oldSettings: ActivitySettings | undefined) => {
|
|
303
|
+
if (!oldSettings) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
const oldSection = oldSettings[
|
|
307
|
+
sectionId
|
|
308
|
+
] as ActivitySettingsRadioOptionsSection;
|
|
309
|
+
const newSettings = {
|
|
310
|
+
...oldSettings,
|
|
311
|
+
[sectionId]: {
|
|
312
|
+
...oldSection,
|
|
313
|
+
selectedOptionName: checked ? optionName : null,
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
return newSettings as ActivitySettings;
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
};
|
|
320
|
+
return (
|
|
321
|
+
<div className="sidebarRadioButtons">
|
|
322
|
+
{options.options.map((option) => (
|
|
323
|
+
<RadioGroup
|
|
324
|
+
key={option.name}
|
|
325
|
+
inputId={sectionId + "-" + option.name}
|
|
326
|
+
checked={options.selectedOptionName === option.name}
|
|
327
|
+
onChange={(value) => {
|
|
328
|
+
onChange(option.name, value);
|
|
329
|
+
}}
|
|
330
|
+
label={option.label}
|
|
331
|
+
/>
|
|
332
|
+
))}
|
|
333
|
+
</div>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
type CheckboxGroupProps = {
|
|
338
|
+
inputId: string;
|
|
339
|
+
label: string;
|
|
340
|
+
checked: boolean;
|
|
341
|
+
onChange?: (value: boolean) => void;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const CheckboxGroup: React.FC<CheckboxGroupProps> = (props) => {
|
|
345
|
+
return (
|
|
346
|
+
<div className="sidebarRadioGroup">
|
|
347
|
+
<Checkbox
|
|
348
|
+
inputId={props.inputId}
|
|
349
|
+
checked={props.checked}
|
|
350
|
+
onChange={(e) =>
|
|
351
|
+
props.onChange && e.checked != null && props.onChange(e.checked)
|
|
352
|
+
}
|
|
353
|
+
/>
|
|
354
|
+
<label className={styles.checkboxLabel} htmlFor={props.inputId}>
|
|
355
|
+
{props.label}
|
|
356
|
+
</label>
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const RadioGroup: React.FC<CheckboxGroupProps> = (props) => {
|
|
362
|
+
return (
|
|
363
|
+
<div className="sidebarRadioGroup">
|
|
364
|
+
<RadioButton
|
|
365
|
+
inputId={props.inputId}
|
|
366
|
+
checked={props.checked}
|
|
367
|
+
onChange={(e) => {
|
|
368
|
+
props.onChange && e.checked != null && props.onChange(e.checked);
|
|
369
|
+
}}
|
|
370
|
+
/>
|
|
371
|
+
<label className={styles.checkboxLabel} htmlFor={props.inputId}>
|
|
372
|
+
{props.label}
|
|
373
|
+
</label>
|
|
374
|
+
</div>
|
|
375
|
+
);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const SwitchGroup: React.FC<CheckboxGroupProps> = (props) => {
|
|
379
|
+
return (
|
|
380
|
+
<div className="sidebarRadioGroup">
|
|
381
|
+
<InputSwitch
|
|
382
|
+
inputId={props.inputId}
|
|
383
|
+
checked={props.checked}
|
|
384
|
+
onChange={(e) =>
|
|
385
|
+
props.onChange && e.checked != null && props.onChange(e.checked)
|
|
386
|
+
}
|
|
387
|
+
/>
|
|
388
|
+
<label className={styles.checkboxLabel} htmlFor={props.inputId}>
|
|
389
|
+
{props.label}
|
|
390
|
+
</label>
|
|
391
|
+
</div>
|
|
392
|
+
);
|
|
393
|
+
};
|
|
@@ -7,6 +7,8 @@ import SidebarContent from "./SidebarContent";
|
|
|
7
7
|
import settings from "../../settings";
|
|
8
8
|
import ActivityQuickActions from "./ActivityQuickActions";
|
|
9
9
|
import useFullscreen from "../../utils/useFullscreen";
|
|
10
|
+
import { useActivityJS } from "../activityJS/ActivityJSProvider";
|
|
11
|
+
import { Dialog } from "primereact/dialog";
|
|
10
12
|
|
|
11
13
|
const ActivityMenu: React.FC = memo(() => {
|
|
12
14
|
const [settingsSidebarVisible, setSettingsSidebarVisible] = useState(false);
|
|
@@ -14,13 +16,15 @@ const ActivityMenu: React.FC = memo(() => {
|
|
|
14
16
|
const isFullscreen = useFullscreen(wantFullscreen, () =>
|
|
15
17
|
setWantFullscreen(false),
|
|
16
18
|
);
|
|
19
|
+
const [helpDialogVisible, setHelpDialogVisible] = useState(false);
|
|
20
|
+
const activityJS = useActivityJS();
|
|
17
21
|
return (
|
|
18
22
|
<div className={styles.activityMenu}>
|
|
19
23
|
<ActivityQuickActions />
|
|
20
24
|
<Button
|
|
21
25
|
severity="secondary"
|
|
22
26
|
size="small"
|
|
23
|
-
icon={isFullscreen ? "pi pi-expand" : "pi pi-expand"
|
|
27
|
+
icon={isFullscreen ? "pi pi-expand" : "pi pi-expand"}
|
|
24
28
|
text
|
|
25
29
|
tooltip="Plein écran"
|
|
26
30
|
tooltipOptions={{
|
|
@@ -47,8 +51,32 @@ const ActivityMenu: React.FC = memo(() => {
|
|
|
47
51
|
position="right"
|
|
48
52
|
onHide={() => setSettingsSidebarVisible(false)}
|
|
49
53
|
>
|
|
50
|
-
<SidebarContent
|
|
54
|
+
<SidebarContent
|
|
55
|
+
showHelp={
|
|
56
|
+
activityJS.activitySession?.type.helpUrl
|
|
57
|
+
? () => {
|
|
58
|
+
setHelpDialogVisible(true);
|
|
59
|
+
setSettingsSidebarVisible(false);
|
|
60
|
+
}
|
|
61
|
+
: undefined
|
|
62
|
+
}
|
|
63
|
+
/>
|
|
51
64
|
</Sidebar>
|
|
65
|
+
{activityJS.activitySession?.type.helpUrl && (
|
|
66
|
+
<Dialog
|
|
67
|
+
id="metaPlayerHelpDialog"
|
|
68
|
+
header="Documentation"
|
|
69
|
+
visible={helpDialogVisible}
|
|
70
|
+
onHide={() => setHelpDialogVisible(false)}
|
|
71
|
+
maximizable={true}
|
|
72
|
+
>
|
|
73
|
+
<iframe
|
|
74
|
+
src={activityJS.activitySession?.type.helpUrl}
|
|
75
|
+
style={{ width: "100%", height: "100%" }}
|
|
76
|
+
title="Documentation"
|
|
77
|
+
/>
|
|
78
|
+
</Dialog>
|
|
79
|
+
)}
|
|
52
80
|
</div>
|
|
53
81
|
);
|
|
54
82
|
});
|