From 4bb2cf65e6a0695ad437e0d788f53842022df8fe Mon Sep 17 00:00:00 2001 From: LASER-Yi Date: Fri, 3 Jun 2022 11:24:19 +0800 Subject: [PATCH] Improve the upload behavior --- frontend/package-lock.json | 62 ++--- frontend/package.json | 2 +- .../src/components/inputs/DropOverlay.tsx | 126 +++++++++ frontend/src/components/inputs/File.tsx | 78 ------ frontend/src/components/inputs/index.ts | 1 + frontend/src/pages/Episodes/index.tsx | 247 ++++++++--------- frontend/src/pages/Movies/Details/index.tsx | 252 ++++++++++-------- 7 files changed, 411 insertions(+), 357 deletions(-) create mode 100644 frontend/src/components/inputs/DropOverlay.tsx delete mode 100644 frontend/src/components/inputs/File.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6afd5c631..5208b3528 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -25,7 +25,6 @@ "@fortawesome/free-regular-svg-icons": "^6", "@fortawesome/free-solid-svg-icons": "^6", "@fortawesome/react-fontawesome": "^0.1", - "@mantine/dropzone": "^4", "@mantine/modals": "^4", "@mantine/notifications": "^4", "@testing-library/jest-dom": "latest", @@ -49,6 +48,7 @@ "prettier": "^2", "prettier-plugin-organize-imports": "^2", "pretty-quick": "^3.1", + "react-dropzone": "^14.2.1", "react-table": "^7", "recharts": "^2.0.8", "sass": "^1", @@ -2332,21 +2332,6 @@ "react-dom": ">=16.8.0" } }, - "node_modules/@mantine/dropzone": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-4.2.7.tgz", - "integrity": "sha512-eQTVX5hClHNYR6UzNa4P559LsbfdqNHJUu/P7TiIvwIHqKRVjDRkuSZMciSpWqBueBfzrdCZQ32exb3l299Xfg==", - "dev": true, - "dependencies": { - "react-dropzone": "^11.4.2" - }, - "peerDependencies": { - "@mantine/core": "4.2.7", - "@mantine/hooks": "4.2.7", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, "node_modules/@mantine/hooks": { "version": "4.2.7", "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-4.2.7.tgz", @@ -5348,15 +5333,15 @@ } }, "node_modules/file-selector": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz", - "integrity": "sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", "dev": true, "dependencies": { - "tslib": "^2.0.3" + "tslib": "^2.4.0" }, "engines": { - "node": ">= 10" + "node": ">= 12" } }, "node_modules/fill-range": { @@ -7163,20 +7148,20 @@ } }, "node_modules/react-dropzone": { - "version": "11.7.1", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.7.1.tgz", - "integrity": "sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ==", + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.1.tgz", + "integrity": "sha512-jzX6wDtAjlfwZ+Fbg+G17EszWUkQVxhMTWMfAC9qSUq7II2pKglHA8aarbFKl0mLpRPDaNUcy+HD/Sf4gkf76Q==", "dev": true, "dependencies": { "attr-accept": "^2.2.2", - "file-selector": "^0.4.0", + "file-selector": "^0.6.0", "prop-types": "^15.8.1" }, "engines": { "node": ">= 10.13" }, "peerDependencies": { - "react": ">= 16.8" + "react": ">= 16.8 || 18.0.0" } }, "node_modules/react-error-boundary": { @@ -10230,15 +10215,6 @@ "react-textarea-autosize": "^8.3.2" } }, - "@mantine/dropzone": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-4.2.7.tgz", - "integrity": "sha512-eQTVX5hClHNYR6UzNa4P559LsbfdqNHJUu/P7TiIvwIHqKRVjDRkuSZMciSpWqBueBfzrdCZQ32exb3l299Xfg==", - "dev": true, - "requires": { - "react-dropzone": "^11.4.2" - } - }, "@mantine/hooks": { "version": "4.2.7", "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-4.2.7.tgz", @@ -12411,12 +12387,12 @@ } }, "file-selector": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz", - "integrity": "sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", "dev": true, "requires": { - "tslib": "^2.0.3" + "tslib": "^2.4.0" } }, "fill-range": { @@ -13727,13 +13703,13 @@ } }, "react-dropzone": { - "version": "11.7.1", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.7.1.tgz", - "integrity": "sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ==", + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.1.tgz", + "integrity": "sha512-jzX6wDtAjlfwZ+Fbg+G17EszWUkQVxhMTWMfAC9qSUq7II2pKglHA8aarbFKl0mLpRPDaNUcy+HD/Sf4gkf76Q==", "dev": true, "requires": { "attr-accept": "^2.2.2", - "file-selector": "^0.4.0", + "file-selector": "^0.6.0", "prop-types": "^15.8.1" } }, diff --git a/frontend/package.json b/frontend/package.json index 5cfba64a3..6dfbd6764 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,7 +29,6 @@ "@fortawesome/free-regular-svg-icons": "^6", "@fortawesome/free-solid-svg-icons": "^6", "@fortawesome/react-fontawesome": "^0.1", - "@mantine/dropzone": "^4", "@mantine/modals": "^4", "@mantine/notifications": "^4", "@testing-library/jest-dom": "latest", @@ -53,6 +52,7 @@ "prettier": "^2", "prettier-plugin-organize-imports": "^2", "pretty-quick": "^3.1", + "react-dropzone": "^14", "react-table": "^7", "recharts": "^2.0.8", "sass": "^1", diff --git a/frontend/src/components/inputs/DropOverlay.tsx b/frontend/src/components/inputs/DropOverlay.tsx new file mode 100644 index 000000000..701d8c023 --- /dev/null +++ b/frontend/src/components/inputs/DropOverlay.tsx @@ -0,0 +1,126 @@ +import { + faArrowUp, + faFileCirclePlus, + faXmark, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Box, createStyles, Overlay, Stack, Text } from "@mantine/core"; +import clsx from "clsx"; +import { FunctionComponent, useMemo } from "react"; +import { DropzoneState } from "react-dropzone"; + +const useStyle = createStyles((theme) => { + return { + container: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + }, + inner: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + display: "flex", + alignItems: "center", + justifyContent: "center", + overflow: "hidden", + margin: theme.spacing.md, + borderRadius: theme.radius.md, + borderWidth: "0.2rem", + borderStyle: "dashed", + borderColor: theme.colors.gray[7], + backgroundColor: theme.fn.rgba(theme.colors.gray[0], 0.4), + }, + accepted: { + borderColor: theme.colors.brand[7], + backgroundColor: theme.fn.rgba(theme.colors.brand[0], 0.6), + }, + rejected: { + borderColor: theme.colors.red[7], + backgroundColor: theme.fn.rgba(theme.colors.red[0], 0.9), + }, + }; +}); + +export interface DropOverlayProps { + state: DropzoneState; + zIndex?: number; +} + +export const DropOverlay: FunctionComponent = ({ + state, + children, + zIndex = 10, +}) => { + const { + getRootProps, + isDragActive, + isDragAccept: accepted, + isDragReject: rejected, + } = state; + + const { classes } = useStyle(); + + const visible = isDragActive; + + const icon = useMemo(() => { + if (accepted) { + return faArrowUp; + } else if (rejected) { + return faXmark; + } else { + return faFileCirclePlus; + } + }, [accepted, rejected]); + + const title = useMemo(() => { + if (accepted) { + return "Release to Upload"; + } else if (rejected) { + return "Cannot Upload Files"; + } else { + return "Upload Subtitles"; + } + }, [accepted, rejected]); + + const subtitle = useMemo(() => { + if (accepted) { + return ""; + } else if (rejected) { + return "Some files are invalid"; + } else { + return "Drop to upload"; + } + }, [accepted, rejected]); + + return ( + + {visible && ( + + + + + + {title} + + {subtitle} + + + + + )} + {children} + + ); +}; diff --git a/frontend/src/components/inputs/File.tsx b/frontend/src/components/inputs/File.tsx deleted file mode 100644 index 5829e68b2..000000000 --- a/frontend/src/components/inputs/File.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { - faArrowUp, - faFileCirclePlus, - faXmark, -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Box, Stack, Text } from "@mantine/core"; -import { - Dropzone, - DropzoneProps, - DropzoneStatus, - FullScreenDropzone, - FullScreenDropzoneProps, -} from "@mantine/dropzone"; -import { FunctionComponent, useMemo } from "react"; - -export type FileProps = Omit & { - inner?: FileInnerComponent; -}; - -const File: FunctionComponent = ({ - inner: Inner = FileInner, - ...props -}) => { - return ( - - {(status) => } - - ); -}; - -export type FileOverlayProps = Omit & { - inner?: FileInnerComponent; -}; - -export const FileOverlay: FunctionComponent = ({ - inner: Inner = FileInner, - ...props -}) => { - return ( - - {(status) => } - - ); -}; - -export type FileInnerProps = { - status: DropzoneStatus; -}; - -type FileInnerComponent = FunctionComponent; - -const FileInner: FileInnerComponent = ({ status }) => { - const { accepted, rejected } = status; - const icon = useMemo(() => { - if (accepted) { - return faArrowUp; - } else if (rejected) { - return faXmark; - } else { - return faFileCirclePlus; - } - }, [accepted, rejected]); - - return ( - - - - - Upload files here - - Drag and drop, or click to select - - - ); -}; - -export default File; diff --git a/frontend/src/components/inputs/index.ts b/frontend/src/components/inputs/index.ts index fc2b3e879..7bae48a63 100644 --- a/frontend/src/components/inputs/index.ts +++ b/frontend/src/components/inputs/index.ts @@ -1,3 +1,4 @@ export { default as Action } from "./Action"; +export * from "./DropOverlay"; export * from "./FileBrowser"; export * from "./Selector"; diff --git a/frontend/src/pages/Episodes/index.tsx b/frontend/src/pages/Episodes/index.tsx index 1bbf365c3..f7a228d31 100644 --- a/frontend/src/pages/Episodes/index.tsx +++ b/frontend/src/pages/Episodes/index.tsx @@ -5,11 +5,10 @@ import { useSeriesById, useSeriesModification, } from "@/apis/hooks"; -import { Toolbox } from "@/components"; +import { DropOverlay, Toolbox } from "@/components"; import { QueryOverlay } from "@/components/async"; import { ItemEditModal } from "@/components/forms/ItemEditForm"; import { SeriesUploadModal } from "@/components/forms/SeriesUploadForm"; -import File, { FileOverlay, FileProps } from "@/components/inputs/File"; import { SubtitleToolsModal } from "@/components/modals"; import { useModals } from "@/modules/modals"; import { notification, task, TaskGroup } from "@/modules/task"; @@ -27,7 +26,8 @@ import { import { Container, Group, Stack } from "@mantine/core"; import { useDocumentTitle } from "@mantine/hooks"; import { showNotification } from "@mantine/notifications"; -import { FunctionComponent, useCallback, useMemo, useRef } from "react"; +import { FunctionComponent, useCallback, useMemo } from "react"; +import { FileRejection, useDropzone } from "react-dropzone"; import { Navigate, useParams } from "react-router-dom"; import Table from "./table"; @@ -66,10 +66,18 @@ const SeriesEpisodesView: FunctionComponent = () => { const hasTask = useIsAnyActionRunning(); - const dialogRef = useRef(null); const onDrop = useCallback( - (files: File[]) => { + (files: File[], rejections: FileRejection[]) => { if (series && profile) { + if (rejections.length > 0) { + showNotification( + notification.warn( + "Some files are rejected", + `${rejections.length} files are invalid` + ) + ); + } + modals.openContextModal(SeriesUploadModal, { files, series, @@ -86,11 +94,17 @@ const SeriesEpisodesView: FunctionComponent = () => { [modals, profile, series] ); - const onReject = useCallback>((rejections) => { - showNotification( - notification.warn("Cannot Upload Files", "Some files are invalid") - ); - }, []); + const dropzone = useDropzone({ + disabled: profile === undefined, + noClick: true, + onDrop, + }); + + // const onReject = useCallback>((rejections) => { + // showNotification( + // notification.warn("Cannot Upload Files", "Some files are invalid") + // ); + // }, []); useDocumentTitle(`${series?.title ?? "Unknown Series"} - Bazarr (Series)`); @@ -101,118 +115,113 @@ const SeriesEpisodesView: FunctionComponent = () => { return ( - {/* TODO: Still have some bugs. Handle it later */} - - - - - { - if (series) { - task.create(series.title, TaskGroup.ScanDisk, action, { - action: "scan-disk", - seriesid: id, - }); - } - }} - > - Scan Disk - - { - if (series) { - task.create(series.title, TaskGroup.SearchSubtitle, action, { - action: "search-missing", - seriesid: id, - }); + + + + { + if (series) { + task.create(series.title, TaskGroup.ScanDisk, action, { + action: "scan-disk", + seriesid: id, + }); + } + }} + > + Scan Disk + + { + if (series) { + task.create( + series.title, + TaskGroup.SearchSubtitle, + action, + { + action: "search-missing", + seriesid: id, + } + ); + } + }} + disabled={ + series === undefined || + series.episodeFileCount === 0 || + series.profileId === null || + !available } - }} - disabled={ - series === undefined || - series.episodeFileCount === 0 || - series.profileId === null || - !available - } - > - Search - - - - { - if (episodes) { - modals.openContextModal(SubtitleToolsModal, { - payload: episodes, - }); + > + Search + + + + - Tools - - dialogRef.current?.()} - > - Upload - - { - if (series) { - modals.openContextModal( - ItemEditModal, - { - item: series, - mutation, - }, - { title: series.title } - ); + icon={faBriefcase} + onClick={() => { + if (episodes) { + modals.openContextModal(SubtitleToolsModal, { + payload: episodes, + }); + } + }} + > + Tools + + - Edit Series - - - - - - -
-
-
+ icon={faCloudUploadAlt} + onClick={dropzone.open} + > + Upload +
+ { + if (series) { + modals.openContextModal( + ItemEditModal, + { + item: series, + mutation, + }, + { title: series.title } + ); + } + }} + > + Edit Series + +
+
+ + + +
+
+
+
); diff --git a/frontend/src/pages/Movies/Details/index.tsx b/frontend/src/pages/Movies/Details/index.tsx index 946896026..a93effd0d 100644 --- a/frontend/src/pages/Movies/Details/index.tsx +++ b/frontend/src/pages/Movies/Details/index.tsx @@ -8,15 +8,14 @@ import { useMovieById, useMovieModification, } from "@/apis/hooks/movies"; -import { Action, Toolbox } from "@/components"; +import { Action, DropOverlay, Toolbox } from "@/components"; import { QueryOverlay } from "@/components/async"; import { ItemEditModal } from "@/components/forms/ItemEditForm"; import { MovieUploadModal } from "@/components/forms/MovieUploadForm"; -import File, { FileOverlay } from "@/components/inputs/File"; import { MovieHistoryModal, SubtitleToolsModal } from "@/components/modals"; import { MovieSearchModal } from "@/components/modals/ManualSearchModal"; import { useModals } from "@/modules/modals"; -import { task, TaskGroup } from "@/modules/task"; +import { notification, task, TaskGroup } from "@/modules/task"; import ItemOverview from "@/pages/views/ItemOverview"; import { useLanguageProfileBy } from "@/utilities/languages"; import { @@ -32,8 +31,10 @@ import { import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Container, Group, Menu, Stack } from "@mantine/core"; import { useDocumentTitle } from "@mantine/hooks"; +import { showNotification } from "@mantine/notifications"; import { isNumber } from "lodash"; -import { FunctionComponent, useCallback, useRef } from "react"; +import { FunctionComponent, useCallback } from "react"; +import { FileRejection, useDropzone } from "react-dropzone"; import { Navigate, useParams } from "react-router-dom"; import Table from "./table"; @@ -78,14 +79,28 @@ const MovieDetailView: FunctionComponent = () => { [downloadAsync] ); - const dialogRef = useRef(null); const onDrop = useCallback( - (files: File[]) => { + (files: File[], rejections: FileRejection[]) => { if (movie && profile) { + if (rejections.length > 0) { + showNotification( + notification.warn( + "Some files are rejected", + `${rejections.length} files are invalid` + ) + ); + } modals.openContextModal(MovieUploadModal, { files, movie, }); + } else { + showNotification( + notification.warn( + "Cannot Upload Files", + "movie or language profile is not ready" + ) + ); } }, [modals, movie, profile] @@ -95,6 +110,12 @@ const MovieDetailView: FunctionComponent = () => { useDocumentTitle(`${movie?.title ?? "Unknown Movie"} - Bazarr (Movies)`); + const dropzone = useDropzone({ + disabled: profile === undefined, + onDrop, + noClick: true, + }); + if (isNaN(id) || (isFetched && !movie)) { return ; } @@ -104,136 +125,135 @@ const MovieDetailView: FunctionComponent = () => { return ( - + {/* - */} + + + { if (movie) { - modals.openContextModal(SubtitleToolsModal, { - payload: [movie], + task.create(movie.title, TaskGroup.ScanDisk, action, { + action: "scan-disk", + radarrid: id, }); } }} > - Tools - - } + Scan Disk + + { if (movie) { - modals.openContextModal(MovieHistoryModal, { movie }); + task.create(movie.title, TaskGroup.SearchSubtitle, action, { + action: "search-missing", + radarrid: id, + }); } }} > - History - - - - - - -
-
+ Search + + { + if (movie) { + modals.openContextModal(MovieSearchModal, { + item: movie, + download, + query: useMoviesProvider, + }); + } + }} + > + Manual + + + + + Upload + + { + if (movie) { + modals.openContextModal( + ItemEditModal, + { + item: movie, + mutation, + }, + { title: movie.title } + ); + } + }} + > + Edit Movie + + + } + > + } + onClick={() => { + if (movie) { + modals.openContextModal(SubtitleToolsModal, { + payload: [movie], + }); + } + }} + > + Tools + + } + onClick={() => { + if (movie) { + modals.openContextModal(MovieHistoryModal, { movie }); + } + }} + > + History + + + + + + +
+
+
);