From e18657e4261cae67d6fe5a235a001dede26721c5 Mon Sep 17 00:00:00 2001 From: LASER-Yi Date: Sun, 27 Mar 2022 16:03:04 +0800 Subject: [PATCH] Improve subtitle tools --- .../components/modals/SubtitleToolModal.tsx | 453 ------------------ frontend/src/components/modals/index.ts | 1 - .../modals/subtitle-tools/ColorTool.tsx | 36 ++ .../modals/subtitle-tools/FrameRateTool.tsx | 65 +++ .../modals/subtitle-tools/TimeTool.tsx | 100 ++++ .../modals/subtitle-tools/ToolContext.ts | 14 + .../modals/subtitle-tools/Translation.tsx | 48 ++ .../modals/subtitle-tools/index.tsx | 230 +++++++++ .../components/modals/subtitle-tools/tools.ts | 80 ++++ .../modals/subtitle-tools/types.d.ts | 9 + frontend/src/pages/Episodes/index.tsx | 7 +- frontend/src/pages/Episodes/table.tsx | 7 +- frontend/src/pages/Movies/Details/index.tsx | 6 +- 13 files changed, 593 insertions(+), 463 deletions(-) delete mode 100644 frontend/src/components/modals/SubtitleToolModal.tsx create mode 100644 frontend/src/components/modals/subtitle-tools/ColorTool.tsx create mode 100644 frontend/src/components/modals/subtitle-tools/FrameRateTool.tsx create mode 100644 frontend/src/components/modals/subtitle-tools/TimeTool.tsx create mode 100644 frontend/src/components/modals/subtitle-tools/ToolContext.ts create mode 100644 frontend/src/components/modals/subtitle-tools/Translation.tsx create mode 100644 frontend/src/components/modals/subtitle-tools/index.tsx create mode 100644 frontend/src/components/modals/subtitle-tools/tools.ts create mode 100644 frontend/src/components/modals/subtitle-tools/types.d.ts 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 - ) => void; -} - -const ColorTool: FunctionComponent = ({ process }) => { - const [selection, setSelection] = useState>(null); - - const Modal = useModal(); - - const submit = useCallback(() => { - if (selection) { - const action = submodProcessColor(selection); - process(action); - } - }, [selection, process]); - - const footer = ( - - ); - - return ( - - - - ); -}; - -const ColorToolModal = withModal(ColorTool, "color-tool"); - -const FrameRateTool: FunctionComponent = ({ process }) => { - const [from, setFrom] = useState>(null); - const [to, setTo] = useState>(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 = ( - - ); - - return ( - - - { - const value = parseFloat(e.currentTarget.value); - if (isNaN(value)) { - setFrom(null); - } else { - setFrom(value); - } - }} - > - { - const value = parseFloat(e.currentTarget.value); - if (isNaN(value)) { - setTo(null); - } else { - setTo(value); - } - }} - > - - - ); -}; - -const FrameRateModal = withModal(FrameRateTool, "frame-rate-tool"); - -const TimeAdjustmentTool: FunctionComponent = ({ 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 => { - 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 = ( - - ); - - return ( - - - - - - - - - - - - ); -}; - -const TimeAdjustmentModal = withModal(TimeAdjustmentTool, "time-adjust-tool"); - -const TranslationTool: FunctionComponent = ({ process }) => { - const { data: languages } = useEnabledLanguages(); - - const available = useMemo( - () => languages.filter((v) => v.code2 in availableTranslation), - [languages] - ); - - const Modal = useModal(); - - const [selectedLanguage, setLanguage] = - useState>(null); - - const submit = useCallback(() => { - if (selectedLanguage) { - process("translate", { language: selectedLanguage.code2 }); - } - }, [selectedLanguage, process]); - - const footer = ( - - ); - return ( - - - Enabled languages not listed here are unsupported by Google Translate. - - - - ); -}; - -const TranslationModal = withModal(TranslationTool, "translate-tool"); - -const CanSelectSubtitle = (item: TableColumnType) => { - return item.path.endsWith(".srt"); -}; - -const STM: FunctionComponent = () => { - const payload = usePayload(); - const [selections, setSelections] = useState([]); - - const Modal = useModal({ size: "xl" }); - const { hide } = useModalControl(); - - const { mutateAsync } = useSubtitleAction(); - - const process = useCallback( - (action: string, override?: Partial) => { - 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[] = useMemo[]>( - () => [ - { - Header: "Language", - accessor: "_language", - Cell: ({ value }) => ( - - - - ), - }, - { - 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( - () => - 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 = ( - k && process(k)}> - process("sync")} - > - Sync - - - - - Remove HI Tags - - - Remove Style Tags - - - OCR Fixes - - - Common Fixes - - - Fix Uppercase - - - Reverse RTL - - show(ColorToolModal)}> - Add Color - - show(FrameRateModal)}> - Change Frame Rate - - show(TimeAdjustmentModal)}> - Adjust Times - - show(TranslationModal)}> - Translate - - - - ); - - return ( - - - - - - - - ); -}; - -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>(null); + + const Modal = useModal(); + + const process = useProcess(); + + const submit = useCallback(() => { + if (selection) { + const action = submodProcessColor(selection); + process(action); + } + }, [process, selection]); + + const footer = ( + + ); + + return ( + + + + ); +}; + +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>(null); + const [to, setTo] = useState>(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 = ( + + ); + + return ( + + + { + const value = parseFloat(e.currentTarget.value); + if (isNaN(value)) { + setFrom(null); + } else { + setFrom(value); + } + }} + > + { + const value = parseFloat(e.currentTarget.value); + if (isNaN(value)) { + setTo(null); + } else { + setTo(value); + } + }} + > + + + ); +}; + +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 => { + 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 = ( + + ); + + return ( + + + + + + + + + + + + ); +}; + +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 +) => void; + +export const ProcessSubtitleContext = createContext(() => { + 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>(null); + + const process = useProcess(); + + const submit = useCallback(() => { + if (selectedLanguage) { + process("translate", { language: selectedLanguage.code2 }); + } + }, [process, selectedLanguage]); + + const footer = ( + + ); + return ( + + + Enabled languages not listed here are unsupported by Google Translate. + + + + ); +}; + +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 = ({ + tools, + count, + select, +}) => { + const payload = usePayload(); + + const Modal = useModal({ + size: "lg", + }); + const { show } = useModalControl(); + + const columns: Column[] = useMemo[]>( + () => [ + { + Header: "Language", + accessor: "raw_language", + Cell: ({ value }) => ( + + + + ), + }, + { + 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( + () => + 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 ( + k && process(k)}> + process(action.key)} + > + {action.name} + + + + {others.map((v) => ( + { + if (v.modal) { + show(v.modal); + } + }} + > + {v.name} + + ))} + + + ); + }, [count, process, show, tools]); + + return ( + + + + ); +}; + +export const SubtitleToolModal = withModal(SubtitleToolView, "subtitle-tools"); + +const SubtitleTools: FunctionComponent = () => { + const modals = useMemo( + () => + tools + .map((t) => t.modal && ) + .filter(isElement), + [] + ); + + const { hide } = useModalControl(); + const [selections, setSelections] = useState([]); + const { mutateAsync } = useSubtitleAction(); + + const process = useCallback( + (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 ( + + + {modals} + + ); +}; + +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; +} 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 = ({ }} emptyText="No Episode Found For This Series" > - + {
- +