parent
e0b988b20f
commit
4a890b2561
@ -0,0 +1,290 @@
|
||||
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 { 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;
|
||||
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,
|
||||
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,
|
||||
},
|
||||
...columns,
|
||||
{
|
||||
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>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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);
|
||||
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>
|
||||
);
|
||||
}
|
Loading…
Reference in new issue