summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorLASER-Yi <[email protected]>2021-08-25 00:33:59 +0800
committerLASER-Yi <[email protected]>2021-08-25 00:33:59 +0800
commit4a890b25617b0a50f3882f95e626c0f226382c7f (patch)
tree3925e7ab519e836208b5685b89726f3c39f1db67
parente0b988b20f71d6fb6cc8bf5b55be1cbaf436d227 (diff)
downloadbazarr-4a890b25617b0a50f3882f95e626c0f226382c7f.tar.gz
bazarr-4a890b25617b0a50f3882f95e626c0f226382c7f.zip
Support multi-language in subtitle upload modal
-rw-r--r--frontend/src/components/LanguageSelector.tsx2
-rw-r--r--frontend/src/components/modals/MovieUploadModal.tsx242
-rw-r--r--frontend/src/components/modals/SeriesUploadModal.tsx352
-rw-r--r--frontend/src/components/modals/SubtitleUploadModal.tsx290
4 files changed, 472 insertions, 414 deletions
diff --git a/frontend/src/components/LanguageSelector.tsx b/frontend/src/components/LanguageSelector.tsx
index 17e17d8ac..f2466e1bc 100644
--- a/frontend/src/components/LanguageSelector.tsx
+++ b/frontend/src/components/LanguageSelector.tsx
@@ -7,7 +7,7 @@ interface Props {
type RemovedSelectorProps<M extends boolean> = Omit<
SelectorProps<Language.Info, M>,
- "label" | "placeholder"
+ "label"
>;
export type LanguageSelectorProps<M extends boolean> = Override<
diff --git a/frontend/src/components/modals/MovieUploadModal.tsx b/frontend/src/components/modals/MovieUploadModal.tsx
index a0557e7b7..85e852f93 100644
--- a/frontend/src/components/modals/MovieUploadModal.tsx
+++ b/frontend/src/components/modals/MovieUploadModal.tsx
@@ -1,27 +1,19 @@
-import { faTrash } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import React, {
- FunctionComponent,
- useCallback,
- useMemo,
- useState,
-} from "react";
-import { Button, Container, Form } from "react-bootstrap";
-import { Column, Row } from "react-table";
+import React, { FunctionComponent, useCallback, useMemo } from "react";
+import { Form } from "react-bootstrap";
+import { Column } from "react-table";
import { dispatchTask } from "../../@modules/task";
import { createTask } from "../../@modules/task/utilities";
import { useProfileBy, useProfileItemsToLanguages } from "../../@redux/hooks";
import { MoviesApi } from "../../apis";
import { BuildKey } from "../../utilities";
-import { FileForm } from "../inputs";
-import { LanguageSelector } from "../LanguageSelector";
-import { SimpleTable } from "../tables";
-import BaseModal, { BaseModalProps } from "./BaseModal";
+import { BaseModalProps } from "./BaseModal";
import { useModalInformation } from "./hooks";
+import SubtitleUploadModal, {
+ PendingSubtitle,
+ Validator,
+} from "./SubtitleUploadModal";
-interface PendingSubtitle {
- file: File;
- language: Language.Info;
+interface Payload {
forced: boolean;
}
@@ -30,178 +22,114 @@ export const TaskGroupName = "Uploading Subtitles...";
const MovieUploadModal: FunctionComponent<BaseModalProps> = (props) => {
const modal = props;
- const { payload, closeModal } = useModalInformation<Item.Movie>(
- modal.modalKey
- );
+ const { payload } = useModalInformation<Item.Movie>(modal.modalKey);
const profile = useProfileBy(payload?.profileId);
const availableLanguages = useProfileItemsToLanguages(profile);
- const [pending, setPending] = useState<PendingSubtitle[]>([]);
-
- const filelist = useMemo(() => pending.map((v) => v.file), [pending]);
-
- const setFiles = useCallback(
- (files: File[]) => {
- const list: PendingSubtitle[] = files.map((v) => ({
- file: v,
- forced: availableLanguages[0].forced ?? false,
- language: availableLanguages[0],
- }));
- setPending(list);
+ const update = useCallback(async (list: PendingSubtitle<Payload>[]) => {
+ return list;
+ }, []);
+
+ const validate = useCallback<Validator<Payload>>(
+ (item) => {
+ if (item.language === null) {
+ return {
+ state: "error",
+ messages: ["Language is not selected"],
+ };
+ } else if (
+ payload?.subtitles.find((v) => v.code2 === item.language?.code2) !==
+ undefined
+ ) {
+ return {
+ state: "warning",
+ messages: ["Override existing subtitle"],
+ };
+ }
+ return {
+ state: "valid",
+ messages: [],
+ };
},
- [availableLanguages]
+ [payload?.subtitles]
);
- const upload = useCallback(() => {
- if (payload === null || pending.length === 0) {
- return;
- }
-
- const { radarrId } = payload;
-
- const tasks = pending.map((v) => {
- const { file, language, forced } = v;
-
- return createTask(
- file.name,
- radarrId,
- MoviesApi.uploadSubtitles.bind(MoviesApi),
- radarrId,
- {
- file: file,
- forced,
- hi: false,
- language: language.code2,
- }
- );
- });
-
- dispatchTask(TaskGroupName, tasks, "Uploading...");
- setFiles([]);
- closeModal();
- }, [payload, closeModal, pending, setFiles]);
-
- const modify = useCallback(
- (row: Row<PendingSubtitle>, info?: PendingSubtitle) => {
- setPending((pd) => {
- const newPending = [...pd];
- if (info) {
- newPending[row.index] = info;
- } else {
- newPending.splice(row.index, 1);
- }
- return newPending;
- });
+ const upload = useCallback(
+ (items: PendingSubtitle<Payload>[]) => {
+ if (payload === null) {
+ return;
+ }
+
+ const { radarrId } = payload;
+
+ const tasks = items
+ .filter((v) => v.language !== null)
+ .map((v) => {
+ const {
+ file,
+ language,
+ payload: { forced },
+ } = v;
+
+ return createTask(
+ file.name,
+ radarrId,
+ MoviesApi.uploadSubtitles.bind(MoviesApi),
+ radarrId,
+ {
+ file: file,
+ forced,
+ hi: false,
+ language: language!.code2,
+ }
+ );
+ });
+
+ dispatchTask(TaskGroupName, tasks, "Uploading...");
},
- []
+ [payload]
);
- const columns = useMemo<Column<PendingSubtitle>[]>(
+ const columns = useMemo<Column<PendingSubtitle<Payload>>[]>(
() => [
{
- id: "state",
- Cell: () => {
- return "hello";
- },
- },
- {
- id: "name",
- Header: "File",
- accessor: (d) => d.file.name,
- },
- {
+ id: "forced",
Header: "Forced",
- accessor: "forced",
+ accessor: "payload",
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}
+ checked={value.forced}
onChange={(v) => {
const newInfo = { ...row.original };
- newInfo.forced = v.target.checked;
+ newInfo.payload.forced = v.target.checked;
update && update(row, newInfo);
}}
></Form.Check>
);
},
},
- {
- Header: "Language",
- accessor: "language",
- className: "w-25",
- Cell: ({ row, update, value }) => {
- return (
- <LanguageSelector
- options={availableLanguages}
- value={value}
- onChange={(lang) => {
- if (lang && update) {
- const newInfo = { ...row.original };
- newInfo.language = lang;
- update(row, newInfo);
- }
- }}
- ></LanguageSelector>
- );
- },
- },
- {
- accessor: "file",
- Cell: ({ row, update }) => {
- return (
- <Button
- size="sm"
- variant="light"
- onClick={() => {
- update && update(row);
- }}
- >
- <FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>
- </Button>
- );
- },
- },
],
- [availableLanguages]
- );
-
- const canUpload = pending.length > 0;
-
- const footer = (
- <Button disabled={!canUpload} onClick={upload}>
- Upload
- </Button>
+ []
);
return (
- <BaseModal title={`Upload - ${payload?.title}`} footer={footer} {...modal}>
- <Container fluid className="flex-column">
- <Form>
- <Form.Group>
- <FileForm
- emptyText="Select..."
- disabled={canUpload || availableLanguages.length === 0}
- multiple
- value={filelist}
- onChange={setFiles}
- ></FileForm>
- </Form.Group>
- </Form>
- <div hidden={!canUpload}>
- <SimpleTable
- columns={columns}
- data={pending}
- responsive={false}
- update={modify}
- ></SimpleTable>
- </div>
- </Container>
- </BaseModal>
+ <SubtitleUploadModal
+ hideAllLanguages
+ initial={{ forced: false }}
+ availableLanguages={availableLanguages}
+ columns={columns}
+ upload={upload}
+ update={update}
+ validate={validate}
+ {...modal}
+ ></SubtitleUploadModal>
);
};
diff --git a/frontend/src/components/modals/SeriesUploadModal.tsx b/frontend/src/components/modals/SeriesUploadModal.tsx
index e91e7fb04..dd5a9040e 100644
--- a/frontend/src/components/modals/SeriesUploadModal.tsx
+++ b/frontend/src/components/modals/SeriesUploadModal.tsx
@@ -1,213 +1,126 @@
-import {
- faCheck,
- faCircleNotch,
- faInfoCircle,
- faTrash,
-} from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import React, {
- FunctionComponent,
- useCallback,
- useEffect,
- useMemo,
- useState,
-} from "react";
-import { Button, Container, Form } from "react-bootstrap";
-import { Column, TableUpdater } from "react-table";
-import { FileForm, LanguageSelector, MessageIcon, SimpleTable } from "..";
+import React, { FunctionComponent, useCallback, useMemo } from "react";
+import { Column } from "react-table";
import { dispatchTask } from "../../@modules/task";
import { createTask } from "../../@modules/task/utilities";
import { useProfileBy, useProfileItemsToLanguages } from "../../@redux/hooks";
import { EpisodesApi, SubtitlesApi } from "../../apis";
import { Selector } from "../inputs";
-import BaseModal, { BaseModalProps } from "./BaseModal";
+import { BaseModalProps } from "./BaseModal";
import { useModalInformation } from "./hooks";
+import SubtitleUploadModal, {
+ PendingSubtitle,
+ Validator,
+} from "./SubtitleUploadModal";
-interface PendingSubtitle {
- file: File;
- fetching: boolean;
- instance?: Item.Episode;
+interface Payload {
+ instance: Item.Episode | null;
}
-type EpisodeMap = {
- [name: string]: Item.Episode;
-};
-
-interface SerieProps {
+interface SeriesProps {
episodes: readonly Item.Episode[];
}
export const TaskGroupName = "Uploading Subtitles...";
-const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
+const SeriesUploadModal: FunctionComponent<SeriesProps & BaseModalProps> = ({
episodes,
...modal
}) => {
- const { payload, closeModal } = useModalInformation<Item.Series>(
- modal.modalKey
- );
-
- const [pending, setPending] = useState<PendingSubtitle[]>([]);
+ const { payload } = useModalInformation<Item.Series>(modal.modalKey);
const profile = useProfileBy(payload?.profileId);
- const avaliableLanguages = useProfileItemsToLanguages(profile);
-
- const [language, setLanguage] = useState<Language.Info | null>(null);
-
- useEffect(() => {
- if (avaliableLanguages.length > 0) {
- setLanguage(avaliableLanguages[0]);
- }
- }, [avaliableLanguages]);
-
- const filelist = useMemo(() => pending.map((v) => v.file), [pending]);
+ const availableLanguages = useProfileItemsToLanguages(profile);
- const checkEpisodes = useCallback(
- async (list: PendingSubtitle[]) => {
+ const update = useCallback(
+ async (list: PendingSubtitle<Payload>[]) => {
+ const newList = [...list];
const names = list.map((v) => v.file.name);
if (names.length > 0) {
const results = await SubtitlesApi.info(names);
- const episodeMap = results.reduce<EpisodeMap>((prev, curr) => {
- const ep = episodes.find(
- (v) => v.season === curr.season && v.episode === curr.episode
- );
- if (ep) {
- prev[curr.filename] = ep;
+ // TODO: Optimization
+ newList.forEach((v) => {
+ const info = results.find((f) => f.filename === v.file.name);
+ if (info) {
+ v.payload.instance =
+ episodes.find(
+ (e) => e.season === info.season && e.episode === info.episode
+ ) ?? null;
}
- return prev;
- }, {});
-
- setPending((pd) =>
- pd.map((v) => {
- const instance = episodeMap[v.file.name];
- return {
- ...v,
- instance,
- fetching: false,
- };
- })
- );
+ });
}
- },
- [episodes]
- );
- const setFiles = useCallback(
- (files: File[]) => {
- // At lease 1 language is required
- const list: PendingSubtitle[] = files.map((f) => {
- return {
- file: f,
- didCheck: false,
- fetching: true,
- };
- });
- setPending(list);
- checkEpisodes(list);
+ return newList;
},
- [checkEpisodes]
+ [episodes]
);
- const upload = useCallback(() => {
- if (payload === null || language === null) {
- return;
+ const validate = useCallback<Validator<Payload>>((item) => {
+ const { language } = item;
+ const { instance } = item.payload;
+ if (language === null || instance === null) {
+ return {
+ state: "error",
+ messages: ["Language or Episode is not selected"],
+ };
+ } else if (
+ instance.subtitles.find((v) => v.code2 === language.code2) !== undefined
+ ) {
+ return {
+ state: "warning",
+ messages: ["Override existing subtitle"],
+ };
}
+ return {
+ state: "valid",
+ messages: [],
+ };
+ }, []);
+
+ const upload = useCallback(
+ (items: PendingSubtitle<Payload>[]) => {
+ if (payload === null) {
+ return;
+ }
- const { sonarrSeriesId: seriesid } = payload;
- const { code2, hi, forced } = language;
-
- const tasks = pending
- .filter((v) => v.instance !== undefined)
- .map((v) => {
- const { sonarrEpisodeId: episodeid } = v.instance!;
-
- const form: FormType.UploadSubtitle = {
- file: v.file,
- language: code2,
- hi: hi ?? false,
- forced: forced ?? false,
- };
-
- return createTask(
- v.file.name,
- episodeid,
- EpisodesApi.uploadSubtitles.bind(EpisodesApi),
- seriesid,
- episodeid,
- form
- );
- });
-
- dispatchTask(TaskGroupName, tasks, "Uploading subtitles...");
- setFiles([]);
- closeModal();
- }, [payload, pending, language, closeModal, setFiles]);
+ const { sonarrSeriesId: seriesid } = payload;
+
+ const tasks = items
+ .filter((v) => v.payload.instance !== undefined)
+ .map((v) => {
+ const { code2, hi, forced } = v.language!;
+ const { sonarrEpisodeId: episodeid } = v.payload.instance!;
+
+ const form: FormType.UploadSubtitle = {
+ file: v.file,
+ language: code2,
+ hi: hi ?? false,
+ forced: forced ?? false,
+ };
+
+ return createTask(
+ v.file.name,
+ episodeid,
+ EpisodesApi.uploadSubtitles.bind(EpisodesApi),
+ seriesid,
+ episodeid,
+ form
+ );
+ });
- const canUpload = useMemo(
- () =>
- pending.length > 0 &&
- pending.every((v) => v.instance !== undefined) &&
- language,
- [pending, language]
+ dispatchTask(TaskGroupName, tasks, "Uploading subtitles...");
+ },
+ [payload]
);
- const showTable = pending.length > 0;
-
- const columns = useMemo<Column<PendingSubtitle>[]>(
+ const columns = useMemo<Column<PendingSubtitle<Payload>>[]>(
() => [
{
- id: "Icon",
- accessor: "fetching",
- className: "text-center",
- Cell: ({ value: fetching, row: { original } }) => {
- let icon = faCircleNotch;
- let color: string | undefined = undefined;
- let spin = false;
- let msgs: string[] = [];
-
- const override = useMemo(
- () =>
- original.instance?.subtitles.find(
- (v) => v.code2 === language?.code2
- ) !== undefined,
- [original.instance?.subtitles]
- );
-
- if (fetching) {
- spin = true;
- } else if (override) {
- icon = faInfoCircle;
- color = "var(--warning)";
- msgs.push("Overwrite existing subtitle");
- } else if (original.instance) {
- icon = faCheck;
- color = "var(--success)";
- } else {
- icon = faInfoCircle;
- color = "var(--warning)";
- msgs.push("Season or episode info is missing");
- }
-
- return (
- <MessageIcon
- messages={msgs}
- color={color}
- icon={icon}
- spin={spin}
- ></MessageIcon>
- );
- },
- },
- {
- Header: "File",
- accessor: (d) => d.file.name,
- },
- {
+ id: "instance",
Header: "Episode",
- accessor: "instance",
+ accessor: "payload",
className: "vw-1",
Cell: ({ value, row, update }) => {
const options = episodes.map<SelectorOption<Item.Episode>>((ep) => ({
@@ -219,7 +132,7 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
(ep: Nullable<Item.Episode>) => {
if (ep) {
const newInfo = { ...row.original };
- newInfo.instance = ep;
+ newInfo.payload.instance = ep;
update && update(row, newInfo);
}
},
@@ -228,101 +141,28 @@ const SeriesUploadModal: FunctionComponent<SerieProps & BaseModalProps> = ({
return (
<Selector
+ disabled={row.original.state === "fetching"}
options={options}
- value={value ?? null}
+ value={value.instance}
onChange={change}
></Selector>
);
},
},
- {
- accessor: "file",
- Cell: ({ row, update }) => {
- return (
- <Button
- size="sm"
- variant="light"
- onClick={() => {
- update && update(row);
- }}
- >
- <FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>
- </Button>
- );
- },
- },
],
- [language?.code2, episodes]
- );
-
- const updateItem = useCallback<TableUpdater<PendingSubtitle>>(
- (row, info?: PendingSubtitle) => {
- setPending((pd) => {
- const newPending = [...pd];
- if (info) {
- newPending[row.index] = info;
- } else {
- newPending.splice(row.index, 1);
- }
- return newPending;
- });
- },
- []
- );
-
- const footer = (
- <div className="d-flex flex-row flex-grow-1 justify-content-between">
- <div className="w-25">
- <LanguageSelector
- options={avaliableLanguages}
- value={language}
- onChange={(l) => {
- if (l) {
- setLanguage(l);
- }
- }}
- ></LanguageSelector>
- </div>
- <div>
- <Button
- disabled={pending.length === 0}
- variant="outline-secondary"
- className="mr-2"
- onClick={() => setFiles([])}
- >
- Clean
- </Button>
- <Button disabled={!canUpload} onClick={upload}>
- Upload
- </Button>
- </div>
- </div>
+ [episodes]
);
return (
- <BaseModal size="lg" title="Upload Subtitles" footer={footer} {...modal}>
- <Container fluid className="flex-column">
- <Form>
- <Form.Group>
- <FileForm
- emptyText="Select..."
- disabled={showTable || avaliableLanguages.length === 0}
- multiple
- value={filelist}
- onChange={setFiles}
- ></FileForm>
- </Form.Group>
- </Form>
- <div hidden={!showTable}>
- <SimpleTable
- columns={columns}
- data={pending}
- responsive={false}
- update={updateItem}
- ></SimpleTable>
- </div>
- </Container>
- </BaseModal>
+ <SubtitleUploadModal
+ columns={columns}
+ initial={{ instance: null }}
+ availableLanguages={availableLanguages}
+ upload={upload}
+ update={update}
+ validate={validate}
+ {...modal}
+ ></SubtitleUploadModal>
);
};
diff --git a/frontend/src/components/modals/SubtitleUploadModal.tsx b/frontend/src/components/modals/SubtitleUploadModal.tsx
new file mode 100644
index 000000000..4584826fb
--- /dev/null
+++ b/frontend/src/components/modals/SubtitleUploadModal.tsx
@@ -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>
+ );
+}