@capytale/meta-player 0.5.1 → 0.5.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capytale/meta-player",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -2,22 +2,18 @@ import { FC, useEffect } from "react";
2
2
  import { AttachedFilesOptions, setAttachedFilesOptions } from "./functionalitiesSlice";
3
3
  import { useAppDispatch } from "../../app/hooks";
4
4
 
5
- type AttachedFilesFunctionalityProps = {
6
- options: Omit<AttachedFilesOptions, "enabled">;
7
- };
5
+ type AttachedFilesFunctionalityProps = Omit<AttachedFilesOptions, "enabled">;
8
6
 
9
- const AttachedFilesFunctionality: FC<AttachedFilesFunctionalityProps> = ({
10
- options,
11
- }) => {
7
+ const AttachedFilesFunctionality: FC<AttachedFilesFunctionalityProps> = (props) => {
12
8
  const dispatch = useAppDispatch();
13
9
  useEffect(() => {
14
10
  dispatch(setAttachedFilesOptions({
15
- ...options,
11
+ ...props,
16
12
  enabled: true,
17
13
  }));
18
14
  return () => {
19
15
  dispatch(setAttachedFilesOptions({
20
- ...options,
16
+ ...props,
21
17
  enabled: false,
22
18
  }));
23
19
  };
@@ -4,26 +4,39 @@ import { createAppSlice } from "../../app/createAppSlice";
4
4
  export type AttachedFileData = {
5
5
  name: string;
6
6
  isTemporary: boolean;
7
- canDownload: boolean;
8
- canCopyLink: boolean;
9
- interactionMode: "download" | "preview" | "custom" | "none";
7
+ downloadMode: "default" | "custom" | "none";
8
+ copyLinkMode: "default" | "custom" | "none";
9
+ interactMode: "download" | "preview" | "custom" | "none";
10
10
  urlOrId: string;
11
11
  }
12
12
 
13
+ export type UploadedFileInfo = {
14
+ source: URL,
15
+ name: string,
16
+ size: number,
17
+ file: File,
18
+ };
19
+
13
20
  export type AttachedFilesOptions = {
14
21
  enabled: boolean;
15
22
  uploadTemporaryFiles?: {
16
- mimeTypes: string[];
17
- handler: (files: File[]) => any;
23
+ uploadButtonLabel?: string;
24
+ mimeTypes?: string[];
25
+ multiple: boolean;
26
+ handler: (fileInfos: UploadedFileInfo[]) => any;
18
27
  };
19
- middleware?: {
20
- listFiles: (originalFiles: AttachedFileData[]) => AttachedFileData[];
21
- handleFileClick?: ((originalFile: AttachedFileData) => any);
28
+ listFilesMiddleware?: (originalFiles: AttachedFileData[]) => AttachedFileData[];
29
+ customHandlers?: {
30
+ downloadFile?: ((originalFile: AttachedFileData) => any);
31
+ copyFileLink?: ((originalFile: AttachedFileData) => any);
32
+ interactWithFile?: ((originalFile: AttachedFileData) => any);
22
33
  };
34
+ onViewFiles?: () => any;
23
35
  };
24
36
 
25
37
  export interface FunctionalitiesState {
26
38
  attachedFilesOptions: AttachedFilesOptions;
39
+ attachedFilesRefresher: number;
27
40
  }
28
41
 
29
42
  export const defaultAttachedFilesOptions: AttachedFilesOptions = {
@@ -31,7 +44,8 @@ export const defaultAttachedFilesOptions: AttachedFilesOptions = {
31
44
  };
32
45
 
33
46
  export const initialState: FunctionalitiesState = {
34
- attachedFilesOptions: defaultAttachedFilesOptions
47
+ attachedFilesOptions: defaultAttachedFilesOptions,
48
+ attachedFilesRefresher: 0,
35
49
  };
36
50
 
37
51
  // If you are not using async thunks you can use the standalone `createSlice`.
@@ -44,22 +58,28 @@ export const functionalitiesSlice = createAppSlice({
44
58
  setAttachedFilesOptions: create.reducer((state, action: PayloadAction<AttachedFilesOptions>) => {
45
59
  state.attachedFilesOptions = action.payload;
46
60
  }),
61
+ refreshAttachedFiles: create.reducer((state) => {
62
+ state.attachedFilesRefresher += 1;
63
+ }),
47
64
  }),
48
65
  // You can define your selectors here. These selectors receive the slice
49
66
  // state as their first argument.
50
67
  selectors: {
51
68
  selectAttachedFilesOptions: (state) => state.attachedFilesOptions,
52
69
  selectAttachedFilesEnabled: (state) => state.attachedFilesOptions.enabled,
70
+ selectAttachedFilesRefresher: (state) => state.attachedFilesRefresher,
53
71
  },
54
72
  });
55
73
 
56
74
  // Action creators are generated for each case reducer function.
57
75
  export const {
58
76
  setAttachedFilesOptions,
77
+ refreshAttachedFiles,
59
78
  } = functionalitiesSlice.actions;
60
79
 
61
80
  // Selectors returned by `slice.selectors` take the root state as their first argument.
62
81
  export const {
63
82
  selectAttachedFilesOptions,
64
83
  selectAttachedFilesEnabled,
84
+ selectAttachedFilesRefresher,
65
85
  } = functionalitiesSlice.selectors;
@@ -1,17 +1,18 @@
1
1
  import { useMemo } from "react";
2
- import { useAppSelector } from "../../app/hooks"
2
+ import { useAppDispatch, useAppSelector } from "../../app/hooks"
3
3
  import { useActivityJS } from "../activityJS/ActivityJSProvider";
4
- import { AttachedFileData, selectAttachedFilesOptions } from "./functionalitiesSlice"
4
+ import { AttachedFileData, refreshAttachedFiles, selectAttachedFilesOptions, selectAttachedFilesRefresher } from "./functionalitiesSlice"
5
5
 
6
6
  export const useAttachedFiles = () => {
7
7
  const attachedFilesOptions = useAppSelector(selectAttachedFilesOptions);
8
+ const attachedFilesRefresher = useAppSelector(selectAttachedFilesRefresher);
8
9
 
9
10
  const activityJS = useActivityJS();
10
11
  const files = useMemo(
11
12
  () =>
12
13
  activityJS.activitySession?.activityBunch.activityNode.attached_files
13
14
  .items || [],
14
- [activityJS],
15
+ [activityJS, attachedFilesRefresher],
15
16
  );
16
17
  const filesData = useMemo<AttachedFileData[]>(() => {
17
18
  return files.map((file) => {
@@ -20,19 +21,24 @@ export const useAttachedFiles = () => {
20
21
  name: name,
21
22
  urlOrId: file,
22
23
  isTemporary: false,
23
- canDownload: true,
24
- canCopyLink: true,
25
- interactionMode: "preview",
24
+ downloadMode: "default",
25
+ copyLinkMode: "default",
26
+ interactMode: "preview",
26
27
  };
27
28
  });
28
29
  }, [files]);
29
30
 
30
31
  const treatedFilesData = useMemo(() => {
31
- if (!attachedFilesOptions.enabled || !attachedFilesOptions.middleware?.listFiles) {
32
+ if (!attachedFilesOptions.enabled || !attachedFilesOptions.listFilesMiddleware) {
32
33
  return filesData;
33
34
  }
34
- return attachedFilesOptions.middleware.listFiles(filesData);
35
- }, [filesData, attachedFilesOptions]);
35
+ return attachedFilesOptions.listFilesMiddleware(filesData);
36
+ }, [filesData, attachedFilesOptions, attachedFilesRefresher]);
36
37
 
37
38
  return treatedFilesData;
39
+ }
40
+
41
+ export const useRefreshAttachedFiles = () => {
42
+ const dispatch = useAppDispatch();
43
+ return () => dispatch(refreshAttachedFiles());
38
44
  }
@@ -15,7 +15,7 @@ import { selectHasAntiCheat } from "../activityData/activityDataSlice";
15
15
  import AttachedFilesSidebarContent from "./AttachedFilesSidebarContent";
16
16
  import { Badge } from "primereact/badge";
17
17
  import { useAttachedFiles } from "../functionalities/hooks";
18
- import { selectAttachedFilesEnabled } from "../functionalities/functionalitiesSlice";
18
+ import { selectAttachedFilesEnabled, selectAttachedFilesOptions } from "../functionalities/functionalitiesSlice";
19
19
 
20
20
  const ActivityMenu: React.FC = memo(() => {
21
21
  const [settingsSidebarVisible, setSettingsSidebarVisible] = useState(false);
@@ -31,6 +31,7 @@ const ActivityMenu: React.FC = memo(() => {
31
31
  const hasAntiCheat = useAppSelector(selectHasAntiCheat);
32
32
  const attachedFiles = useAttachedFiles();
33
33
  const attachedFilesEnabled = useAppSelector(selectAttachedFilesEnabled);
34
+ const attachedFilesOptions = useAppSelector(selectAttachedFilesOptions);
34
35
  return (
35
36
  <div className={styles.activityMenu}>
36
37
  <ActivityQuickActions />
@@ -52,7 +53,10 @@ const ActivityMenu: React.FC = memo(() => {
52
53
  showDelay: settings.TOOLTIP_SHOW_DELAY,
53
54
  }}
54
55
  className="p-overlay-badge"
55
- onClick={() => setAttachedFilesSidebarVisible(true)}
56
+ onClick={() => {
57
+ attachedFilesOptions.onViewFiles?.();
58
+ setAttachedFilesSidebarVisible(true);
59
+ }}
56
60
  />
57
61
  )}
58
62
  <Button
@@ -1,5 +1,14 @@
1
+ .sidebarContent {
2
+ height: 100%;
3
+ overflow-y: auto;
4
+ display: flex;
5
+ flex-direction: column;
6
+ }
7
+
1
8
  .attachedFiles {
2
9
  margin: 4px 0;
10
+ flex-grow: 1;
11
+ flex-shrink: 1;
3
12
  }
4
13
 
5
14
  .fileRow {
@@ -24,3 +33,11 @@
24
33
  flex-shrink: 0;
25
34
  flex-grow: 0;
26
35
  }
36
+
37
+ .uploadFilesButtonContainer {
38
+ flex-shrink: 0;
39
+ flex-grow: 0;
40
+ & > * {
41
+ width: 100%;
42
+ }
43
+ }
@@ -11,15 +11,57 @@ import { Toast } from "primereact/toast";
11
11
  import styles from "./AttachedFilesSidebarContent.module.scss";
12
12
  import { copyToClipboard } from "../../utils/clipboard";
13
13
  import { downloadFile } from "../../utils/download";
14
+ import { useFileUpload } from "use-file-upload";
14
15
 
15
16
  const AttachedFilesSidebarContent: FC = () => {
16
17
  const filesData = useAttachedFiles();
18
+ const attachedFilesOptions = useAppSelector(selectAttachedFilesOptions);
19
+ const [_, selectFile] = useFileUpload();
17
20
 
18
21
  return (
19
- <div className={styles.attachedFiles}>
20
- {filesData.map((file) => (
21
- <AttachedFileLinks key={file.name} fileData={file} />
22
- ))}
22
+ <div className={styles.sidebarContent}>
23
+ <div className={styles.attachedFiles}>
24
+ {filesData.map((file) => (
25
+ <AttachedFileLinks key={file.name} fileData={file} />
26
+ ))}
27
+ </div>
28
+ {attachedFilesOptions.uploadTemporaryFiles && (
29
+ <div className={styles.uploadFilesButtonContainer}>
30
+ <Button
31
+ icon="pi pi-upload"
32
+ label={
33
+ attachedFilesOptions.uploadTemporaryFiles?.uploadButtonLabel ||
34
+ "Ajouter fichier temporaire"
35
+ }
36
+ onClick={() => {
37
+ selectFile(
38
+ {
39
+ multiple: attachedFilesOptions.uploadTemporaryFiles?.multiple,
40
+ accept:
41
+ attachedFilesOptions.uploadTemporaryFiles?.mimeTypes?.join(
42
+ ",",
43
+ ) || undefined,
44
+ },
45
+ (filesOrFiles) => {
46
+ if (Array.isArray(filesOrFiles)) {
47
+ attachedFilesOptions.uploadTemporaryFiles?.handler(
48
+ filesOrFiles,
49
+ );
50
+ } else {
51
+ /* Deconstructing the object here so that if there is a
52
+ bug, the error will be caught here and not in the handler */
53
+ const { source, name, size, file } = filesOrFiles;
54
+ attachedFilesOptions.uploadTemporaryFiles?.handler([
55
+ { source, name, size, file },
56
+ ]);
57
+ }
58
+ },
59
+ );
60
+ }}
61
+ className={styles.uploadButton}
62
+ />
63
+ </div>
64
+ )}
23
65
  </div>
24
66
  );
25
67
  };
@@ -36,13 +78,13 @@ const AttachedFileLinks: FC<{ fileData: AttachedFileData }> = ({
36
78
  <Button
37
79
  label={fileData.name}
38
80
  severity={fileData.isTemporary ? "warning" : "secondary"}
39
- disabled={fileData.interactionMode === "none"}
81
+ disabled={fileData.interactMode === "none"}
40
82
  onClick={() => {
41
- if (fileData.interactionMode === "custom") {
42
- attachedFilesOptions.middleware?.handleFileClick?.(fileData);
43
- } else if (fileData.interactionMode === "download") {
83
+ if (fileData.interactMode === "custom") {
84
+ attachedFilesOptions.customHandlers?.interactWithFile?.(fileData);
85
+ } else if (fileData.interactMode === "download") {
44
86
  window.open(fileData.urlOrId, "_blank")?.focus();
45
- } else if (fileData.interactionMode === "preview") {
87
+ } else if (fileData.interactMode === "preview") {
46
88
  toast.current?.show({
47
89
  severity: "info",
48
90
  summary: "Aperçu non disponible",
@@ -54,46 +96,56 @@ const AttachedFileLinks: FC<{ fileData: AttachedFileData }> = ({
54
96
  className={styles.fileInteraction}
55
97
  text
56
98
  />
57
- {fileData.canDownload && (
99
+ {fileData.downloadMode !== "none" && (
58
100
  <Button
59
101
  icon="pi pi-download"
60
102
  severity="secondary"
61
103
  onClick={() => {
62
- downloadFile(fileData.urlOrId, fileData.name);
63
- toast.current?.show({
64
- severity: "success",
65
- summary: "Téléchargement lancé",
66
- detail: fileData.name,
67
- life: 3000,
68
- });
104
+ if (fileData.downloadMode === "custom") {
105
+ attachedFilesOptions.customHandlers?.downloadFile?.(fileData);
106
+ return;
107
+ } else if (fileData.downloadMode === "default") {
108
+ downloadFile(fileData.urlOrId, fileData.name);
109
+ toast.current?.show({
110
+ severity: "success",
111
+ summary: "Téléchargement lancé",
112
+ detail: fileData.name,
113
+ life: 3000,
114
+ });
115
+ }
69
116
  }}
70
117
  className={styles.fileSmallInteraction}
71
118
  text
72
119
  />
73
120
  )}
74
- {fileData.canCopyLink && (
121
+ {fileData.copyLinkMode !== "none" && (
75
122
  <Button
76
123
  icon="pi pi-link"
77
124
  severity="secondary"
78
125
  onClick={() => {
79
- // copy link to clipboard
80
- copyToClipboard(fileData.urlOrId, (success, error) => {
81
- if (success) {
82
- toast.current?.show({
83
- severity: "success",
84
- summary: "Lien copié",
85
- detail: fileData.name,
86
- life: 3000,
87
- });
88
- } else {
89
- toast.current?.show({
90
- severity: "error",
91
- summary: "Erreur lors de la copie du lien",
92
- detail: error,
93
- life: 5000,
94
- });
95
- }
96
- });
126
+ if (fileData.copyLinkMode === "custom") {
127
+ attachedFilesOptions.customHandlers?.copyFileLink?.(fileData);
128
+ return;
129
+ } else if (fileData.copyLinkMode === "default") {
130
+ // copy link to clipboard
131
+ copyToClipboard(fileData.urlOrId, (success, error) => {
132
+ if (success) {
133
+ toast.current?.show({
134
+ severity: "success",
135
+ summary: "Lien copié",
136
+ detail: fileData.name,
137
+ life: 3000,
138
+ });
139
+ } else {
140
+ toast.current?.show({
141
+ severity: "error",
142
+ summary: "Erreur lors de la copie du lien",
143
+ detail: error,
144
+ life: 5000,
145
+ });
146
+ }
147
+ });
148
+ }
97
149
  }}
98
150
  style={{
99
151
  flexGrow: 0,
package/src/index.tsx CHANGED
@@ -20,8 +20,10 @@ import { ActivityQuickActionsSetter } from "./features/navbar/ActivityQuickActio
20
20
  import ActivitySettingsSetter from "./features/activitySettings/ActivitySettingsSetter";
21
21
  import IsDirtySetter from "./features/activityData/IsDirtySetter";
22
22
  import AttachedFilesFunctionality from "./features/functionalities/AttachedFilesFunctionality";
23
+ import { useRefreshAttachedFiles } from "./features/functionalities/hooks";
23
24
  import { Toast } from "./external/prime";
24
25
  import type { ToastMessage } from "./external/prime";
26
+ import type { AttachedFileData, UploadedFileInfo } from "./features/functionalities/functionalitiesSlice";
25
27
 
26
28
  export {
27
29
  MetaPlayer,
@@ -43,6 +45,7 @@ export {
43
45
  ActivitySettingsSetter,
44
46
  IsDirtySetter,
45
47
  AttachedFilesFunctionality,
48
+ useRefreshAttachedFiles,
46
49
  Toast,
47
50
  };
48
- export type { ToastMessage };
51
+ export type { AttachedFileData, UploadedFileInfo, ToastMessage };