bazarr/frontend/src/components/modals/SubtitleUploadModal.tsx

339 lines
8.5 KiB
TypeScript

import {
faCheck,
faCircleNotch,
faInfoCircle,
faTimes,
faTrash,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Button, Container, Form } from "react-bootstrap";
import { Column, TableUpdater } from "react-table";
import { LanguageSelector, MessageIcon } from "..";
import { BuildKey } from "../../utilities";
import { FileForm } from "../inputs";
import { SimpleTable } from "../tables";
import BaseModal, { BaseModalProps } from "./BaseModal";
import { useCloseModal } from "./hooks";
export interface PendingSubtitle<P> {
file: File;
state: "valid" | "fetching" | "warning" | "error";
messages: string[];
language: Language.Info | null;
forced: boolean;
hi: boolean;
payload: P;
}
export type Validator<T> = (
item: PendingSubtitle<T>
) => Pick<PendingSubtitle<T>, "state" | "messages">;
interface Props<T> {
initial: T;
availableLanguages: Language.Info[];
upload: (items: PendingSubtitle<T>[]) => void;
update: (items: PendingSubtitle<T>[]) => Promise<PendingSubtitle<T>[]>;
validate: Validator<T>;
columns: Column<PendingSubtitle<T>>[];
hideAllLanguages?: boolean;
}
export default function SubtitleUploadModal<T>(
props: Props<T> & Omit<BaseModalProps, "footer" | "title" | "size">
) {
const {
initial,
columns,
upload,
update,
validate,
availableLanguages,
hideAllLanguages,
} = props;
const closeModal = useCloseModal();
const [pending, setPending] = useState<PendingSubtitle<T>[]>([]);
const fileList = useMemo(() => pending.map((v) => v.file), [pending]);
const initialRef = useRef(initial);
const setFiles = useCallback(
async (files: File[]) => {
const initialLanguage =
availableLanguages.length > 0 ? availableLanguages[0] : null;
let list = files.map<PendingSubtitle<T>>((file) => ({
file,
state: "fetching",
messages: [],
language: initialLanguage,
forced: false,
hi: false,
payload: { ...initialRef.current },
}));
if (update) {
setPending(list);
list = await update(list);
} else {
list = list.map<PendingSubtitle<T>>((v) => ({
...v,
state: "valid",
}));
}
list = list.map((v) => ({
...v,
...validate(v),
}));
setPending(list);
},
[update, validate, availableLanguages]
);
const modify = useCallback<TableUpdater<PendingSubtitle<T>>>(
(row, info?: PendingSubtitle<T>) => {
setPending((pd) => {
const newPending = [...pd];
if (info) {
info = { ...info, ...validate(info) };
newPending[row.index] = info;
} else {
newPending.splice(row.index, 1);
}
return newPending;
});
},
[validate]
);
useEffect(() => {
setPending((pd) => {
const newPd = pd.map((v) => {
if (v.state !== "fetching") {
return { ...v, ...validate(v) };
} else {
return v;
}
});
return newPd;
});
}, [validate]);
const columnsWithAction = useMemo<Column<PendingSubtitle<T>>[]>(
() => [
{
id: "icon",
accessor: "state",
className: "text-center",
Cell: ({ value, row }) => {
let icon = faCircleNotch;
let color: string | undefined = undefined;
let spin = false;
switch (value) {
case "fetching":
spin = true;
break;
case "warning":
icon = faInfoCircle;
color = "var(--warning)";
break;
case "valid":
icon = faCheck;
color = "var(--success)";
break;
default:
icon = faTimes;
color = "var(--danger)";
break;
}
const messages = row.original.messages;
return (
<MessageIcon
messages={messages}
color={color}
icon={icon}
spin={spin}
></MessageIcon>
);
},
},
{
Header: "File",
accessor: (d) => d.file.name,
},
{
id: "hi",
Header: "HI",
accessor: "hi",
Cell: ({ row, value, update }) => {
const { original, index } = row;
return (
<Form.Check
custom
disabled={original.state === "fetching"}
id={BuildKey(index, original.file.name, "hi")}
checked={value}
onChange={(v) => {
const newInfo = { ...row.original };
newInfo.hi = v.target.checked;
update && update(row, newInfo);
}}
></Form.Check>
);
},
},
{
id: "forced",
Header: "Forced",
accessor: "forced",
Cell: ({ row, value, update }) => {
const { original, index } = row;
return (
<Form.Check
custom
disabled={original.state === "fetching"}
id={BuildKey(index, original.file.name, "forced")}
checked={value}
onChange={(v) => {
const newInfo = { ...row.original };
newInfo.forced = v.target.checked;
update && update(row, newInfo);
}}
></Form.Check>
);
},
},
{
id: "language",
Header: "Language",
accessor: "language",
className: "w-25",
Cell: ({ row, update, value }) => {
return (
<LanguageSelector
disabled={row.original.state === "fetching"}
options={availableLanguages}
value={value}
onChange={(lang) => {
if (lang && update) {
const newInfo = { ...row.original };
newInfo.language = lang;
update(row, newInfo);
}
}}
></LanguageSelector>
);
},
},
...columns,
{
id: "action",
accessor: "file",
Cell: ({ row, update }) => (
<Button
size="sm"
variant="light"
disabled={row.original.state === "fetching"}
onClick={() => {
update && update(row);
}}
>
<FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>
</Button>
),
},
],
[columns, availableLanguages]
);
const showTable = pending.length > 0;
const canUpload = useMemo(
() =>
pending.length > 0 &&
pending.every((v) => v.state === "valid" || v.state === "warning"),
[pending]
);
const footer = (
<div className="d-flex flex-row-reverse flex-grow-1 justify-content-between">
<div>
<Button
hidden={!showTable}
variant="outline-secondary"
className="mr-2"
onClick={() => setFiles([])}
>
Clean
</Button>
<Button
disabled={!canUpload || !showTable}
onClick={() => {
upload(pending);
setFiles([]);
closeModal();
}}
>
Upload
</Button>
</div>
<div className="w-25" hidden={hideAllLanguages}>
<LanguageSelector
options={availableLanguages}
value={null}
disabled={!showTable}
onChange={(lang) => {
if (lang) {
setPending((pd) =>
pd
.map((v) => ({ ...v, language: lang }))
.map((v) => ({ ...v, ...validate(v) }))
);
}
}}
></LanguageSelector>
</div>
</div>
);
return (
<BaseModal
size={showTable ? "xl" : "lg"}
title="Upload Subtitles"
footer={footer}
{...props}
>
<Container fluid className="flex-column">
<Form>
<Form.Group>
<FileForm
disabled={showTable}
emptyText="Select..."
multiple
value={fileList}
onChange={setFiles}
></FileForm>
</Form.Group>
</Form>
<div hidden={!showTable}>
<SimpleTable
columns={columnsWithAction}
data={pending}
responsive={false}
update={modify}
></SimpleTable>
</div>
</Container>
</BaseModal>
);
}