@capytale/meta-player 0.1.5 → 0.2.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.
package/.eslintrc.json CHANGED
@@ -3,7 +3,8 @@
3
3
  "eslint:recommended",
4
4
  "react-app",
5
5
  "plugin:react/jsx-runtime",
6
- "prettier"
6
+ "prettier",
7
+ "plugin:react-hooks/recommended"
7
8
  ],
8
9
  "parser": "@typescript-eslint/parser",
9
10
  "parserOptions": { "project": true, "tsconfigRootDir": "./" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capytale/meta-player",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -15,13 +15,12 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@capytale/activity.js": "^3.1.0",
18
+ "@capytale/capytale-anti-triche": "^0.2.1",
18
19
  "@capytale/capytale-rich-text-editor": "^0.4.2",
19
20
  "@reduxjs/toolkit": "^2.0.1",
20
21
  "@uidotdev/usehooks": "^2.4.1",
21
22
  "primeicons": "^7.0.0",
22
23
  "primereact": "^10.8.3",
23
- "react": "^18.2.0",
24
- "react-dom": "^18.2.0",
25
24
  "react-html-props": "^2.0.9",
26
25
  "react-redux": "^9.1.0",
27
26
  "screenfull": "^6.0.2"
@@ -31,18 +30,25 @@
31
30
  "@testing-library/jest-dom": "^6.2.0",
32
31
  "@testing-library/react": "^14.1.2",
33
32
  "@testing-library/user-event": "^14.5.2",
34
- "@types/react": "^18.2.47",
35
- "@types/react-dom": "^18.2.18",
36
- "@vitejs/plugin-react": "^4.2.1",
33
+ "@types/react": "^18.3.8",
34
+ "@types/react-dom": "^18.3.0",
35
+ "@vitejs/plugin-react": "^4.3.1",
37
36
  "eslint": "^8.56.0",
38
37
  "eslint-config-prettier": "^9.1.0",
39
38
  "eslint-config-react-app": "^7.0.1",
40
39
  "eslint-plugin-prettier": "^5.1.3",
40
+ "eslint-plugin-react-hooks": "^4.6.2",
41
41
  "jsdom": "^23.2.0",
42
42
  "prettier": "^3.2.1",
43
+ "react": "^18.3.1",
44
+ "react-dom": "^18.3.1",
43
45
  "sass": "^1.75.0",
44
46
  "typescript": "^5.3.3",
45
47
  "vite": "^5.4.6",
46
48
  "vitest": "^1.2.0"
49
+ },
50
+ "peerDependencies": {
51
+ "react": "^18.3.1",
52
+ "react-dom": "^18.3.1"
47
53
  }
48
54
  }
@@ -1,4 +1,4 @@
1
- import React, { PropsWithChildren } from "react";
1
+ import React, { FC, PropsWithChildren } from "react";
2
2
 
3
3
  import { Provider } from "react-redux";
4
4
 
@@ -9,19 +9,47 @@ import App from "./App";
9
9
  import { store } from "./app/store";
10
10
  import "./index.css";
11
11
  import ThemeSwitcher from "./features/theming/ThemeSwitcher";
12
- import { ActivityJSProvider } from "./features/activityJS/ActivityJSProvider";
12
+ import {
13
+ ActivityJSProvider,
14
+ useActivityJS,
15
+ } from "./features/activityJS/ActivityJSProvider";
13
16
  import { LoadOptions } from "./features/activityJS/internal-hooks";
14
17
  import { ErrorBoundary } from "./utils/ErrorBoundary";
15
18
  import Saver from "./features/activityJS/Saver";
19
+ import {
20
+ CapytaleAntiCheat,
21
+ ContentHidingMethod,
22
+ ExitDetectionMethod,
23
+ } from "@capytale/capytale-anti-triche";
24
+ import { useAppSelector } from "./app/hooks";
25
+ import {
26
+ selectActivityInfo,
27
+ selectAntiCheat,
28
+ selectHasAntiCheat,
29
+ selectIsAntiCheatExitDetectionDisabled,
30
+ selectIsDirty,
31
+ selectReturnUrl,
32
+ } from "./features/activityData/activityDataSlice";
33
+
34
+ type AntiCheatOptions = {
35
+ preserveDom: boolean;
36
+ hasIframes: boolean;
37
+ };
16
38
 
17
39
  type MetaPlayerProps = PropsWithChildren<{
18
40
  activityJSOptions?: LoadOptions;
41
+ antiCheatOptions?: Partial<AntiCheatOptions>;
19
42
  }>;
20
43
 
21
- const MetaPlayer: React.FC<MetaPlayerProps> = (props) => {
44
+ const MetaPlayer: FC<MetaPlayerProps> = (props) => {
22
45
  const primeSettings: Partial<APIOptions> = {
23
46
  ripple: true,
24
47
  };
48
+ const antiCheatOptions: AntiCheatOptions = {
49
+ preserveDom: true,
50
+ hasIframes: false,
51
+ ...props.antiCheatOptions,
52
+ };
25
53
  return (
26
54
  <PrimeReactProvider value={primeSettings}>
27
55
  <Provider store={store}>
@@ -29,7 +57,9 @@ const MetaPlayer: React.FC<MetaPlayerProps> = (props) => {
29
57
  <ErrorBoundary fallback={<div>Une erreur est survenue</div>}>
30
58
  <ActivityJSProvider options={props.activityJSOptions}>
31
59
  <Saver />
32
- <App>{props.children}</App>
60
+ <MetaPlayerContent antiCheatOptions={antiCheatOptions}>
61
+ <App>{props.children}</App>
62
+ </MetaPlayerContent>
33
63
  </ActivityJSProvider>
34
64
  </ErrorBoundary>
35
65
  </Provider>
@@ -37,4 +67,64 @@ const MetaPlayer: React.FC<MetaPlayerProps> = (props) => {
37
67
  );
38
68
  };
39
69
 
70
+ type MetaPlayerContentProps = {
71
+ antiCheatOptions: AntiCheatOptions;
72
+ };
73
+
74
+ const MetaPlayerContent: React.FC<PropsWithChildren<MetaPlayerContentProps>> = (
75
+ props,
76
+ ) => {
77
+ const isDirty = useAppSelector(selectIsDirty);
78
+ const antiCheat = useAppSelector(selectAntiCheat);
79
+ const hasAntiCheat = useAppSelector(selectHasAntiCheat);
80
+ const activityInfo = useAppSelector(selectActivityInfo);
81
+ const returnUrl = useAppSelector(selectReturnUrl);
82
+ const isAntiCheatExitDetectionDisabled = useAppSelector(
83
+ selectIsAntiCheatExitDetectionDisabled,
84
+ );
85
+ const studentName = activityInfo.studentInfo
86
+ ? `${activityInfo.studentInfo.firstName} ${activityInfo.studentInfo.lastName}`
87
+ : "";
88
+ const activityJS = useActivityJS();
89
+
90
+ const touch = () => {
91
+ if (
92
+ activityJS.activitySession == null ||
93
+ activityJS.activitySession.activityBunch.assignmentNode == null
94
+ ) {
95
+ console.error("ActivityJS data not loaded or touch not available");
96
+ return Promise.resolve();
97
+ } else {
98
+ return activityJS.activitySession.activityBunch.assignmentNode.touch();
99
+ }
100
+ };
101
+
102
+ return (
103
+ <CapytaleAntiCheat
104
+ enabled={hasAntiCheat}
105
+ hashedPassword={antiCheat?.passwordHash || null}
106
+ startLocked={antiCheat?.startLocked || null}
107
+ activityTitle={activityInfo.title}
108
+ studentName={studentName}
109
+ isDirty={isDirty}
110
+ returnUrl={returnUrl}
111
+ dbTouchFunction={touch}
112
+ disableExitDetection={isAntiCheatExitDetectionDisabled}
113
+ contentClassName="anti-cheat-content"
114
+ exitDetectionMethod={
115
+ props.antiCheatOptions.hasIframes
116
+ ? ExitDetectionMethod.VISIBILITY_CHANGE
117
+ : ExitDetectionMethod.BLUR
118
+ }
119
+ contentHidingMethod={
120
+ props.antiCheatOptions.preserveDom
121
+ ? ContentHidingMethod.DISPLAY_NONE
122
+ : ContentHidingMethod.REMOVE_FROM_DOM
123
+ }
124
+ >
125
+ {props.children}
126
+ </CapytaleAntiCheat>
127
+ );
128
+ };
129
+
40
130
  export default MetaPlayer;
@@ -0,0 +1,19 @@
1
+ import { FC, useEffect } from "react";
2
+ import { setIsAntiCheatExitDetectionDisabled } from "./activityDataSlice";
3
+ import { useAppDispatch } from "../../app/hooks";
4
+
5
+ type IsAntiCheatExitDetectionDisabledSetterProps = {
6
+ isDisabled: boolean;
7
+ };
8
+
9
+ const IsAntiCheatExitDetectionDisabledSetter: FC<
10
+ IsAntiCheatExitDetectionDisabledSetterProps
11
+ > = (props) => {
12
+ const dispatch = useAppDispatch();
13
+ useEffect(() => {
14
+ dispatch(setIsAntiCheatExitDetectionDisabled(props.isDisabled));
15
+ }, [props]);
16
+ return null;
17
+ };
18
+
19
+ export default IsAntiCheatExitDetectionDisabledSetter;
@@ -45,6 +45,10 @@ interface ActivityJSData {
45
45
  comments: string | null;
46
46
  grading: string | null;
47
47
  workflow: wf | null | undefined;
48
+ antiCheat?: null | {
49
+ passwordHash?: string | null;
50
+ startLocked: boolean;
51
+ };
48
52
  }
49
53
 
50
54
  type SaveState = "idle" | "should-save" | "saving";
@@ -55,6 +59,7 @@ interface UIState {
55
59
  saveState: SaveState;
56
60
  isPlayerDirty: boolean;
57
61
  isMPDirty: boolean;
62
+ isAntiCheatExitDetectionDisabled: boolean;
58
63
  }
59
64
 
60
65
  export interface ActivityDataState
@@ -93,6 +98,8 @@ const initialState: ActivityDataState = {
93
98
  saveState: "idle",
94
99
  isPlayerDirty: false,
95
100
  isMPDirty: false,
101
+
102
+ isAntiCheatExitDetectionDisabled: false,
96
103
  };
97
104
 
98
105
  // If you are not using async thunks you can use the standalone `createSlice`.
@@ -125,6 +132,7 @@ export const activityDataSlice = createAppSlice({
125
132
  state.nid = action.payload.nid;
126
133
  state.activityNid = action.payload.activityNid;
127
134
  state.workflow = action.payload.workflow;
135
+ state.antiCheat = action.payload.antiCheat;
128
136
  },
129
137
  ),
130
138
  setPlayerSettings: create.reducer(
@@ -179,6 +187,11 @@ export const activityDataSlice = createAppSlice({
179
187
  setIsMPDirty: create.reducer((state, action: PayloadAction<boolean>) => {
180
188
  state.isMPDirty = action.payload;
181
189
  }),
190
+ setIsAntiCheatExitDetectionDisabled: create.reducer(
191
+ (state, action: PayloadAction<boolean>) => {
192
+ state.isAntiCheatExitDetectionDisabled = action.payload;
193
+ },
194
+ ),
182
195
  }),
183
196
  // You can define your selectors here. These selectors receive the slice
184
197
  // state as their first argument.
@@ -220,6 +233,17 @@ export const activityDataSlice = createAppSlice({
220
233
  data.mode === "create" ||
221
234
  data.mode === "review" ||
222
235
  (data.mode === "assignment" && data.workflow === "current"),
236
+
237
+ selectAntiCheat: (data) => data.antiCheat,
238
+ selectHasAntiCheat: (data) =>
239
+ !!(
240
+ data.antiCheat &&
241
+ data.antiCheat.passwordHash &&
242
+ data.mode === "assignment" &&
243
+ data.workflow === "current"
244
+ ),
245
+ selectIsAntiCheatExitDetectionDisabled: (data) =>
246
+ data.isAntiCheatExitDetectionDisabled,
223
247
  },
224
248
  });
225
249
 
@@ -238,6 +262,7 @@ export const {
238
262
  setWorkflow,
239
263
  setIsPlayerDirty,
240
264
  setIsMPDirty,
265
+ setIsAntiCheatExitDetectionDisabled,
241
266
  } = activityDataSlice.actions;
242
267
 
243
268
  // Selectors returned by `slice.selectors` take the root state as their first argument.
@@ -265,4 +290,8 @@ export const {
265
290
  selectCanChoosePedagoLayout,
266
291
  selectCanChooseTheme,
267
292
  selectShowSaveButton,
293
+
294
+ selectAntiCheat,
295
+ selectHasAntiCheat,
296
+ selectIsAntiCheatExitDetectionDisabled,
268
297
  } = activityDataSlice.selectors;
@@ -92,6 +92,12 @@ export const ActivityJSProvider: FC<ActivityJSProviderProps> = (props) => {
92
92
  comments: ab.assignmentNode?.comments.value || null,
93
93
  grading: ab.assignmentNode?.grading.value || null,
94
94
  workflow: ab.assignmentNode?.workflow,
95
+ antiCheat: ab.hasAntiCheat
96
+ ? {
97
+ passwordHash: ab.antiCheatPasswd,
98
+ startLocked: !ab.assignmentNode?.isNew,
99
+ }
100
+ : null,
95
101
  }),
96
102
  );
97
103
  tracker.trackActivity(data);
@@ -6,10 +6,10 @@ import {
6
6
  selectGrading,
7
7
  selectInstructions,
8
8
  selectSaveState,
9
- selectWorkflow,
10
9
  setIsMPDirty,
11
10
  setIsPlayerDirty,
12
11
  setSaveState,
12
+ setWorkflow,
13
13
  } from "../activityData/activityDataSlice";
14
14
  import { useActivityJS } from "./ActivityJSProvider";
15
15
  import { Toast } from "primereact/toast";
@@ -24,7 +24,6 @@ const Saver: FC<{}> = () => {
24
24
  const grading = useAppSelector(selectGrading);
25
25
  const beforeSave = useAppSelector(selectBeforeSave);
26
26
  const afterSave = useAppSelector(selectAfterSave);
27
- const workflow = useAppSelector(selectWorkflow);
28
27
 
29
28
  const toast = useRef<Toast>(null);
30
29
 
@@ -85,16 +84,16 @@ const Saver: FC<{}> = () => {
85
84
  ? answerSheetContent
86
85
  : JSON.stringify(answerSheetContent);
87
86
  }
88
- if (
89
- activityJs.activitySession.mode === "assignment" ||
90
- activityJs.activitySession.mode === "review"
91
- ) {
92
- ab.assignmentNode!.workflow = workflow!;
93
- }
94
87
  try {
95
88
  const saveData = await ab.save();
96
89
  console.log("Save return data", saveData);
97
90
  dispatch(setSaveState("idle"));
91
+ if (
92
+ activityJs.activitySession.mode === "assignment" ||
93
+ activityJs.activitySession.mode === "review"
94
+ ) {
95
+ dispatch(setWorkflow(ab.assignmentNode!.workflow));
96
+ }
98
97
  dispatch(setIsPlayerDirty(false));
99
98
  dispatch(setIsMPDirty(false));
100
99
  toast.current!.show({
@@ -10,6 +10,8 @@ import useFullscreen from "../../utils/useFullscreen";
10
10
  import { useActivityJS } from "../activityJS/ActivityJSProvider";
11
11
  import { Dialog } from "primereact/dialog";
12
12
  import capytaleUI from "@capytale/activity.js/backend/capytale/ui";
13
+ import { useAppSelector } from "../../app/hooks";
14
+ import { selectHasAntiCheat } from "../activityData/activityDataSlice";
13
15
 
14
16
  const ActivityMenu: React.FC = memo(() => {
15
17
  const [settingsSidebarVisible, setSettingsSidebarVisible] = useState(false);
@@ -20,6 +22,7 @@ const ActivityMenu: React.FC = memo(() => {
20
22
  const isInIframe = useMemo(() => capytaleUI.isInCapytaletIframe(), []);
21
23
  const [helpDialogVisible, setHelpDialogVisible] = useState(false);
22
24
  const activityJS = useActivityJS();
25
+ const hasAntiCheat = useAppSelector(selectHasAntiCheat);
23
26
  return (
24
27
  <div className={styles.activityMenu}>
25
28
  <ActivityQuickActions />
@@ -35,18 +38,20 @@ const ActivityMenu: React.FC = memo(() => {
35
38
  }}
36
39
  onClick={() => setSettingsSidebarVisible(true)}
37
40
  />
38
- <Button
39
- severity="secondary"
40
- size="small"
41
- icon={isFullscreen ? "pi pi-expand" : "pi pi-expand"}
42
- text
43
- tooltip="Plein écran"
44
- tooltipOptions={{
45
- position: "bottom",
46
- showDelay: settings.TOOLTIP_SHOW_DELAY,
47
- }}
48
- onClick={() => setWantFullscreen((prev) => !prev)}
49
- />
41
+ {!hasAntiCheat && (
42
+ <Button
43
+ severity="secondary"
44
+ size="small"
45
+ icon={isFullscreen ? "pi pi-expand" : "pi pi-expand"}
46
+ text
47
+ tooltip="Plein écran"
48
+ tooltipOptions={{
49
+ position: "bottom",
50
+ showDelay: settings.TOOLTIP_SHOW_DELAY,
51
+ }}
52
+ onClick={() => setWantFullscreen((prev) => !prev)}
53
+ />
54
+ )}
50
55
  {isInIframe && (
51
56
  <Button
52
57
  severity="secondary"
@@ -17,7 +17,6 @@ import {
17
17
  selectShowSaveButton,
18
18
  selectWorkflow,
19
19
  setSaveState,
20
- setWorkflow,
21
20
  } from "../activityData/activityDataSlice";
22
21
  import { copyToClipboard } from "../../utils/clipboard";
23
22
  import settings from "../../settings";
@@ -25,9 +24,11 @@ import { wf } from "@capytale/activity.js/activity/field/workflow";
25
24
  import capytaleUI from "@capytale/activity.js/backend/capytale/ui";
26
25
  import ButtonDoubleIcon from "../../utils/ButtonDoubleIcon";
27
26
  import CardSelector from "../../utils/CardSelector";
27
+ import { useActivityJS } from "../activityJS/ActivityJSProvider";
28
28
 
29
29
  const CapytaleMenu: React.FC = () => {
30
30
  const dispatch = useAppDispatch();
31
+ const activityJS = useActivityJS();
31
32
  const sharingInfo = useAppSelector(selectSharingInfo);
32
33
  const saveState = useAppSelector(selectSaveState);
33
34
  const windowsSize = useWindowSize();
@@ -46,7 +47,7 @@ const CapytaleMenu: React.FC = () => {
46
47
  const isInIframe = useMemo(() => capytaleUI.isInCapytaletIframe(), []);
47
48
  const toast = useRef<Toast>(null);
48
49
  const changeWorkflow = (value: wf) => {
49
- dispatch(setWorkflow(value));
50
+ activityJS.activitySession!.activityBunch.assignmentNode!.workflow = value;
50
51
  dispatch(setSaveState("should-save"));
51
52
  };
52
53
  const confirmFinishAssignment = () => {
@@ -80,25 +81,27 @@ const CapytaleMenu: React.FC = () => {
80
81
  }}
81
82
  />
82
83
  )}
83
- {showSaveButton && (
84
- <Button
85
- label={isQuiteSmall ? undefined : "Enregistrer"}
86
- disabled={!isDirty}
87
- severity={"warning"}
88
- size="small"
89
- icon="pi pi-save"
90
- loading={saveState !== "idle"}
84
+ {
85
+ showSaveButton && (
86
+ <Button
87
+ label={isQuiteSmall ? undefined : "Enregistrer"}
88
+ disabled={!isDirty}
89
+ severity={"warning"}
90
+ size="small"
91
+ icon="pi pi-save"
92
+ loading={saveState !== "idle"}
93
+ onClick={() => {
94
+ dispatch(setSaveState("should-save"));
95
+ }}
96
+ />
97
+ ) /**
91
98
  tooltip={isDirty ? "Enregistrer l'activité" : "Rien à enregistrer"}
92
99
  tooltipOptions={{
93
100
  position: "bottom",
94
101
  showDelay: settings.TOOLTIP_SHOW_DELAY,
95
102
  showOnDisabled: true,
96
- }}
97
- onClick={() => {
98
- dispatch(setSaveState("should-save"));
99
- }}
100
- />
101
- )}
103
+ }} */
104
+ }
102
105
 
103
106
  {mode === "create" && (
104
107
  <SplitButton
package/src/index.css CHANGED
@@ -87,3 +87,13 @@ body,
87
87
  display: block;
88
88
  }
89
89
  }
90
+
91
+ .anti-cheat-content {
92
+ height: 100vh;
93
+ width: 100vw;
94
+ overflow: hidden;
95
+ }
96
+
97
+ .anti-cheat h1:first-child {
98
+ margin-top: 0;
99
+ }