diff options
author | LASER-Yi <[email protected]> | 2022-03-27 16:03:04 +0800 |
---|---|---|
committer | LASER-Yi <[email protected]> | 2022-03-27 16:03:04 +0800 |
commit | e18657e4261cae67d6fe5a235a001dede26721c5 (patch) | |
tree | ca281c608d0e3222fb0448803bea170528fbbdba /frontend/src | |
parent | 658237dd5076a3d4823552ad17c101d3ba6177fc (diff) | |
download | bazarr-e18657e4261cae67d6fe5a235a001dede26721c5.tar.gz bazarr-e18657e4261cae67d6fe5a235a001dede26721c5.zip |
Improve subtitle tools
Diffstat (limited to 'frontend/src')
13 files changed, 593 insertions, 463 deletions
diff --git a/frontend/src/components/modals/SubtitleToolModal.tsx b/frontend/src/components/modals/SubtitleToolModal.tsx deleted file mode 100644 index 823aca5a7..000000000 --- a/frontend/src/components/modals/SubtitleToolModal.tsx +++ /dev/null @@ -1,453 +0,0 @@ -import { useSubtitleAction } from "@/apis/hooks"; -import { - useModal, - useModalControl, - usePayload, - withModal, -} from "@/modules/modals"; -import { createTask, dispatchTask } from "@/modules/task/utilities"; -import { isMovie, submodProcessColor } from "@/utilities"; -import { LOG } from "@/utilities/console"; -import { useEnabledLanguages } from "@/utilities/languages"; -import { - faClock, - faCode, - faDeaf, - faExchangeAlt, - faFilm, - faImage, - faLanguage, - faMagic, - faMinus, - faPaintBrush, - faPlay, - faPlus, - faTextHeight, -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - ChangeEventHandler, - FunctionComponent, - useCallback, - useMemo, - useState, -} from "react"; -import { - Badge, - Button, - ButtonGroup, - Dropdown, - Form, - InputGroup, -} from "react-bootstrap"; -import { Column, useRowSelect } from "react-table"; -import { - ActionButton, - ActionButtonItem, - LanguageSelector, - Selector, - SimpleTable, -} from ".."; -import Language from "../bazarr/Language"; -import { useCustomSelection } from "../tables/plugins"; -import { availableTranslation, colorOptions } from "./toolOptions"; - -type SupportType = Item.Episode | Item.Movie; - -type TableColumnType = FormType.ModifySubtitle & { - _language: Language.Info; -}; - -function getIdAndType(item: SupportType): [number, "episode" | "movie"] { - if (isMovie(item)) { - return [item.radarrId, "movie"]; - } else { - return [item.sonarrEpisodeId, "episode"]; - } -} - -function submodProcessFrameRate(from: number, to: number) { - return `change_FPS(from=${from},to=${to})`; -} - -function submodProcessOffset(h: number, m: number, s: number, ms: number) { - return `shift_offset(h=${h},m=${m},s=${s},ms=${ms})`; -} - -interface ToolModalProps { - process: ( - action: string, - override?: Partial<FormType.ModifySubtitle> - ) => void; -} - -const ColorTool: FunctionComponent<ToolModalProps> = ({ process }) => { - const [selection, setSelection] = useState<Nullable<string>>(null); - - const Modal = useModal(); - - const submit = useCallback(() => { - if (selection) { - const action = submodProcessColor(selection); - process(action); - } - }, [selection, process]); - - const footer = ( - <Button disabled={selection === null} onClick={submit}> - Save - </Button> - ); - - return ( - <Modal title="Choose Color" footer={footer}> - <Selector options={colorOptions} onChange={setSelection}></Selector> - </Modal> - ); -}; - -const ColorToolModal = withModal(ColorTool, "color-tool"); - -const FrameRateTool: FunctionComponent<ToolModalProps> = ({ process }) => { - const [from, setFrom] = useState<Nullable<number>>(null); - const [to, setTo] = useState<Nullable<number>>(null); - - const canSave = from !== null && to !== null && from !== to; - - const Modal = useModal(); - - const submit = useCallback(() => { - if (canSave) { - const action = submodProcessFrameRate(from, to); - process(action); - } - }, [canSave, from, to, process]); - - const footer = ( - <Button disabled={!canSave} onClick={submit}> - Save - </Button> - ); - - return ( - <Modal title="Change Frame Rate" footer={footer}> - <InputGroup className="px-2"> - <Form.Control - placeholder="From" - type="number" - onChange={(e) => { - const value = parseFloat(e.currentTarget.value); - if (isNaN(value)) { - setFrom(null); - } else { - setFrom(value); - } - }} - ></Form.Control> - <Form.Control - placeholder="To" - type="number" - onChange={(e) => { - const value = parseFloat(e.currentTarget.value); - if (isNaN(value)) { - setTo(null); - } else { - setTo(value); - } - }} - ></Form.Control> - </InputGroup> - </Modal> - ); -}; - -const FrameRateModal = withModal(FrameRateTool, "frame-rate-tool"); - -const TimeAdjustmentTool: FunctionComponent<ToolModalProps> = ({ process }) => { - const [isPlus, setPlus] = useState(true); - const [offset, setOffset] = useState<[number, number, number, number]>([ - 0, 0, 0, 0, - ]); - - const Modal = useModal(); - - const updateOffset = useCallback( - (idx: number): ChangeEventHandler<HTMLInputElement> => { - return (e) => { - let value = parseFloat(e.currentTarget.value); - if (isNaN(value)) { - value = 0; - } - const newOffset = [...offset] as [number, number, number, number]; - newOffset[idx] = value; - setOffset(newOffset); - }; - }, - [offset] - ); - - const canSave = offset.some((v) => v !== 0); - - const submit = useCallback(() => { - if (canSave) { - const newOffset = offset.map((v) => (isPlus ? v : -v)); - const action = submodProcessOffset( - newOffset[0], - newOffset[1], - newOffset[2], - newOffset[3] - ); - process(action); - } - }, [process, canSave, offset, isPlus]); - - const footer = ( - <Button disabled={!canSave} onClick={submit}> - Save - </Button> - ); - - return ( - <Modal title="Adjust Times" footer={footer}> - <InputGroup> - <InputGroup.Prepend> - <Button - variant="secondary" - title={isPlus ? "Later" : "Earlier"} - onClick={() => setPlus(!isPlus)} - > - <FontAwesomeIcon icon={isPlus ? faPlus : faMinus}></FontAwesomeIcon> - </Button> - </InputGroup.Prepend> - <Form.Control - type="number" - placeholder="hour" - onChange={updateOffset(0)} - ></Form.Control> - <Form.Control - type="number" - placeholder="min" - onChange={updateOffset(1)} - ></Form.Control> - <Form.Control - type="number" - placeholder="sec" - onChange={updateOffset(2)} - ></Form.Control> - <Form.Control - type="number" - placeholder="ms" - onChange={updateOffset(3)} - ></Form.Control> - </InputGroup> - </Modal> - ); -}; - -const TimeAdjustmentModal = withModal(TimeAdjustmentTool, "time-adjust-tool"); - -const TranslationTool: FunctionComponent<ToolModalProps> = ({ process }) => { - const { data: languages } = useEnabledLanguages(); - - const available = useMemo( - () => languages.filter((v) => v.code2 in availableTranslation), - [languages] - ); - - const Modal = useModal(); - - const [selectedLanguage, setLanguage] = - useState<Nullable<Language.Info>>(null); - - const submit = useCallback(() => { - if (selectedLanguage) { - process("translate", { language: selectedLanguage.code2 }); - } - }, [selectedLanguage, process]); - - const footer = ( - <Button disabled={!selectedLanguage} onClick={submit}> - Translate - </Button> - ); - return ( - <Modal title="Translation" footer={footer}> - <Form.Label> - Enabled languages not listed here are unsupported by Google Translate. - </Form.Label> - <LanguageSelector - options={available} - onChange={setLanguage} - ></LanguageSelector> - </Modal> - ); -}; - -const TranslationModal = withModal(TranslationTool, "translate-tool"); - -const CanSelectSubtitle = (item: TableColumnType) => { - return item.path.endsWith(".srt"); -}; - -const STM: FunctionComponent = () => { - const payload = usePayload<SupportType[]>(); - const [selections, setSelections] = useState<TableColumnType[]>([]); - - const Modal = useModal({ size: "xl" }); - const { hide } = useModalControl(); - - const { mutateAsync } = useSubtitleAction(); - - const process = useCallback( - (action: string, override?: Partial<FormType.ModifySubtitle>) => { - LOG("info", "executing action", action); - hide(); - const tasks = selections.map((s) => { - const form: FormType.ModifySubtitle = { - id: s.id, - type: s.type, - language: s.language, - path: s.path, - ...override, - }; - return createTask(s.path, mutateAsync, { action, form }); - }); - - dispatchTask(tasks, "modify-subtitles"); - }, - [hide, selections, mutateAsync] - ); - - const { show } = useModalControl(); - - const columns: Column<TableColumnType>[] = useMemo<Column<TableColumnType>[]>( - () => [ - { - Header: "Language", - accessor: "_language", - Cell: ({ value }) => ( - <Badge variant="secondary"> - <Language.Text value={value} long></Language.Text> - </Badge> - ), - }, - { - id: "file", - Header: "File", - accessor: "path", - Cell: ({ value }) => { - const path = value; - - let idx = path.lastIndexOf("/"); - - if (idx === -1) { - idx = path.lastIndexOf("\\"); - } - - if (idx !== -1) { - return path.slice(idx + 1); - } else { - return path; - } - }, - }, - ], - [] - ); - - const data = useMemo<TableColumnType[]>( - () => - payload?.flatMap((item) => { - const [id, type] = getIdAndType(item); - return item.subtitles.flatMap((v) => { - if (v.path !== null) { - return [ - { - id, - type, - language: v.code2, - path: v.path, - _language: v, - }, - ]; - } else { - return []; - } - }); - }) ?? [], - [payload] - ); - - const plugins = [useRowSelect, useCustomSelection]; - - const footer = ( - <Dropdown as={ButtonGroup} onSelect={(k) => k && process(k)}> - <ActionButton - size="sm" - disabled={selections.length === 0} - icon={faPlay} - onClick={() => process("sync")} - > - Sync - </ActionButton> - <Dropdown.Toggle - disabled={selections.length === 0} - split - variant="light" - size="sm" - className="px-2" - ></Dropdown.Toggle> - <Dropdown.Menu> - <Dropdown.Item eventKey="remove_HI"> - <ActionButtonItem icon={faDeaf}>Remove HI Tags</ActionButtonItem> - </Dropdown.Item> - <Dropdown.Item eventKey="remove_tags"> - <ActionButtonItem icon={faCode}>Remove Style Tags</ActionButtonItem> - </Dropdown.Item> - <Dropdown.Item eventKey="OCR_fixes"> - <ActionButtonItem icon={faImage}>OCR Fixes</ActionButtonItem> - </Dropdown.Item> - <Dropdown.Item eventKey="common"> - <ActionButtonItem icon={faMagic}>Common Fixes</ActionButtonItem> - </Dropdown.Item> - <Dropdown.Item eventKey="fix_uppercase"> - <ActionButtonItem icon={faTextHeight}>Fix Uppercase</ActionButtonItem> - </Dropdown.Item> - <Dropdown.Item eventKey="reverse_rtl"> - <ActionButtonItem icon={faExchangeAlt}>Reverse RTL</ActionButtonItem> - </Dropdown.Item> - <Dropdown.Item onSelect={() => show(ColorToolModal)}> - <ActionButtonItem icon={faPaintBrush}>Add Color</ActionButtonItem> - </Dropdown.Item> - <Dropdown.Item onSelect={() => show(FrameRateModal)}> - <ActionButtonItem icon={faFilm}>Change Frame Rate</ActionButtonItem> - </Dropdown.Item> - <Dropdown.Item onSelect={() => show(TimeAdjustmentModal)}> - <ActionButtonItem icon={faClock}>Adjust Times</ActionButtonItem> - </Dropdown.Item> - <Dropdown.Item onSelect={() => show(TranslationModal)}> - <ActionButtonItem icon={faLanguage}>Translate</ActionButtonItem> - </Dropdown.Item> - </Dropdown.Menu> - </Dropdown> - ); - - return ( - <Modal title="Subtitle Tools" footer={footer}> - <SimpleTable - emptyText="No External Subtitles Found" - plugins={plugins} - columns={columns} - onSelect={setSelections} - canSelect={CanSelectSubtitle} - data={data} - ></SimpleTable> - <ColorToolModal process={process}></ColorToolModal> - <FrameRateModal process={process}></FrameRateModal> - <TimeAdjustmentModal process={process}></TimeAdjustmentModal> - <TranslationModal process={process}></TranslationModal> - </Modal> - ); -}; - -export default withModal(STM, "subtitle-tools"); diff --git a/frontend/src/components/modals/index.ts b/frontend/src/components/modals/index.ts index f52d9228d..b5b223abf 100644 --- a/frontend/src/components/modals/index.ts +++ b/frontend/src/components/modals/index.ts @@ -2,4 +2,3 @@ export * from "./HistoryModal"; export { default as ItemEditorModal } from "./ItemEditorModal"; export { default as MovieUploadModal } from "./MovieUploadModal"; export { default as SeriesUploadModal } from "./SeriesUploadModal"; -export { default as SubtitleToolModal } from "./SubtitleToolModal"; diff --git a/frontend/src/components/modals/subtitle-tools/ColorTool.tsx b/frontend/src/components/modals/subtitle-tools/ColorTool.tsx new file mode 100644 index 000000000..b5ae20acc --- /dev/null +++ b/frontend/src/components/modals/subtitle-tools/ColorTool.tsx @@ -0,0 +1,36 @@ +import { Selector } from "@/components"; +import { useModal, withModal } from "@/modules/modals"; +import { submodProcessColor } from "@/utilities"; +import { FunctionComponent, useCallback, useState } from "react"; +import { Button } from "react-bootstrap"; +import { colorOptions } from "../toolOptions"; +import { useProcess } from "./ToolContext"; + +const ColorTool: FunctionComponent = () => { + const [selection, setSelection] = useState<Nullable<string>>(null); + + const Modal = useModal(); + + const process = useProcess(); + + const submit = useCallback(() => { + if (selection) { + const action = submodProcessColor(selection); + process(action); + } + }, [process, selection]); + + const footer = ( + <Button disabled={selection === null} onClick={submit}> + Save + </Button> + ); + + return ( + <Modal title="Choose Color" footer={footer}> + <Selector options={colorOptions} onChange={setSelection}></Selector> + </Modal> + ); +}; + +export default withModal(ColorTool, "color-tool"); diff --git a/frontend/src/components/modals/subtitle-tools/FrameRateTool.tsx b/frontend/src/components/modals/subtitle-tools/FrameRateTool.tsx new file mode 100644 index 000000000..4c72e4d1b --- /dev/null +++ b/frontend/src/components/modals/subtitle-tools/FrameRateTool.tsx @@ -0,0 +1,65 @@ +import { useModal, withModal } from "@/modules/modals"; +import { FunctionComponent, useCallback, useState } from "react"; +import { Button, Form, InputGroup } from "react-bootstrap"; +import { useProcess } from "./ToolContext"; + +function submodProcessFrameRate(from: number, to: number) { + return `change_FPS(from=${from},to=${to})`; +} + +const FrameRateTool: FunctionComponent = () => { + const [from, setFrom] = useState<Nullable<number>>(null); + const [to, setTo] = useState<Nullable<number>>(null); + + const canSave = from !== null && to !== null && from !== to; + + const Modal = useModal(); + + const process = useProcess(); + + const submit = useCallback(() => { + if (canSave) { + const action = submodProcessFrameRate(from, to); + process(action); + } + }, [canSave, from, process, to]); + + const footer = ( + <Button disabled={!canSave} onClick={submit}> + Save + </Button> + ); + + return ( + <Modal title="Change Frame Rate" footer={footer}> + <InputGroup className="px-2"> + <Form.Control + placeholder="From" + type="number" + onChange={(e) => { + const value = parseFloat(e.currentTarget.value); + if (isNaN(value)) { + setFrom(null); + } else { + setFrom(value); + } + }} + ></Form.Control> + <Form.Control + placeholder="To" + type="number" + onChange={(e) => { + const value = parseFloat(e.currentTarget.value); + if (isNaN(value)) { + setTo(null); + } else { + setTo(value); + } + }} + ></Form.Control> + </InputGroup> + </Modal> + ); +}; + +export default withModal(FrameRateTool, "frame-rate-tool"); diff --git a/frontend/src/components/modals/subtitle-tools/TimeTool.tsx b/frontend/src/components/modals/subtitle-tools/TimeTool.tsx new file mode 100644 index 000000000..6cbf62f4a --- /dev/null +++ b/frontend/src/components/modals/subtitle-tools/TimeTool.tsx @@ -0,0 +1,100 @@ +import { useModal, withModal } from "@/modules/modals"; +import { faMinus, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + ChangeEventHandler, + FunctionComponent, + useCallback, + useState, +} from "react"; +import { Button, Form, InputGroup } from "react-bootstrap"; +import { useProcess } from "./ToolContext"; + +function submodProcessOffset(h: number, m: number, s: number, ms: number) { + return `shift_offset(h=${h},m=${m},s=${s},ms=${ms})`; +} + +const TimeAdjustmentTool: FunctionComponent = () => { + const [isPlus, setPlus] = useState(true); + const [offset, setOffset] = useState<[number, number, number, number]>([ + 0, 0, 0, 0, + ]); + + const Modal = useModal(); + + const updateOffset = useCallback( + (idx: number): ChangeEventHandler<HTMLInputElement> => { + return (e) => { + let value = parseFloat(e.currentTarget.value); + if (isNaN(value)) { + value = 0; + } + const newOffset = [...offset] as [number, number, number, number]; + newOffset[idx] = value; + setOffset(newOffset); + }; + }, + [offset] + ); + + const canSave = offset.some((v) => v !== 0); + + const process = useProcess(); + + const submit = useCallback(() => { + if (canSave) { + const newOffset = offset.map((v) => (isPlus ? v : -v)); + const action = submodProcessOffset( + newOffset[0], + newOffset[1], + newOffset[2], + newOffset[3] + ); + process(action); + } + }, [canSave, offset, process, isPlus]); + + const footer = ( + <Button disabled={!canSave} onClick={submit}> + Save + </Button> + ); + + return ( + <Modal title="Adjust Times" footer={footer}> + <InputGroup> + <InputGroup.Prepend> + <Button + variant="secondary" + title={isPlus ? "Later" : "Earlier"} + onClick={() => setPlus(!isPlus)} + > + <FontAwesomeIcon icon={isPlus ? faPlus : faMinus}></FontAwesomeIcon> + </Button> + </InputGroup.Prepend> + <Form.Control + type="number" + placeholder="hour" + onChange={updateOffset(0)} + ></Form.Control> + <Form.Control + type="number" + placeholder="min" + onChange={updateOffset(1)} + ></Form.Control> + <Form.Control + type="number" + placeholder="sec" + onChange={updateOffset(2)} + ></Form.Control> + <Form.Control + type="number" + placeholder="ms" + onChange={updateOffset(3)} + ></Form.Control> + </InputGroup> + </Modal> + ); +}; + +export default withModal(TimeAdjustmentTool, "time-adjustment"); diff --git a/frontend/src/components/modals/subtitle-tools/ToolContext.ts b/frontend/src/components/modals/subtitle-tools/ToolContext.ts new file mode 100644 index 000000000..5f1aecaa7 --- /dev/null +++ b/frontend/src/components/modals/subtitle-tools/ToolContext.ts @@ -0,0 +1,14 @@ +import { createContext, useContext } from "react"; + +export type ProcessSubtitleType = ( + action: string, + override?: Partial<FormType.ModifySubtitle> +) => void; + +export const ProcessSubtitleContext = createContext<ProcessSubtitleType>(() => { + throw new Error("ProcessSubtitleContext not initialized"); +}); + +export function useProcess() { + return useContext(ProcessSubtitleContext); +} diff --git a/frontend/src/components/modals/subtitle-tools/Translation.tsx b/frontend/src/components/modals/subtitle-tools/Translation.tsx new file mode 100644 index 000000000..5f87c3121 --- /dev/null +++ b/frontend/src/components/modals/subtitle-tools/Translation.tsx @@ -0,0 +1,48 @@ +import { LanguageSelector } from "@/components/LanguageSelector"; +import { useModal, withModal } from "@/modules/modals"; +import { useEnabledLanguages } from "@/utilities/languages"; +import { FunctionComponent, useCallback, useMemo, useState } from "react"; +import { Button, Form } from "react-bootstrap"; +import { availableTranslation } from "../toolOptions"; +import { useProcess } from "./ToolContext"; + +const TranslationTool: FunctionComponent = () => { + const { data: languages } = useEnabledLanguages(); + + const available = useMemo( + () => languages.filter((v) => v.code2 in availableTranslation), + [languages] + ); + + const Modal = useModal(); + + const [selectedLanguage, setLanguage] = + useState<Nullable<Language.Info>>(null); + + const process = useProcess(); + + const submit = useCallback(() => { + if (selectedLanguage) { + process("translate", { language: selectedLanguage.code2 }); + } + }, [process, selectedLanguage]); + + const footer = ( + <Button disabled={!selectedLanguage} onClick={submit}> + Translate + </Button> + ); + return ( + <Modal title="Translation" footer={footer}> + <Form.Label> + Enabled languages not listed here are unsupported by Google Translate. + </Form.Label> + <LanguageSelector + options={available} + onChange={setLanguage} + ></LanguageSelector> + </Modal> + ); +}; + +export default withModal(TranslationTool, "translation-tool"); diff --git a/frontend/src/components/modals/subtitle-tools/index.tsx b/frontend/src/components/modals/subtitle-tools/index.tsx new file mode 100644 index 000000000..e46cb519e --- /dev/null +++ b/frontend/src/components/modals/subtitle-tools/index.tsx @@ -0,0 +1,230 @@ +import { useSubtitleAction } from "@/apis/hooks"; +import Language from "@/components/bazarr/Language"; +import { ActionButton, ActionButtonItem } from "@/components/buttons"; +import { SimpleTable } from "@/components/tables"; +import { useCustomSelection } from "@/components/tables/plugins"; +import { + useModal, + useModalControl, + usePayload, + withModal, +} from "@/modules/modals"; +import { createTask, dispatchTask } from "@/modules/task/utilities"; +import { isMovie } from "@/utilities"; +import { LOG } from "@/utilities/console"; +import { isObject } from "lodash"; +import { FunctionComponent, useCallback, useMemo, useState } from "react"; +import { Badge, ButtonGroup, Dropdown } from "react-bootstrap"; +import { Column, useRowSelect } from "react-table"; +import { + ProcessSubtitleContext, + ProcessSubtitleType, + useProcess, +} from "./ToolContext"; +import { tools } from "./tools"; +import { ToolOptions } from "./types"; + +type SupportType = Item.Episode | Item.Movie; + +type TableColumnType = FormType.ModifySubtitle & { + raw_language: Language.Info; +}; + +function getIdAndType(item: SupportType): [number, "episode" | "movie"] { + if (isMovie(item)) { + return [item.radarrId, "movie"]; + } else { + return [item.sonarrEpisodeId, "episode"]; + } +} + +const CanSelectSubtitle = (item: TableColumnType) => { + return item.path.endsWith(".srt"); +}; + +function isElement(value: unknown): value is JSX.Element { + return isObject(value); +} + +interface SubtitleToolViewProps { + count: number; + tools: ToolOptions[]; + select: (items: TableColumnType[]) => void; +} + +const SubtitleToolView: FunctionComponent<SubtitleToolViewProps> = ({ + tools, + count, + select, +}) => { + const payload = usePayload<SupportType[]>(); + + const Modal = useModal({ + size: "lg", + }); + const { show } = useModalControl(); + + const columns: Column<TableColumnType>[] = useMemo<Column<TableColumnType>[]>( + () => [ + { + Header: "Language", + accessor: "raw_language", + Cell: ({ value }) => ( + <Badge variant="secondary"> + <Language.Text value={value} long></Language.Text> + </Badge> + ), + }, + { + id: "file", + Header: "File", + accessor: "path", + Cell: ({ value }) => { + const path = value; + + let idx = path.lastIndexOf("/"); + + if (idx === -1) { + idx = path.lastIndexOf("\\"); + } + + if (idx !== -1) { + return path.slice(idx + 1); + } else { + return path; + } + }, + }, + ], + [] + ); + + const data = useMemo<TableColumnType[]>( + () => + payload?.flatMap((item) => { + const [id, type] = getIdAndType(item); + return item.subtitles.flatMap((v) => { + if (v.path !== null) { + return [ + { + id, + type, + language: v.code2, + path: v.path, + raw_language: v, + }, + ]; + } else { + return []; + } + }); + }) ?? [], + [payload] + ); + + const plugins = [useRowSelect, useCustomSelection]; + + const process = useProcess(); + + const footer = useMemo(() => { + const action = tools[0]; + const others = tools.slice(1); + + return ( + <Dropdown as={ButtonGroup} onSelect={(k) => k && process(k)}> + <ActionButton + size="sm" + disabled={count === 0} + icon={action.icon} + onClick={() => process(action.key)} + > + {action.name} + </ActionButton> + <Dropdown.Toggle + disabled={count === 0} + split + variant="light" + size="sm" + className="px-2" + ></Dropdown.Toggle> + <Dropdown.Menu> + {others.map((v) => ( + <Dropdown.Item + key={v.key} + eventKey={v.modal ? undefined : v.key} + onSelect={() => { + if (v.modal) { + show(v.modal); + } + }} + > + <ActionButtonItem icon={v.icon}>{v.name}</ActionButtonItem> + </Dropdown.Item> + ))} + </Dropdown.Menu> + </Dropdown> + ); + }, [count, process, show, tools]); + + return ( + <Modal title="Subtitle Tools" footer={footer}> + <SimpleTable + emptyText="No External Subtitles Found" + plugins={plugins} + columns={columns} + onSelect={select} + canSelect={CanSelectSubtitle} + data={data} + ></SimpleTable> + </Modal> + ); +}; + +export const SubtitleToolModal = withModal(SubtitleToolView, "subtitle-tools"); + +const SubtitleTools: FunctionComponent = () => { + const modals = useMemo( + () => + tools + .map((t) => t.modal && <t.modal key={t.key}></t.modal>) + .filter(isElement), + [] + ); + + const { hide } = useModalControl(); + const [selections, setSelections] = useState<TableColumnType[]>([]); + const { mutateAsync } = useSubtitleAction(); + + const process = useCallback<ProcessSubtitleType>( + (action, override) => { + LOG("info", "executing action", action); + hide(SubtitleToolModal.modalKey); + const tasks = selections.map((s) => { + const form: FormType.ModifySubtitle = { + id: s.id, + type: s.type, + language: s.language, + path: s.path, + ...override, + }; + return createTask(s.path, mutateAsync, { action, form }); + }); + + dispatchTask(tasks, "modify-subtitles"); + }, + [hide, selections, mutateAsync] + ); + + return ( + <ProcessSubtitleContext.Provider value={process}> + <SubtitleToolModal + count={selections.length} + tools={tools} + select={setSelections} + ></SubtitleToolModal> + {modals} + </ProcessSubtitleContext.Provider> + ); +}; + +export default SubtitleTools; diff --git a/frontend/src/components/modals/subtitle-tools/tools.ts b/frontend/src/components/modals/subtitle-tools/tools.ts new file mode 100644 index 000000000..4310e9791 --- /dev/null +++ b/frontend/src/components/modals/subtitle-tools/tools.ts @@ -0,0 +1,80 @@ +import { + faClock, + faCode, + faDeaf, + faExchangeAlt, + faFilm, + faImage, + faLanguage, + faMagic, + faPaintBrush, + faPlay, + faTextHeight, +} from "@fortawesome/free-solid-svg-icons"; +import ColorTool from "./ColorTool"; +import FrameRateTool from "./FrameRateTool"; +import TimeTool from "./TimeTool"; +import Translation from "./Translation"; +import { ToolOptions } from "./types"; + +export const tools: ToolOptions[] = [ + { + key: "sync", + icon: faPlay, + name: "Sync", + }, + { + key: "remove_HI", + icon: faDeaf, + name: "Remove HI Tags", + }, + { + key: "remove_tags", + icon: faCode, + name: "Remove Style Tags", + }, + { + key: "OCR_fixes", + icon: faImage, + name: "OCR Fixes", + }, + { + key: "common", + icon: faMagic, + name: "Common Fixes", + }, + { + key: "fix_uppercase", + icon: faTextHeight, + name: "Fix Uppercase", + }, + { + key: "reverse_rtl", + icon: faExchangeAlt, + name: "Reverse RTL", + }, + { + key: "add_color", + icon: faPaintBrush, + name: "Add Color", + modal: ColorTool, + }, + { + key: "change_frame_rate", + icon: faFilm, + name: "Change Frame Rate", + modal: FrameRateTool, + }, + { + key: "adjust_time", + icon: faClock, + name: "Adjust Times", + modal: TimeTool, + }, + { + key: "translation", + icon: faLanguage, + name: "Translate", + modal: Translation, + }, +]; diff --git a/frontend/src/components/modals/subtitle-tools/types.d.ts b/frontend/src/components/modals/subtitle-tools/types.d.ts new file mode 100644 index 000000000..338ba2231 --- /dev/null +++ b/frontend/src/components/modals/subtitle-tools/types.d.ts @@ -0,0 +1,9 @@ +import { ModalComponent } from "@/modules/modals/WithModal"; +import { IconDefinition } from "@fortawesome/free-solid-svg-icons"; + +export interface ToolOptions { + key: string; + icon: IconDefinition; + name: string; + modal?: ModalComponent<unknown>; +} diff --git a/frontend/src/pages/Episodes/index.tsx b/frontend/src/pages/Episodes/index.tsx index 9915a6293..f61bba3f0 100644 --- a/frontend/src/pages/Episodes/index.tsx +++ b/frontend/src/pages/Episodes/index.tsx @@ -7,11 +7,8 @@ import { } from "@/apis/hooks"; import { ContentHeader, LoadingIndicator } from "@/components"; import ItemOverview from "@/components/ItemOverview"; -import { - ItemEditorModal, - SeriesUploadModal, - SubtitleToolModal, -} from "@/components/modals"; +import { ItemEditorModal, SeriesUploadModal } from "@/components/modals"; +import { SubtitleToolModal } from "@/components/modals/subtitle-tools"; import { useModalControl } from "@/modules/modals"; import { createAndDispatchTask } from "@/modules/task/utilities"; import { useLanguageProfileBy } from "@/utilities/languages"; diff --git a/frontend/src/pages/Episodes/table.tsx b/frontend/src/pages/Episodes/table.tsx index 4a9326201..996755227 100644 --- a/frontend/src/pages/Episodes/table.tsx +++ b/frontend/src/pages/Episodes/table.tsx @@ -1,7 +1,10 @@ import { useDownloadEpisodeSubtitles, useEpisodesProvider } from "@/apis/hooks"; import { ActionButton, GroupTable, TextPopover } from "@/components"; -import { EpisodeHistoryModal, SubtitleToolModal } from "@/components/modals"; +import { EpisodeHistoryModal } from "@/components/modals"; import { EpisodeSearchModal } from "@/components/modals/ManualSearchModal"; +import SubtitleTools, { + SubtitleToolModal, +} from "@/components/modals/subtitle-tools"; import { useModalControl } from "@/modules/modals"; import { useShowOnlyDesired } from "@/modules/redux/hooks"; import { BuildKey, filterSubtitleBy } from "@/utilities"; @@ -209,7 +212,7 @@ const Table: FunctionComponent<Props> = ({ }} emptyText="No Episode Found For This Series" ></GroupTable> - <SubtitleToolModal></SubtitleToolModal> + <SubtitleTools></SubtitleTools> <EpisodeHistoryModal></EpisodeHistoryModal> <EpisodeSearchModal download={download} diff --git a/frontend/src/pages/Movies/Details/index.tsx b/frontend/src/pages/Movies/Details/index.tsx index 41efde6c5..ca0a56ee3 100644 --- a/frontend/src/pages/Movies/Details/index.tsx +++ b/frontend/src/pages/Movies/Details/index.tsx @@ -14,9 +14,11 @@ import { ItemEditorModal, MovieHistoryModal, MovieUploadModal, - SubtitleToolModal, } from "@/components/modals"; import { MovieSearchModal } from "@/components/modals/ManualSearchModal"; +import SubtitleTools, { + SubtitleToolModal, +} from "@/components/modals/subtitle-tools"; import { useModalControl } from "@/modules/modals"; import { createAndDispatchTask } from "@/modules/task/utilities"; import { useLanguageProfileBy } from "@/utilities/languages"; @@ -174,7 +176,7 @@ const MovieDetailView: FunctionComponent = () => { <Table movie={movie} profile={profile} disabled={hasTask}></Table> </Row> <ItemEditorModal mutation={mutation}></ItemEditorModal> - <SubtitleToolModal></SubtitleToolModal> + <SubtitleTools></SubtitleTools> <MovieHistoryModal></MovieHistoryModal> <MovieUploadModal></MovieUploadModal> <MovieSearchModal |