import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons"; import { faCheck, faCircleNotch, 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 React, { 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, LanguageText, Selector, SimpleTable, usePayload, useShowModal, } from ".."; import { useEnabledLanguages } from "../../@redux/hooks"; import { SubtitlesApi } from "../../apis"; import { isMovie, submodProcessColor } from "../../utilites"; import { log } from "../../utilites/logger"; import { useCustomSelection } from "../tables/plugins"; import BaseModal, { BaseModalProps } from "./BaseModal"; import { useCloseModalUntil } from "./provider"; import { availableTranslation, colorOptions } from "./toolOptions"; type SupportType = Item.Episode | Item.Movie; type TableColumnType = FormType.ModifySubtitle & { _language: Language.Info; }; enum State { Pending, Processing, Done, } type ProcessState = StrictObject; // TODO: Extract this interface StateIconProps { state: State; } const StateIcon: FunctionComponent = ({ state }) => { let icon = faQuestionCircle; switch (state) { case State.Pending: icon = faClock; break; case State.Processing: icon = faCircleNotch; break; case State.Done: icon = faCheck; break; } return ( ); }; 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 AddColorModal: FunctionComponent = ( props ) => { const { process, ...modal } = props; const [selection, setSelection] = useState>(null); const submit = useCallback(() => { if (selection) { const action = submodProcessColor(selection); process(action); } }, [selection, process]); const footer = useMemo( () => ( ), [selection, submit] ); return ( ); }; const FrameRateModal: FunctionComponent = ( props ) => { const { process, ...modal } = props; const [from, setFrom] = useState>(null); const [to, setTo] = useState>(null); const canSave = from !== null && to !== null && from !== to; const submit = useCallback(() => { if (canSave) { const action = submodProcessFrameRate(from!, to!); process(action); } }, [canSave, from, to, process]); const footer = useMemo( () => ( ), [submit, canSave] ); 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 AdjustTimesModal: FunctionComponent = ( props ) => { const { process, ...modal } = props; const [isPlus, setPlus] = useState(true); const [offset, setOffset] = useState<[number, number, number, number]>([ 0, 0, 0, 0, ]); const updateOffset = useCallback( (idx: number) => { return (e: any) => { 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 = useMemo( () => ( ), [submit, canSave] ); return ( ); }; const TranslateModal: FunctionComponent = ({ process, ...modal }) => { const languages = useEnabledLanguages(); const available = useMemo( () => languages.filter((v) => v.code2 in availableTranslation), [languages] ); const [selectedLanguage, setLanguage] = useState>(null); const submit = useCallback(() => { if (selectedLanguage) { process("translate", { language: selectedLanguage.code2 }); } }, [selectedLanguage, process]); const footer = useMemo( () => ( ), [submit, selectedLanguage] ); return ( Enabled languages not listed here are unsupported by Google Translate. ); }; interface STMProps {} const STM: FunctionComponent = ({ ...props }) => { const items = usePayload(props.modalKey); const [updating, setUpdate] = useState(false); const [processState, setProcessState] = useState({}); const [selections, setSelections] = useState([]); const closeUntil = useCloseModalUntil(props.modalKey); const process = useCallback( async (action: string, override?: Partial) => { log("info", "executing action", action); closeUntil(); setUpdate(true); let states = selections.reduce( (v, curr) => ({ [curr.path]: State.Pending, ...v }), {} ); setProcessState(states); for (const raw of selections) { states = { ...states, [raw.path]: State.Processing, }; setProcessState(states); const form: FormType.ModifySubtitle = { id: raw.id, type: raw.type, language: raw.language, path: raw.path, ...override, }; await SubtitlesApi.modify(action, form); states = { ...states, [raw.path]: State.Done, }; setProcessState(states); } setUpdate(false); }, [closeUntil, selections] ); const showModal = useShowModal(); const columns: Column[] = useMemo[]>( () => [ { id: "state", accessor: "path", selectHide: true, Cell: ({ value, loose }) => { if (loose) { const stateList = loose[0] as ProcessState; if (value in stateList) { const state = stateList[value]; return ; } } return null; }, }, { Header: "Language", accessor: "_language", Cell: ({ value }) => ( ), }, { id: "file", Header: "File", accessor: "path", Cell: (row) => { const path = row.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( () => items?.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 []; } }); }) ?? [], [items] ); const plugins = [useRowSelect, useCustomSelection]; const footer = useMemo( () => ( k && process(k)}> process("sync")} > Sync Remove HI Tags Remove Style Tags OCR Fixes Common Fixes Fix Uppercase Reverse RTL showModal("add-color")}> Add Color showModal("change-frame-rate")}> Change Frame Rate showModal("adjust-times")}> Adjust Times showModal("translate-sub")}> Translate ), [showModal, updating, selections.length, process] ); return ( ); }; export default STM;