Skip to content
Extraits de code Groupes Projets

Comparer les révisions

Les modifications sont affichées comme si la révision source était fusionnée avec la révision cible. En savoir plus sur la comparaison des révisions.

Source

Sélectionner le projet cible
No results found

Cible

Sélectionner le projet cible
  • capytale/meta-player
  • romain.casati/meta-player
2 résultats
Afficher les modifications
Validations sur la source (2)
# vite-template-redux
# Meta Player Capytale
Uses [Vite](https://vitejs.dev/), [Vitest](https://vitest.dev/), and [React Testing Library](https://github.com/testing-library/react-testing-library) to create a modern [React](https://react.dev/) app compatible with [Create React App](https://create-react-app.dev/)
## To put in player HTML
```sh
npx degit reduxjs/redux-templates/packages/vite-template-redux my-app
- CSS used by the meta-player:
```html
<link id="theme-link" rel="stylesheet" href="https://cdn.ac-paris.fr/capytale/meta-player/themes/lara-light-blue/theme.css">
```
## Goals
- Easy migration from Create React App or Vite
- As beginner friendly as Create React App
- Optimized performance compared to Create React App
- Customizable without ejecting
## Scripts
- `dev`/`start` - start dev server and open browser
- `build` - build for production
- `preview` - locally preview production build
- `test` - launch test runner
## Inspiration
- [Create React App](https://github.com/facebook/create-react-app/tree/main/packages/cra-template)
- [Vite](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react)
- [Vitest](https://github.com/vitest-dev/vitest/tree/main/examples/react-testing-lib)
- If using the attached files preview capability:
```html
<link rel="stylesheet" href="https://cdn.ac-paris.fr/highlight/styles/default.css">
<script src="https://cdn.ac-paris.fr/highlight/highlight.min.js"></script>
```
\ No newline at end of file
{
"name": "@capytale/meta-player",
"version": "0.5.12",
"version": "0.5.13",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@capytale/meta-player",
"version": "0.5.12",
"version": "0.5.13",
"dependencies": {
"@capytale/activity.js": "^3.1.14",
"@capytale/capytale-anti-triche": "^0.2.1",
"@capytale/capytale-rich-text-editor": "^0.4.3",
"@reduxjs/toolkit": "^2.0.1",
"@uidotdev/usehooks": "^2.4.1",
"mime": "^4.0.6",
"primeicons": "^7.0.0",
"primereact": "^10.8.3",
"react-dropzone": "^14.2.9",
......@@ -3182,6 +3183,20 @@
"node": ">=8.6"
}
},
"node_modules/mime": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/mime/-/mime-4.0.6.tgz",
"integrity": "sha512-4rGt7rvQHBbaSOF9POGkk1ocRP16Md1x36Xma8sz8h8/vfCUI2OtEIeCqe4Ofes853x4xDoPiFLIT47J5fI/7A==",
"funding": [
"https://github.com/sponsors/broofa"
],
"bin": {
"mime": "bin/cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
......
{
"name": "@capytale/meta-player",
"version": "0.5.12",
"version": "0.5.13",
"type": "module",
"scripts": {
"dev": "vite",
......@@ -21,6 +21,7 @@
"@capytale/capytale-rich-text-editor": "^0.4.3",
"@reduxjs/toolkit": "^2.0.1",
"@uidotdev/usehooks": "^2.4.1",
"mime": "^4.0.6",
"primeicons": "^7.0.0",
"primereact": "^10.8.3",
"react-dropzone": "^14.2.9",
......
......@@ -26,6 +26,7 @@ import {
import settings from "./settings";
import ReviewNavbar from "./features/navbar/review-navbar";
import { useSave } from "./features/activityData/hooks";
import PreviewDialog from "./features/functionalities/PreviewDialog";
type AppProps = PropsWithChildren<{}>;
......@@ -130,6 +131,7 @@ const App: FC<AppProps> = (props) => {
</SplitterPanel>
</Splitter>
</div>
<PreviewDialog />
</div>
);
};
......
import { FC, memo, useEffect, useRef, useState } from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import {
PreviewFile,
selectPreviewFile,
setPreviewFile,
} from "./functionalitiesSlice";
import { Dialog } from "primereact/dialog";
const HighlightedCode = memo(({ code }: { code: string }) => {
const codeRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (codeRef.current != null) {
if ((window as any).hljs) {
(window as any).hljs.highlightBlock(codeRef.current);
} else {
console.warn("Highlight.js not loaded");
}
}
}, [code]);
return (
<pre>
<code
ref={codeRef}
style={{ padding: 0, background: "white", maxWidth: "100%" }}
>
{code}
</code>
</pre>
);
});
const PreviewDialog: FC = () => {
const previewFile = useAppSelector(selectPreviewFile);
const [persistentPreviewFile, setPersistentPreviewFile] =
useState<PreviewFile | null>(null);
const dispatch = useAppDispatch();
const visible = previewFile != null;
useEffect(() => {
if (previewFile != null) {
setPersistentPreviewFile(previewFile);
}
}, [previewFile]);
if (persistentPreviewFile == null) {
return null;
}
return (
<Dialog
visible={visible}
header={persistentPreviewFile.title}
onHide={() => {
dispatch(setPreviewFile(null));
}}
style={{ maxWidth: "100%" }}
>
{persistentPreviewFile.type === "text" ? (
<HighlightedCode code={persistentPreviewFile.content} />
) : (
<img
src={persistentPreviewFile.url}
alt={persistentPreviewFile.title}
style={{
maxWidth: "100%",
}}
/>
)}
</Dialog>
);
};
export default PreviewDialog;
......@@ -34,9 +34,15 @@ export type AttachedFilesOptions = {
onViewFiles?: () => any;
};
export interface FunctionalitiesState {
export type PreviewFile = { title: string } & (
{ type: "text", content: string } |
{ type: "image", url: string }
);
export type FunctionalitiesState = {
attachedFilesOptions: AttachedFilesOptions;
attachedFilesRefresher: number;
previewFile: PreviewFile | null;
}
export const defaultAttachedFilesOptions: AttachedFilesOptions = {
......@@ -46,6 +52,7 @@ export const defaultAttachedFilesOptions: AttachedFilesOptions = {
export const initialState: FunctionalitiesState = {
attachedFilesOptions: defaultAttachedFilesOptions,
attachedFilesRefresher: 0,
previewFile: null,
};
// If you are not using async thunks you can use the standalone `createSlice`.
......@@ -61,6 +68,9 @@ export const functionalitiesSlice = createAppSlice({
refreshAttachedFiles: create.reducer((state) => {
state.attachedFilesRefresher += 1;
}),
setPreviewFile: create.reducer((state, action: PayloadAction<PreviewFile | null>) => {
state.previewFile = action.payload;
}),
}),
// You can define your selectors here. These selectors receive the slice
// state as their first argument.
......@@ -68,6 +78,7 @@ export const functionalitiesSlice = createAppSlice({
selectAttachedFilesOptions: (state) => state.attachedFilesOptions,
selectAttachedFilesEnabled: (state) => state.attachedFilesOptions.enabled,
selectAttachedFilesRefresher: (state) => state.attachedFilesRefresher,
selectPreviewFile: (state) => state.previewFile,
},
});
......@@ -75,6 +86,7 @@ export const functionalitiesSlice = createAppSlice({
export const {
setAttachedFilesOptions,
refreshAttachedFiles,
setPreviewFile,
} = functionalitiesSlice.actions;
// Selectors returned by `slice.selectors` take the root state as their first argument.
......@@ -82,4 +94,5 @@ export const {
selectAttachedFilesOptions,
selectAttachedFilesEnabled,
selectAttachedFilesRefresher,
selectPreviewFile,
} = functionalitiesSlice.selectors;
import { useMemo } from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks"
import { useActivityJS } from "../activityJS/ActivityJSProvider";
import { AttachedFileData, refreshAttachedFiles, selectAttachedFilesOptions, selectAttachedFilesRefresher } from "./functionalitiesSlice"
import { AttachedFileData, refreshAttachedFiles, selectAttachedFilesOptions, selectAttachedFilesRefresher, setPreviewFile } from "./functionalitiesSlice"
export const useAttachedFiles = () => {
const attachedFilesOptions = useAppSelector(selectAttachedFilesOptions);
......@@ -36,9 +36,35 @@ export const useAttachedFiles = () => {
}, [filesData, attachedFilesOptions, attachedFilesRefresher]);
return treatedFilesData;
}
};
export const useRefreshAttachedFiles = () => {
const dispatch = useAppDispatch();
return () => dispatch(refreshAttachedFiles());
}
\ No newline at end of file
};
export const usePreviewTextFile = () => {
const dispatch = useAppDispatch();
return (title: string, content: string) => {
dispatch(
setPreviewFile({
title,
type: "text",
content,
}),
);
}
};
export const usePreviewImageFile = () => {
const dispatch = useAppDispatch();
return (title: string, url: string) => {
dispatch(
setPreviewFile({
title,
type: "image",
url,
}),
);
}
};
import { FC, useRef } from "react";
import { useAttachedFiles } from "../../functionalities/hooks";
import {
useAttachedFiles,
usePreviewImageFile,
usePreviewTextFile,
} from "../../functionalities/hooks";
import { useAppSelector } from "../../../app/hooks";
import {
AttachedFileData,
......@@ -12,6 +16,7 @@ import styles from "./AttachedFilesSidebarContent.module.scss";
import { copyToClipboard } from "../../../utils/clipboard";
import { downloadFile } from "../../../utils/download";
import { useFileUpload } from "use-file-upload";
import mime from "mime";
const AttachedFilesSidebarContent: FC = () => {
const filesData = useAttachedFiles();
......@@ -72,6 +77,8 @@ const AttachedFileLinks: FC<{ fileData: AttachedFileData }> = ({
}) => {
const attachedFilesOptions = useAppSelector(selectAttachedFilesOptions);
const toast = useRef<Toast>(null);
const previewTextFile = usePreviewTextFile();
const previewImageFile = usePreviewImageFile();
return (
<div className={styles.fileRow}>
......@@ -80,18 +87,31 @@ const AttachedFileLinks: FC<{ fileData: AttachedFileData }> = ({
label={fileData.name}
severity={fileData.isTemporary ? "warning" : "secondary"}
disabled={fileData.interactMode === "none"}
onClick={() => {
onClick={async () => {
if (fileData.interactMode === "custom") {
attachedFilesOptions.customHandlers?.interactWithFile?.(fileData);
} else if (fileData.interactMode === "download") {
window.open(fileData.urlOrId, "_blank")?.focus();
} else if (fileData.interactMode === "preview") {
toast.current?.show({
severity: "info",
summary: "Aperçu non disponible",
detail: fileData.name,
life: 3000,
});
const mimeType = mime.getType(fileData.name);
if (
mimeType?.startsWith("text") ||
mimeType === "application/json"
) {
// fetch file content
const response = await fetch(fileData.urlOrId);
const content = await response.text();
previewTextFile(fileData.name, content);
} else if (mimeType?.startsWith("image")) {
previewImageFile(fileData.name, fileData.urlOrId);
} else {
toast.current?.show({
severity: "info",
summary: `Impossible de prévisualiser {fileData.name}`,
detail: `Aperçu non disponible pour ce type de fichier (${mimeType})`,
life: 3000,
});
}
}
}}
className={styles.fileInteraction}
......
......@@ -13,7 +13,11 @@ import {
useActivityJS,
useActivityJsEssentials,
} from "./features/activityJS/ActivityJSProvider";
import { useNotifyIsDirty, useCanSave, useSave } from "./features/activityData/hooks";
import {
useNotifyIsDirty,
useCanSave,
useSave,
} from "./features/activityData/hooks";
import { useOrientation } from "./features/layout/hooks";
import { ActivitySidebarActionsSetter } from "./features/navbar/sidebars/ActivitySidebarActions";
import { ActivityQuickActionsSetter } from "./features/navbar/activity-menu/ActivityQuickActions";
......@@ -22,6 +26,10 @@ import IsDirtySetter from "./features/activityData/IsDirtySetter";
import AttachedFilesFunctionality from "./features/functionalities/AttachedFilesFunctionality";
import { useRefreshAttachedFiles } from "./features/functionalities/hooks";
import { useActivitySettings } from "./features/activitySettings/hooks";
import {
usePreviewTextFile,
usePreviewImageFile,
} from "./features/functionalities/hooks";
import { Toast } from "./external/prime";
import type { ToastMessage } from "./external/prime";
import type {
......@@ -46,6 +54,8 @@ export {
useOrientation,
useThemeType,
useActivitySettings,
usePreviewTextFile,
usePreviewImageFile,
ActivitySidebarActionsSetter,
ActivityQuickActionsSetter,
ActivitySettingsSetter,
......