Improve subtitle tools

pull/1783/head
LASER-Yi 2 years ago
parent 658237dd50
commit e18657e426
No known key found for this signature in database
GPG Key ID: BB28903D50A1D408

@ -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");

@ -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";

@ -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");

@ -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");

@ -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");

@ -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);
}

@ -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");

@ -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;

@ -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,
},
];

@ -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>;
}

@ -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";

@ -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}

@ -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

Loading…
Cancel
Save