mirror of
https://github.com/morpheus65535/bazarr
synced 2025-03-15 00:18:48 +00:00
Improve the upload behavior
This commit is contained in:
parent
182b125a9e
commit
4bb2cf65e6
7 changed files with 411 additions and 357 deletions
62
frontend/package-lock.json
generated
62
frontend/package-lock.json
generated
|
@ -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"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
126
frontend/src/components/inputs/DropOverlay.tsx
Normal file
126
frontend/src/components/inputs/DropOverlay.tsx
Normal file
|
@ -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<DropOverlayProps> = ({
|
||||
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 (
|
||||
<Box sx={{ position: "relative" }} {...getRootProps()}>
|
||||
{visible && (
|
||||
<Box className={classes.container} style={{ zIndex }}>
|
||||
<Stack
|
||||
spacing="xs"
|
||||
className={clsx(classes.inner, {
|
||||
[classes.accepted]: accepted,
|
||||
[classes.rejected]: rejected,
|
||||
})}
|
||||
style={{ zIndex: zIndex + 1 }}
|
||||
>
|
||||
<Box>
|
||||
<FontAwesomeIcon icon={icon} size="3x" />
|
||||
</Box>
|
||||
<Text size="xl">{title}</Text>
|
||||
<Text color="gray" size="sm">
|
||||
{subtitle}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Overlay zIndex={zIndex}></Overlay>
|
||||
</Box>
|
||||
)}
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
|
@ -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<DropzoneProps, "children"> & {
|
||||
inner?: FileInnerComponent;
|
||||
};
|
||||
|
||||
const File: FunctionComponent<FileProps> = ({
|
||||
inner: Inner = FileInner,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<Dropzone {...props}>
|
||||
{(status) => <Inner status={status}></Inner>}
|
||||
</Dropzone>
|
||||
);
|
||||
};
|
||||
|
||||
export type FileOverlayProps = Omit<FullScreenDropzoneProps, "children"> & {
|
||||
inner?: FileInnerComponent;
|
||||
};
|
||||
|
||||
export const FileOverlay: FunctionComponent<FileOverlayProps> = ({
|
||||
inner: Inner = FileInner,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<FullScreenDropzone {...props}>
|
||||
{(status) => <Inner status={status}></Inner>}
|
||||
</FullScreenDropzone>
|
||||
);
|
||||
};
|
||||
|
||||
export type FileInnerProps = {
|
||||
status: DropzoneStatus;
|
||||
};
|
||||
|
||||
type FileInnerComponent = FunctionComponent<FileInnerProps>;
|
||||
|
||||
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 (
|
||||
<Stack m="lg" align="center" spacing="xs" style={{ pointerEvents: "none" }}>
|
||||
<Box mb="md">
|
||||
<FontAwesomeIcon size="3x" icon={icon}></FontAwesomeIcon>
|
||||
</Box>
|
||||
<Text size="lg">Upload files here</Text>
|
||||
<Text color="dimmed" size="sm">
|
||||
Drag and drop, or click to select
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default File;
|
|
@ -1,3 +1,4 @@
|
|||
export { default as Action } from "./Action";
|
||||
export * from "./DropOverlay";
|
||||
export * from "./FileBrowser";
|
||||
export * from "./Selector";
|
||||
|
|
|
@ -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<VoidFunction>(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<Sure<FileProps["onReject"]>>((rejections) => {
|
||||
showNotification(
|
||||
notification.warn("Cannot Upload Files", "Some files are invalid")
|
||||
);
|
||||
}, []);
|
||||
const dropzone = useDropzone({
|
||||
disabled: profile === undefined,
|
||||
noClick: true,
|
||||
onDrop,
|
||||
});
|
||||
|
||||
// const onReject = useCallback<Sure<FileProps["onReject"]>>((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 (
|
||||
<Container px={0} fluid>
|
||||
<QueryOverlay result={seriesQuery}>
|
||||
{/* TODO: Still have some bugs. Handle it later */}
|
||||
<FileOverlay
|
||||
disabled={profile === undefined}
|
||||
accept={[""]}
|
||||
onDrop={onDrop}
|
||||
></FileOverlay>
|
||||
<div hidden>
|
||||
{/* A workaround to allow click to upload files */}
|
||||
<File
|
||||
disabled={profile === undefined}
|
||||
openRef={dialogRef}
|
||||
onDrop={onDrop}
|
||||
onReject={onReject}
|
||||
></File>
|
||||
</div>
|
||||
<Toolbox>
|
||||
<Group spacing="xs">
|
||||
<Toolbox.Button
|
||||
icon={faSync}
|
||||
disabled={!available || hasTask}
|
||||
onClick={() => {
|
||||
if (series) {
|
||||
task.create(series.title, TaskGroup.ScanDisk, action, {
|
||||
action: "scan-disk",
|
||||
seriesid: id,
|
||||
});
|
||||
<DropOverlay state={dropzone}>
|
||||
<Toolbox>
|
||||
<Group spacing="xs">
|
||||
<Toolbox.Button
|
||||
icon={faSync}
|
||||
disabled={!available || hasTask}
|
||||
onClick={() => {
|
||||
if (series) {
|
||||
task.create(series.title, TaskGroup.ScanDisk, action, {
|
||||
action: "scan-disk",
|
||||
seriesid: id,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Scan Disk
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faSearch}
|
||||
onClick={() => {
|
||||
if (series) {
|
||||
task.create(
|
||||
series.title,
|
||||
TaskGroup.SearchSubtitle,
|
||||
action,
|
||||
{
|
||||
action: "search-missing",
|
||||
seriesid: id,
|
||||
}
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
series === undefined ||
|
||||
series.episodeFileCount === 0 ||
|
||||
series.profileId === null ||
|
||||
!available
|
||||
}
|
||||
}}
|
||||
>
|
||||
Scan Disk
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faSearch}
|
||||
onClick={() => {
|
||||
if (series) {
|
||||
task.create(series.title, TaskGroup.SearchSubtitle, action, {
|
||||
action: "search-missing",
|
||||
seriesid: id,
|
||||
});
|
||||
>
|
||||
Search
|
||||
</Toolbox.Button>
|
||||
</Group>
|
||||
<Group spacing="xs">
|
||||
<Toolbox.Button
|
||||
disabled={
|
||||
series === undefined ||
|
||||
series.episodeFileCount === 0 ||
|
||||
!available ||
|
||||
hasTask
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
series === undefined ||
|
||||
series.episodeFileCount === 0 ||
|
||||
series.profileId === null ||
|
||||
!available
|
||||
}
|
||||
>
|
||||
Search
|
||||
</Toolbox.Button>
|
||||
</Group>
|
||||
<Group spacing="xs">
|
||||
<Toolbox.Button
|
||||
disabled={
|
||||
series === undefined ||
|
||||
series.episodeFileCount === 0 ||
|
||||
!available ||
|
||||
hasTask
|
||||
}
|
||||
icon={faBriefcase}
|
||||
onClick={() => {
|
||||
if (episodes) {
|
||||
modals.openContextModal(SubtitleToolsModal, {
|
||||
payload: episodes,
|
||||
});
|
||||
icon={faBriefcase}
|
||||
onClick={() => {
|
||||
if (episodes) {
|
||||
modals.openContextModal(SubtitleToolsModal, {
|
||||
payload: episodes,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Tools
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
disabled={
|
||||
series === undefined ||
|
||||
series.episodeFileCount === 0 ||
|
||||
series.profileId === null ||
|
||||
!available
|
||||
}
|
||||
}}
|
||||
>
|
||||
Tools
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
disabled={
|
||||
series === undefined ||
|
||||
series.episodeFileCount === 0 ||
|
||||
series.profileId === null ||
|
||||
!available
|
||||
}
|
||||
icon={faCloudUploadAlt}
|
||||
onClick={() => dialogRef.current?.()}
|
||||
>
|
||||
Upload
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faWrench}
|
||||
disabled={hasTask}
|
||||
onClick={() => {
|
||||
if (series) {
|
||||
modals.openContextModal(
|
||||
ItemEditModal,
|
||||
{
|
||||
item: series,
|
||||
mutation,
|
||||
},
|
||||
{ title: series.title }
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Edit Series
|
||||
</Toolbox.Button>
|
||||
</Group>
|
||||
</Toolbox>
|
||||
<Stack>
|
||||
<ItemOverview item={series ?? null} details={details}></ItemOverview>
|
||||
<QueryOverlay result={episodesQuery}>
|
||||
<Table
|
||||
episodes={episodes ?? null}
|
||||
profile={profile}
|
||||
disabled={hasTask || !series || series.profileId === null}
|
||||
></Table>
|
||||
</QueryOverlay>
|
||||
</Stack>
|
||||
icon={faCloudUploadAlt}
|
||||
onClick={dropzone.open}
|
||||
>
|
||||
Upload
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faWrench}
|
||||
disabled={hasTask}
|
||||
onClick={() => {
|
||||
if (series) {
|
||||
modals.openContextModal(
|
||||
ItemEditModal,
|
||||
{
|
||||
item: series,
|
||||
mutation,
|
||||
},
|
||||
{ title: series.title }
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Edit Series
|
||||
</Toolbox.Button>
|
||||
</Group>
|
||||
</Toolbox>
|
||||
<Stack>
|
||||
<ItemOverview
|
||||
item={series ?? null}
|
||||
details={details}
|
||||
></ItemOverview>
|
||||
<QueryOverlay result={episodesQuery}>
|
||||
<Table
|
||||
episodes={episodes ?? null}
|
||||
profile={profile}
|
||||
disabled={hasTask || !series || series.profileId === null}
|
||||
></Table>
|
||||
</QueryOverlay>
|
||||
</Stack>
|
||||
</DropOverlay>
|
||||
</QueryOverlay>
|
||||
</Container>
|
||||
);
|
||||
|
|
|
@ -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<VoidFunction>(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 <Navigate to="/not-found"></Navigate>;
|
||||
}
|
||||
|
@ -104,136 +125,135 @@ const MovieDetailView: FunctionComponent = () => {
|
|||
return (
|
||||
<Container fluid px={0}>
|
||||
<QueryOverlay result={movieQuery}>
|
||||
<FileOverlay
|
||||
<DropOverlay state={dropzone}>
|
||||
{/* <FileOverlay
|
||||
disabled={profile === undefined}
|
||||
accept={[""]}
|
||||
onDrop={onDrop}
|
||||
></FileOverlay>
|
||||
<div hidden>
|
||||
{/* A workaround to allow click to upload files */}
|
||||
></FileOverlay> */}
|
||||
{/* <div hidden>
|
||||
<File
|
||||
disabled={profile === undefined}
|
||||
accept={[""]}
|
||||
openRef={dialogRef}
|
||||
onDrop={onDrop}
|
||||
></File>
|
||||
</div>
|
||||
<Toolbox>
|
||||
<Group spacing="xs">
|
||||
<Toolbox.Button
|
||||
icon={faSync}
|
||||
disabled={hasTask}
|
||||
onClick={() => {
|
||||
if (movie) {
|
||||
task.create(movie.title, TaskGroup.ScanDisk, action, {
|
||||
action: "scan-disk",
|
||||
radarrid: id,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Scan Disk
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faSearch}
|
||||
disabled={!isNumber(movie?.profileId)}
|
||||
onClick={() => {
|
||||
if (movie) {
|
||||
task.create(movie.title, TaskGroup.SearchSubtitle, action, {
|
||||
action: "search-missing",
|
||||
radarrid: id,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faUser}
|
||||
disabled={!isNumber(movie?.profileId) || hasTask}
|
||||
onClick={() => {
|
||||
if (movie) {
|
||||
modals.openContextModal(MovieSearchModal, {
|
||||
item: movie,
|
||||
download,
|
||||
query: useMoviesProvider,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Manual
|
||||
</Toolbox.Button>
|
||||
</Group>
|
||||
<Group spacing="xs">
|
||||
<Toolbox.Button
|
||||
disabled={!allowEdit || movie.profileId === null || hasTask}
|
||||
icon={faCloudUploadAlt}
|
||||
onClick={() => {
|
||||
dialogRef.current?.();
|
||||
}}
|
||||
>
|
||||
Upload
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faWrench}
|
||||
disabled={hasTask}
|
||||
onClick={() => {
|
||||
if (movie) {
|
||||
modals.openContextModal(
|
||||
ItemEditModal,
|
||||
{
|
||||
item: movie,
|
||||
mutation,
|
||||
},
|
||||
{ title: movie.title }
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Edit Movie
|
||||
</Toolbox.Button>
|
||||
<Menu
|
||||
control={
|
||||
<Action
|
||||
label="More Actions"
|
||||
icon={faEllipsis}
|
||||
disabled={hasTask}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Menu.Item
|
||||
icon={<FontAwesomeIcon icon={faToolbox} />}
|
||||
</div> */}
|
||||
<Toolbox>
|
||||
<Group spacing="xs">
|
||||
<Toolbox.Button
|
||||
icon={faSync}
|
||||
disabled={hasTask}
|
||||
onClick={() => {
|
||||
if (movie) {
|
||||
modals.openContextModal(SubtitleToolsModal, {
|
||||
payload: [movie],
|
||||
task.create(movie.title, TaskGroup.ScanDisk, action, {
|
||||
action: "scan-disk",
|
||||
radarrid: id,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Tools
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
icon={<FontAwesomeIcon icon={faHistory} />}
|
||||
Scan Disk
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faSearch}
|
||||
disabled={!isNumber(movie?.profileId)}
|
||||
onClick={() => {
|
||||
if (movie) {
|
||||
modals.openContextModal(MovieHistoryModal, { movie });
|
||||
task.create(movie.title, TaskGroup.SearchSubtitle, action, {
|
||||
action: "search-missing",
|
||||
radarrid: id,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
History
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Toolbox>
|
||||
<Stack>
|
||||
<ItemOverview item={movie ?? null} details={[]}></ItemOverview>
|
||||
<Table
|
||||
movie={movie ?? null}
|
||||
profile={profile}
|
||||
disabled={hasTask}
|
||||
></Table>
|
||||
</Stack>
|
||||
Search
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faUser}
|
||||
disabled={!isNumber(movie?.profileId) || hasTask}
|
||||
onClick={() => {
|
||||
if (movie) {
|
||||
modals.openContextModal(MovieSearchModal, {
|
||||
item: movie,
|
||||
download,
|
||||
query: useMoviesProvider,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Manual
|
||||
</Toolbox.Button>
|
||||
</Group>
|
||||
<Group spacing="xs">
|
||||
<Toolbox.Button
|
||||
disabled={!allowEdit || movie.profileId === null || hasTask}
|
||||
icon={faCloudUploadAlt}
|
||||
onClick={dropzone.open}
|
||||
>
|
||||
Upload
|
||||
</Toolbox.Button>
|
||||
<Toolbox.Button
|
||||
icon={faWrench}
|
||||
disabled={hasTask}
|
||||
onClick={() => {
|
||||
if (movie) {
|
||||
modals.openContextModal(
|
||||
ItemEditModal,
|
||||
{
|
||||
item: movie,
|
||||
mutation,
|
||||
},
|
||||
{ title: movie.title }
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Edit Movie
|
||||
</Toolbox.Button>
|
||||
<Menu
|
||||
control={
|
||||
<Action
|
||||
label="More Actions"
|
||||
icon={faEllipsis}
|
||||
disabled={hasTask}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Menu.Item
|
||||
icon={<FontAwesomeIcon icon={faToolbox} />}
|
||||
onClick={() => {
|
||||
if (movie) {
|
||||
modals.openContextModal(SubtitleToolsModal, {
|
||||
payload: [movie],
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Tools
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
icon={<FontAwesomeIcon icon={faHistory} />}
|
||||
onClick={() => {
|
||||
if (movie) {
|
||||
modals.openContextModal(MovieHistoryModal, { movie });
|
||||
}
|
||||
}}
|
||||
>
|
||||
History
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Toolbox>
|
||||
<Stack>
|
||||
<ItemOverview item={movie ?? null} details={[]}></ItemOverview>
|
||||
<Table
|
||||
movie={movie ?? null}
|
||||
profile={profile}
|
||||
disabled={hasTask}
|
||||
></Table>
|
||||
</Stack>
|
||||
</DropOverlay>
|
||||
</QueryOverlay>
|
||||
</Container>
|
||||
);
|
||||
|
|
Loading…
Add table
Reference in a new issue