bazarr/frontend/src/components/forms/MovieUploadForm.tsx

297 lines
7.3 KiB
TypeScript

import { useMovieSubtitleModification } from "@/apis/hooks";
import { useModals, withModal } from "@/modules/modals";
import { TaskGroup, task } from "@/modules/task";
import { useArrayAction, useSelectorOptions } from "@/utilities";
import FormUtils from "@/utilities/form";
import {
useLanguageProfileBy,
useProfileItemsToLanguages,
} from "@/utilities/languages";
import {
faCheck,
faCircleNotch,
faInfoCircle,
faTimes,
faTrash,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
Checkbox,
Divider,
MantineColor,
Stack,
Text,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { isString } from "lodash";
import { FunctionComponent, useEffect, useMemo } from "react";
import { Column } from "react-table";
import TextPopover from "../TextPopover";
import { Action, Selector } from "../inputs";
import { SimpleTable } from "../tables";
type SubtitleFile = {
file: File;
language: Language.Info | null;
forced: boolean;
hi: boolean;
validateResult?: SubtitleValidateResult;
};
type SubtitleValidateResult = {
state: "valid" | "warning" | "error";
messages?: string;
};
const validator = (
movie: Item.Movie,
file: SubtitleFile,
): SubtitleValidateResult => {
if (file.language === null) {
return {
state: "error",
messages: "Language is not selected",
};
} else {
const { subtitles } = movie;
const existing = subtitles.find(
(v) => v.code2 === file.language?.code2 && isString(v.path),
);
if (existing !== undefined) {
return {
state: "warning",
messages: "Override existing subtitle",
};
}
}
return {
state: "valid",
};
};
interface Props {
files: File[];
movie: Item.Movie;
onComplete?: () => void;
}
const MovieUploadForm: FunctionComponent<Props> = ({
files,
movie,
onComplete,
}) => {
const modals = useModals();
const profile = useLanguageProfileBy(movie.profileId);
const languages = useProfileItemsToLanguages(profile);
const languageOptions = useSelectorOptions(
languages,
(v) => v.name,
(v) => v.code2,
);
const defaultLanguage = useMemo(
() => (languages.length > 0 ? languages[0] : null),
[languages],
);
const form = useForm({
initialValues: {
files: files
.map<SubtitleFile>((file) => ({
file,
language: defaultLanguage,
forced: defaultLanguage?.forced ?? false,
hi: defaultLanguage?.hi ?? false,
}))
.map<SubtitleFile>((v) => ({
...v,
validateResult: validator(movie, v),
})),
},
validate: {
files: FormUtils.validation((values) => {
return (
values.find(
(v) =>
v.language === null ||
v.validateResult === undefined ||
v.validateResult.state === "error",
) === undefined
);
}, "Some files cannot be uploaded, please check"),
},
});
useEffect(() => {
if (form.values.files.length <= 0) {
modals.closeSelf();
}
}, [form.values.files.length, modals]);
const action = useArrayAction<SubtitleFile>((fn) => {
form.setValues((values) => {
const newFiles = fn(values.files ?? []);
newFiles.forEach((v) => {
v.validateResult = validator(movie, v);
});
return { ...values, files: newFiles };
});
});
const columns = useMemo<Column<SubtitleFile>[]>(
() => [
{
accessor: "validateResult",
Cell: ({ cell: { value } }) => {
const icon = useMemo(() => {
switch (value?.state) {
case "valid":
return faCheck;
case "warning":
return faInfoCircle;
case "error":
return faTimes;
default:
return faCircleNotch;
}
}, [value?.state]);
const color = useMemo<MantineColor | undefined>(() => {
switch (value?.state) {
case "valid":
return "green";
case "warning":
return "yellow";
case "error":
return "red";
default:
return undefined;
}
}, [value?.state]);
return (
<TextPopover text={value?.messages}>
<Text c={color} inline>
<FontAwesomeIcon icon={icon}></FontAwesomeIcon>
</Text>
</TextPopover>
);
},
},
{
Header: "File",
id: "filename",
accessor: "file",
Cell: ({ value }) => {
return <Text className="table-primary">{value.name}</Text>;
},
},
{
Header: "Forced",
accessor: "forced",
Cell: ({ row: { original, index }, value }) => {
return (
<Checkbox
checked={value}
onChange={({ currentTarget: { checked } }) => {
action.mutate(index, { ...original, forced: checked });
}}
></Checkbox>
);
},
},
{
Header: "HI",
accessor: "hi",
Cell: ({ row: { original, index }, value }) => {
return (
<Checkbox
checked={value}
onChange={({ currentTarget: { checked } }) => {
action.mutate(index, { ...original, hi: checked });
}}
></Checkbox>
);
},
},
{
Header: "Language",
accessor: "language",
Cell: ({ row: { original, index }, value }) => {
return (
<Selector
{...languageOptions}
className="table-long-break"
value={value}
onChange={(item) => {
action.mutate(index, { ...original, language: item });
}}
></Selector>
);
},
},
{
id: "action",
accessor: "file",
Cell: ({ row: { index } }) => {
return (
<Action
label="Remove"
icon={faTrash}
color="red"
onClick={() => action.remove(index)}
></Action>
);
},
},
],
[action, languageOptions],
);
const { upload } = useMovieSubtitleModification();
return (
<form
onSubmit={form.onSubmit(({ files }) => {
const { radarrId } = movie;
files.forEach(({ file, language, hi, forced }) => {
if (language === null) {
throw new Error("Language is not selected");
}
task.create(file.name, TaskGroup.UploadSubtitle, upload.mutateAsync, {
radarrId,
form: { file, language: language.code2, hi, forced },
});
});
onComplete?.();
modals.closeSelf();
})}
>
<Stack className="table-long-break">
<SimpleTable columns={columns} data={form.values.files}></SimpleTable>
<Divider></Divider>
<Button type="submit">Upload</Button>
</Stack>
</form>
);
};
export const MovieUploadModal = withModal(
MovieUploadForm,
"upload-movie-subtitle",
{
title: "Upload Subtitles",
size: "xl",
},
);
export default MovieUploadForm;