2021-08-24 16:33:59 +00:00
|
|
|
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 "..";
|
2021-08-25 14:08:36 +00:00
|
|
|
import { BuildKey } from "../../utilities";
|
2021-08-24 16:33:59 +00:00
|
|
|
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;
|
2021-08-25 14:08:36 +00:00
|
|
|
forced: boolean;
|
|
|
|
hi: boolean;
|
2021-08-24 16:33:59 +00:00
|
|
|
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,
|
2021-08-25 14:08:36 +00:00
|
|
|
forced: false,
|
|
|
|
hi: false,
|
2021-08-24 16:33:59 +00:00
|
|
|
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,
|
|
|
|
},
|
2021-08-25 14:08:36 +00:00
|
|
|
{
|
|
|
|
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>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
},
|
2021-08-24 16:33:59 +00:00
|
|
|
{
|
|
|
|
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>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
},
|
2021-08-25 14:08:36 +00:00
|
|
|
...columns,
|
2021-08-24 16:33:59 +00:00
|
|
|
{
|
|
|
|
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);
|
2021-08-26 15:34:56 +00:00
|
|
|
setFiles([]);
|
2021-08-24 16:33:59 +00:00
|
|
|
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>
|
|
|
|
);
|
|
|
|
}
|