From 87c5d0d9defdc3f01865eeb844dfe191934411fb Mon Sep 17 00:00:00 2001 From: LASER-Yi Date: Sat, 26 Mar 2022 22:24:36 +0800 Subject: [PATCH 1/4] Fix notifications settings cannot be edited after saved --- .../src/pages/Settings/Notifications/components.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/Settings/Notifications/components.tsx b/frontend/src/pages/Settings/Notifications/components.tsx index 5b8ed2007..f53ab7132 100644 --- a/frontend/src/pages/Settings/Notifications/components.tsx +++ b/frontend/src/pages/Settings/Notifications/components.tsx @@ -8,7 +8,13 @@ import { } from "@/components"; import { useModalControl, usePayload } from "@/modules/redux/hooks/modal"; import { BuildKey } from "@/utilities"; -import { FunctionComponent, useCallback, useMemo, useState } from "react"; +import { + FunctionComponent, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; import { Button, Col, Container, Form, Row } from "react-bootstrap"; import { ColCard, useLatestArray, useUpdateArray } from "../components"; import { notificationsKey } from "../keys"; @@ -43,6 +49,10 @@ const NotificationModal: FunctionComponent = ({ const [current, setCurrent] = useState>(payload); + useEffect(() => { + setCurrent(payload); + }, [payload]); + const updateUrl = useCallback((url: string) => { setCurrent((current) => { if (current) { From 658237dd5076a3d4823552ad17c101d3ba6177fc Mon Sep 17 00:00:00 2001 From: LASER-Yi Date: Sun, 27 Mar 2022 14:42:28 +0800 Subject: [PATCH 2/4] Refactor modal system --- frontend/src/components/index.tsx | 1 - frontend/src/components/modals/BaseModal.tsx | 51 ---- .../src/components/modals/HistoryModal.tsx | 32 ++- .../src/components/modals/ItemEditorModal.tsx | 44 ++- .../components/modals/ManualSearchModal.tsx | 69 +++-- .../components/modals/MovieUploadModal.tsx | 18 +- .../components/modals/SeriesUploadModal.tsx | 19 +- .../components/modals/SubtitleToolModal.tsx | 251 ++++++++---------- .../components/modals/SubtitleUploadModal.tsx | 31 +-- frontend/src/components/modals/index.ts | 1 - frontend/src/modules/modals/ModalContext.ts | 14 + frontend/src/modules/modals/ModalWrapper.tsx | 44 +++ frontend/src/modules/modals/WithModal.tsx | 52 ++++ frontend/src/modules/modals/components.tsx | 23 ++ frontend/src/modules/modals/hooks.ts | 90 +++++++ frontend/src/modules/modals/index.ts | 3 + frontend/src/modules/redux/hooks/modal.ts | 36 --- frontend/src/pages/Episodes/index.tsx | 23 +- frontend/src/pages/Episodes/table.tsx | 28 +- frontend/src/pages/Movies/Details/index.tsx | 34 ++- frontend/src/pages/Movies/index.tsx | 9 +- frontend/src/pages/Series/index.tsx | 9 +- .../src/pages/Settings/Languages/modal.tsx | 61 ++--- .../src/pages/Settings/Languages/table.tsx | 8 +- .../Settings/Notifications/components.tsx | 145 +++++----- .../pages/Settings/Providers/components.tsx | 52 ++-- .../src/pages/Settings/Providers/index.tsx | 3 +- .../System/Backups/BackupDeleteModal.tsx | 30 ++- .../System/Backups/BackupRestoreModal.tsx | 36 +-- frontend/src/pages/System/Backups/table.tsx | 20 +- frontend/src/pages/System/Logs/modal.tsx | 17 +- frontend/src/pages/System/Logs/table.tsx | 6 +- 32 files changed, 674 insertions(+), 586 deletions(-) delete mode 100644 frontend/src/components/modals/BaseModal.tsx create mode 100644 frontend/src/modules/modals/ModalContext.ts create mode 100644 frontend/src/modules/modals/ModalWrapper.tsx create mode 100644 frontend/src/modules/modals/WithModal.tsx create mode 100644 frontend/src/modules/modals/components.tsx create mode 100644 frontend/src/modules/modals/hooks.ts create mode 100644 frontend/src/modules/modals/index.ts delete mode 100644 frontend/src/modules/redux/hooks/modal.ts diff --git a/frontend/src/components/index.tsx b/frontend/src/components/index.tsx index acb6b1fa1..054498626 100644 --- a/frontend/src/components/index.tsx +++ b/frontend/src/components/index.tsx @@ -131,6 +131,5 @@ export * from "./buttons"; export * from "./header"; export * from "./inputs"; export * from "./LanguageSelector"; -export * from "./modals"; export * from "./SearchBar"; export * from "./tables"; diff --git a/frontend/src/components/modals/BaseModal.tsx b/frontend/src/components/modals/BaseModal.tsx deleted file mode 100644 index f9488ef6b..000000000 --- a/frontend/src/components/modals/BaseModal.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useIsShowed, useModalControl } from "@/modules/redux/hooks/modal"; -import clsx from "clsx"; -import { FunctionComponent, useCallback, useState } from "react"; -import { Modal } from "react-bootstrap"; - -export interface BaseModalProps { - modalKey: string; - size?: "sm" | "lg" | "xl"; - closeable?: boolean; - title?: string; - footer?: JSX.Element; -} - -export const BaseModal: FunctionComponent = (props) => { - const { size, modalKey, title, children, footer, closeable = true } = props; - const [needExit, setExit] = useState(false); - - const { hide: hideModal } = useModalControl(); - const showIndex = useIsShowed(modalKey); - const isShowed = showIndex !== -1; - - const hide = useCallback(() => { - setExit(true); - }, []); - - const exit = useCallback(() => { - if (isShowed) { - hideModal(modalKey); - } - setExit(false); - }, [isShowed, hideModal, modalKey]); - - return ( - - {title} - {children} - - - ); -}; - -export default BaseModal; diff --git a/frontend/src/components/modals/HistoryModal.tsx b/frontend/src/components/modals/HistoryModal.tsx index dd4adf2bd..58e47cd90 100644 --- a/frontend/src/components/modals/HistoryModal.tsx +++ b/frontend/src/components/modals/HistoryModal.tsx @@ -4,18 +4,17 @@ import { useMovieAddBlacklist, useMovieHistory, } from "@/apis/hooks"; -import { usePayload } from "@/modules/redux/hooks/modal"; +import { useModal, usePayload, withModal } from "@/modules/modals"; import { FunctionComponent, useMemo } from "react"; import { Column } from "react-table"; import { HistoryIcon, PageTable, QueryOverlay, TextPopover } from ".."; import Language from "../bazarr/Language"; import { BlacklistButton } from "../inputs/blacklist"; -import BaseModal, { BaseModalProps } from "./BaseModal"; -export const MovieHistoryModal: FunctionComponent = (props) => { - const { ...modal } = props; +const MovieHistoryView: FunctionComponent = () => { + const movie = usePayload(); - const movie = usePayload(modal.modalKey); + const Modal = useModal({ size: "lg" }); const history = useMovieHistory(movie?.radarrId); @@ -84,7 +83,7 @@ export const MovieHistoryModal: FunctionComponent = (props) => { ); return ( - + = (props) => { data={data ?? []} > - + ); }; -export const EpisodeHistoryModal: FunctionComponent = ( - props -) => { - const episode = usePayload(props.modalKey); +export const MovieHistoryModal = withModal(MovieHistoryView, "movie-history"); + +const EpisodeHistoryView: FunctionComponent = () => { + const episode = usePayload(); + + const Modal = useModal({ size: "lg" }); const history = useEpisodeHistory(episode?.sonarrEpisodeId); @@ -175,7 +176,7 @@ export const EpisodeHistoryModal: FunctionComponent = ( ); return ( - + = ( data={data ?? []} > - + ); }; + +export const EpisodeHistoryModal = withModal( + EpisodeHistoryView, + "episode-history" +); diff --git a/frontend/src/components/modals/ItemEditorModal.tsx b/frontend/src/components/modals/ItemEditorModal.tsx index ad714598d..ffe44feb5 100644 --- a/frontend/src/components/modals/ItemEditorModal.tsx +++ b/frontend/src/components/modals/ItemEditorModal.tsx @@ -1,26 +1,28 @@ import { useIsAnyActionRunning, useLanguageProfiles } from "@/apis/hooks"; -import { useModalControl, usePayload } from "@/modules/redux/hooks/modal"; +import { + useModal, + useModalControl, + usePayload, + withModal, +} from "@/modules/modals"; import { GetItemId } from "@/utilities"; -import { FunctionComponent, useEffect, useMemo, useState } from "react"; +import { FunctionComponent, useMemo, useState } from "react"; import { Container, Form } from "react-bootstrap"; import { UseMutationResult } from "react-query"; import { AsyncButton, Selector, SelectorOption } from ".."; -import BaseModal, { BaseModalProps } from "./BaseModal"; interface Props { mutation: UseMutationResult; } -const Editor: FunctionComponent = (props) => { - const { mutation, ...modal } = props; - +const Editor: FunctionComponent = ({ mutation }) => { const { data: profiles } = useLanguageProfiles(); - const payload = usePayload(modal.modalKey); - const { hide } = useModalControl(); - + const payload = usePayload(); const { mutateAsync, isLoading } = mutation; + const { hide } = useModalControl(); + const hasTask = useIsAnyActionRunning(); const profileOptions = useMemo[]>( @@ -33,9 +35,12 @@ const Editor: FunctionComponent = (props) => { const [id, setId] = useState>(payload?.profileId ?? null); - useEffect(() => { - setId(payload?.profileId ?? null); - }, [payload]); + const Modal = useModal({ + closeable: !isLoading, + onMounted: () => { + setId(payload?.profileId ?? null); + }, + }); const footer = ( = (props) => { return null; } }} - onSuccess={() => { - hide(); - }} + onSuccess={() => hide()} > Save ); return ( - +
@@ -95,8 +93,8 @@ const Editor: FunctionComponent = (props) => {
-
+ ); }; -export default Editor; +export default withModal(Editor, "edit"); diff --git a/frontend/src/components/modals/ManualSearchModal.tsx b/frontend/src/components/modals/ManualSearchModal.tsx index 5f3e1a6f0..17e5a786b 100644 --- a/frontend/src/components/modals/ManualSearchModal.tsx +++ b/frontend/src/components/modals/ManualSearchModal.tsx @@ -1,4 +1,4 @@ -import { usePayload } from "@/modules/redux/hooks/modal"; +import { useModal, usePayload, withModal } from "@/modules/modals"; import { createAndDispatchTask } from "@/modules/task/utilities"; import { GetItemId, isMovie } from "@/utilities"; import { @@ -10,13 +10,7 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import clsx from "clsx"; -import { - FunctionComponent, - useCallback, - useEffect, - useMemo, - useState, -} from "react"; +import { FunctionComponent, useCallback, useMemo, useState } from "react"; import { Badge, Button, @@ -29,7 +23,7 @@ import { } from "react-bootstrap"; import { UseQueryResult } from "react-query"; import { Column } from "react-table"; -import { BaseModal, BaseModalProps, LoadingIndicator, PageTable } from ".."; +import { LoadingIndicator, PageTable } from ".."; import Language from "../bazarr/Language"; type SupportType = Item.Movie | Item.Episode; @@ -41,24 +35,15 @@ interface Props { ) => UseQueryResult; } -export function ManualSearchModal( - props: Props & BaseModalProps -) { - const { download, query: useSearch, ...modal } = props; +function ManualSearchView(props: Props) { + const { download, query: useSearch } = props; - const item = usePayload(modal.modalKey); + const item = usePayload(); const itemId = useMemo(() => GetItemId(item ?? {}), [item]); const [id, setId] = useState(undefined); - // Cleanup the ID when user switches episode / movie - useEffect(() => { - if (itemId !== undefined && itemId !== id) { - setId(undefined); - } - }, [id, itemId]); - const results = useSearch(id); const isStale = results.data === undefined; @@ -225,12 +210,6 @@ export function ManualSearchModal( } }; - const footer = ( - - ); - const title = useMemo(() => { let title = "Unknown"; @@ -246,19 +225,39 @@ export function ManualSearchModal( return `Search - ${title}`; }, [item]); + const Modal = useModal({ + size: "xl", + closeable: results.isFetching === false, + onMounted: () => { + // Cleanup the ID when user switches episode / movie + if (itemId !== id) { + setId(undefined); + } + }, + }); + + const footer = ( + + ); + return ( - + {content()} - + ); } +export const MovieSearchModal = withModal>( + ManualSearchView, + "movie-manual-search" +); +export const EpisodeSearchModal = withModal>( + ManualSearchView, + "episode-manual-search" +); + const StateIcon: FunctionComponent<{ matches: string[]; dont: string[] }> = ({ matches, dont, diff --git a/frontend/src/components/modals/MovieUploadModal.tsx b/frontend/src/components/modals/MovieUploadModal.tsx index 3b3730668..464c055dc 100644 --- a/frontend/src/components/modals/MovieUploadModal.tsx +++ b/frontend/src/components/modals/MovieUploadModal.tsx @@ -1,21 +1,18 @@ import { useMovieSubtitleModification } from "@/apis/hooks"; -import { usePayload } from "@/modules/redux/hooks/modal"; +import { usePayload, withModal } from "@/modules/modals"; import { createTask, dispatchTask } from "@/modules/task/utilities"; import { useLanguageProfileBy, useProfileItemsToLanguages, } from "@/utilities/languages"; import { FunctionComponent, useCallback } from "react"; -import { BaseModalProps } from "./BaseModal"; -import SubtitleUploadModal, { +import SubtitleUploader, { PendingSubtitle, Validator, } from "./SubtitleUploadModal"; -const MovieUploadModal: FunctionComponent = (props) => { - const modal = props; - - const payload = usePayload(modal.modalKey); +const MovieUploadModal: FunctionComponent = () => { + const payload = usePayload(); const profile = useLanguageProfileBy(payload?.profileId); @@ -87,7 +84,7 @@ const MovieUploadModal: FunctionComponent = (props) => { ); return ( - = (props) => { upload={upload} update={update} validate={validate} - {...modal} - > + > ); }; -export default MovieUploadModal; +export default withModal(MovieUploadModal, "movie-upload"); diff --git a/frontend/src/components/modals/SeriesUploadModal.tsx b/frontend/src/components/modals/SeriesUploadModal.tsx index 23c3101f3..89a033e51 100644 --- a/frontend/src/components/modals/SeriesUploadModal.tsx +++ b/frontend/src/components/modals/SeriesUploadModal.tsx @@ -1,6 +1,6 @@ import { useEpisodeSubtitleModification } from "@/apis/hooks"; import api from "@/apis/raw"; -import { usePayload } from "@/modules/redux/hooks/modal"; +import { usePayload, withModal } from "@/modules/modals"; import { createTask, dispatchTask } from "@/modules/task/utilities"; import { useLanguageProfileBy, @@ -9,8 +9,7 @@ import { import { FunctionComponent, useCallback, useMemo } from "react"; import { Column } from "react-table"; import { Selector, SelectorOption } from "../inputs"; -import { BaseModalProps } from "./BaseModal"; -import SubtitleUploadModal, { +import SubtitleUploader, { PendingSubtitle, useRowMutation, Validator, @@ -24,11 +23,8 @@ interface SeriesProps { episodes: readonly Item.Episode[]; } -const SeriesUploadModal: FunctionComponent = ({ - episodes, - ...modal -}) => { - const payload = usePayload(modal.modalKey); +const SeriesUploadModal: FunctionComponent = ({ episodes }) => { + const payload = usePayload(); const profile = useLanguageProfileBy(payload?.profileId); @@ -165,16 +161,15 @@ const SeriesUploadModal: FunctionComponent = ({ ); return ( - + > ); }; -export default SeriesUploadModal; +export default withModal(SeriesUploadModal, "series-upload"); diff --git a/frontend/src/components/modals/SubtitleToolModal.tsx b/frontend/src/components/modals/SubtitleToolModal.tsx index b15444879..823aca5a7 100644 --- a/frontend/src/components/modals/SubtitleToolModal.tsx +++ b/frontend/src/components/modals/SubtitleToolModal.tsx @@ -1,5 +1,10 @@ import { useSubtitleAction } from "@/apis/hooks"; -import { useModalControl, usePayload } from "@/modules/redux/hooks/modal"; +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"; @@ -45,7 +50,6 @@ import { } from ".."; import Language from "../bazarr/Language"; import { useCustomSelection } from "../tables/plugins"; -import BaseModal, { BaseModalProps } from "./BaseModal"; import { availableTranslation, colorOptions } from "./toolOptions"; type SupportType = Item.Episode | Item.Movie; @@ -77,12 +81,11 @@ interface ToolModalProps { ) => void; } -const AddColorModal: FunctionComponent = ( - props -) => { - const { process, ...modal } = props; +const ColorTool: FunctionComponent = ({ process }) => { const [selection, setSelection] = useState>(null); + const Modal = useModal(); + const submit = useCallback(() => { if (selection) { const action = submodProcessColor(selection); @@ -90,31 +93,29 @@ const AddColorModal: FunctionComponent = ( } }, [selection, process]); - const footer = useMemo( - () => ( - - ), - [selection, submit] + const footer = ( + ); + return ( - + - + ); }; -const FrameRateModal: FunctionComponent = ( - props -) => { - const { process, ...modal } = props; +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); @@ -129,7 +130,7 @@ const FrameRateModal: FunctionComponent = ( ); return ( - + = ( }} > - + ); }; -const AdjustTimesModal: FunctionComponent = ( - props -) => { - const { process, ...modal } = props; +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) => { @@ -200,17 +201,14 @@ const AdjustTimesModal: FunctionComponent = ( } }, [process, canSave, offset, isPlus]); - const footer = useMemo( - () => ( - - ), - [submit, canSave] + const footer = ( + ); return ( - + - ), - [submit, selectedLanguage] + const footer = ( + ); - return ( - + Enabled languages not listed here are unsupported by Google Translate. @@ -284,18 +279,21 @@ const TranslateModal: FunctionComponent = ({ options={available} onChange={setLanguage} > - + ); }; +const TranslationModal = withModal(TranslationTool, "translate-tool"); + const CanSelectSubtitle = (item: TableColumnType) => { return item.path.endsWith(".srt"); }; -const STM: FunctionComponent = ({ ...props }) => { - const payload = usePayload(props.modalKey); +const STM: FunctionComponent = () => { + const payload = usePayload(); const [selections, setSelections] = useState([]); + const Modal = useModal({ size: "xl" }); const { hide } = useModalControl(); const { mutateAsync } = useSubtitleAction(); @@ -303,8 +301,7 @@ const STM: FunctionComponent = ({ ...props }) => { const process = useCallback( (action: string, override?: Partial) => { LOG("info", "executing action", action); - hide(props.modalKey); - + hide(); const tasks = selections.map((s) => { const form: FormType.ModifySubtitle = { id: s.id, @@ -318,7 +315,7 @@ const STM: FunctionComponent = ({ ...props }) => { dispatchTask(tasks, "modify-subtitles"); }, - [hide, props.modalKey, selections, mutateAsync] + [hide, selections, mutateAsync] ); const { show } = useModalControl(); @@ -383,92 +380,74 @@ const STM: FunctionComponent = ({ ...props }) => { 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 - - - show("add-color")}> - Add Color - - show("change-frame-rate")}> - Change Frame Rate - - show("adjust-times")}> - Adjust Times - - show("translate-sub")}> - Translate - - - - ), - [selections.length, process, show] + 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 STM; +export default withModal(STM, "subtitle-tools"); diff --git a/frontend/src/components/modals/SubtitleUploadModal.tsx b/frontend/src/components/modals/SubtitleUploadModal.tsx index 76693abbc..c14f0b23e 100644 --- a/frontend/src/components/modals/SubtitleUploadModal.tsx +++ b/frontend/src/components/modals/SubtitleUploadModal.tsx @@ -1,4 +1,4 @@ -import { useModalControl } from "@/modules/redux/hooks/modal"; +import { useModal, useModalControl } from "@/modules/modals"; import { BuildKey } from "@/utilities"; import { LOG } from "@/utilities/console"; import { @@ -23,7 +23,6 @@ import { Column } from "react-table"; import { LanguageSelector, MessageIcon } from ".."; import { FileForm } from "../inputs"; import { SimpleTable } from "../tables"; -import BaseModal, { BaseModalProps } from "./BaseModal"; type ModifyFn = (index: number, info?: PendingSubtitle) => void; @@ -59,10 +58,7 @@ interface Props { hideAllLanguages?: boolean; } -type ComponentProps = Props & - Omit; - -function SubtitleUploadModal(props: ComponentProps) { +function SubtitleUploader(props: Props) { const { initial, columns, @@ -73,10 +69,16 @@ function SubtitleUploadModal(props: ComponentProps) { hideAllLanguages, } = props; - const { hide } = useModalControl(); - const [pending, setPending] = useState[]>([]); + const showTable = pending.length > 0; + + const Modal = useModal({ + size: showTable ? "xl" : "lg", + }); + + const { hide } = useModalControl(); + const fileList = useMemo(() => pending.map((v) => v.file), [pending]); const initialRef = useRef(initial); @@ -281,8 +283,6 @@ function SubtitleUploadModal(props: ComponentProps) { [columns, availableLanguages] ); - const showTable = pending.length > 0; - const canUpload = useMemo( () => pending.length > 0 && @@ -332,12 +332,7 @@ function SubtitleUploadModal(props: ComponentProps) { ); return ( - +
@@ -360,8 +355,8 @@ function SubtitleUploadModal(props: ComponentProps) { - + ); } -export default SubtitleUploadModal; +export default SubtitleUploader; diff --git a/frontend/src/components/modals/index.ts b/frontend/src/components/modals/index.ts index 5f02dc94e..f52d9228d 100644 --- a/frontend/src/components/modals/index.ts +++ b/frontend/src/components/modals/index.ts @@ -1,4 +1,3 @@ -export * from "./BaseModal"; export * from "./HistoryModal"; export { default as ItemEditorModal } from "./ItemEditorModal"; export { default as MovieUploadModal } from "./MovieUploadModal"; diff --git a/frontend/src/modules/modals/ModalContext.ts b/frontend/src/modules/modals/ModalContext.ts new file mode 100644 index 000000000..81e7fba86 --- /dev/null +++ b/frontend/src/modules/modals/ModalContext.ts @@ -0,0 +1,14 @@ +import { createContext, Dispatch, SetStateAction } from "react"; + +export interface ModalData { + key: string; + closeable: boolean; + size: "sm" | "lg" | "xl" | undefined; +} + +export type ModalSetter = { + [P in keyof Omit]: Dispatch>; +}; + +export const ModalDataContext = createContext(null); +export const ModalSetterContext = createContext(null); diff --git a/frontend/src/modules/modals/ModalWrapper.tsx b/frontend/src/modules/modals/ModalWrapper.tsx new file mode 100644 index 000000000..aeb176604 --- /dev/null +++ b/frontend/src/modules/modals/ModalWrapper.tsx @@ -0,0 +1,44 @@ +import clsx from "clsx"; +import { FunctionComponent, useCallback, useState } from "react"; +import { Modal } from "react-bootstrap"; +import { useCurrentLayer, useModalControl, useModalData } from "./hooks"; + +interface Props {} + +export const ModalWrapper: FunctionComponent = ({ children }) => { + const { size, closeable, key } = useModalData(); + const [needExit, setExit] = useState(false); + + const { hide: hideModal } = useModalControl(); + + const layer = useCurrentLayer(); + const isShowed = layer !== -1; + + const hide = useCallback(() => { + setExit(true); + }, []); + + const exit = useCallback(() => { + if (isShowed) { + hideModal(key); + } + setExit(false); + }, [isShowed, hideModal, key]); + + return ( + + {children} + + ); +}; + +export default ModalWrapper; diff --git a/frontend/src/modules/modals/WithModal.tsx b/frontend/src/modules/modals/WithModal.tsx new file mode 100644 index 000000000..0d09e14e2 --- /dev/null +++ b/frontend/src/modules/modals/WithModal.tsx @@ -0,0 +1,52 @@ +import { FunctionComponent, useMemo, useState } from "react"; +import { + ModalData, + ModalDataContext, + ModalSetter, + ModalSetterContext, +} from "./ModalContext"; +import ModalWrapper from "./ModalWrapper"; + +export interface ModalProps {} + +export type ModalComponent

= FunctionComponent

& { + modalKey: string; +}; + +export default function withModal( + Content: FunctionComponent, + key: string +) { + const Comp: ModalComponent = (props: ModalProps & T) => { + const [closeable, setCloseable] = useState(true); + const [size, setSize] = useState(undefined); + const data: ModalData = useMemo( + () => ({ + key, + size, + closeable, + }), + [closeable, size] + ); + + const setter: ModalSetter = useMemo( + () => ({ + closeable: setCloseable, + size: setSize, + }), + [] + ); + + return ( + + + + + + + + ); + }; + Comp.modalKey = key; + return Comp; +} diff --git a/frontend/src/modules/modals/components.tsx b/frontend/src/modules/modals/components.tsx new file mode 100644 index 000000000..171f19d3c --- /dev/null +++ b/frontend/src/modules/modals/components.tsx @@ -0,0 +1,23 @@ +import { FunctionComponent, ReactNode } from "react"; +import { Modal } from "react-bootstrap"; +import { useModalData } from "./hooks"; + +interface StandardModalProps { + title: string; + footer?: ReactNode; +} + +export const StandardModalView: FunctionComponent = ({ + children, + footer, + title, +}) => { + const { closeable } = useModalData(); + return ( + <> + {title} + {children} + + + ); +}; diff --git a/frontend/src/modules/modals/hooks.ts b/frontend/src/modules/modals/hooks.ts new file mode 100644 index 000000000..03ffe0d9c --- /dev/null +++ b/frontend/src/modules/modals/hooks.ts @@ -0,0 +1,90 @@ +import { + hideModalAction, + showModalAction, +} from "@/modules/redux/actions/modal"; +import { useReduxAction, useReduxStore } from "@/modules/redux/hooks/base"; +import { useCallback, useContext, useEffect, useMemo, useRef } from "react"; +import { StandardModalView } from "./components"; +import { + ModalData, + ModalDataContext, + ModalSetterContext, +} from "./ModalContext"; +import { ModalComponent } from "./WithModal"; + +type ModalProps = Partial> & { + onMounted?: () => void; +}; + +export function useModal(props?: ModalProps): typeof StandardModalView { + const setter = useContext(ModalSetterContext); + + useEffect(() => { + if (setter && props) { + setter.closeable(props.closeable ?? true); + setter.size(props.size); + } + }, [props, setter]); + + const ref = useRef(props?.onMounted); + ref.current = props?.onMounted; + + const layer = useCurrentLayer(); + + useEffect(() => { + if (layer !== -1 && ref.current) { + ref.current(); + } + }, [layer]); + + return StandardModalView; +} + +export function useModalControl() { + const showAction = useReduxAction(showModalAction); + + const show = useCallback( +

(comp: ModalComponent

, payload?: unknown) => { + showAction({ key: comp.modalKey, payload }); + }, + [showAction] + ); + + const hideAction = useReduxAction(hideModalAction); + + const hide = useCallback( + (key?: string) => { + hideAction(key); + }, + [hideAction] + ); + + return { show, hide }; +} + +export function useModalData(): ModalData { + const data = useContext(ModalDataContext); + + if (data === null) { + throw new Error("useModalData should be used inside Modal"); + } + + return data; +} + +export function usePayload(): T | null { + const { key } = useModalData(); + const stack = useReduxStore((s) => s.modal.stack); + + return useMemo( + () => (stack.find((m) => m.key === key)?.payload as T) ?? null, + [stack, key] + ); +} + +export function useCurrentLayer() { + const { key } = useModalData(); + const stack = useReduxStore((s) => s.modal.stack); + + return useMemo(() => stack.findIndex((m) => m.key === key), [stack, key]); +} diff --git a/frontend/src/modules/modals/index.ts b/frontend/src/modules/modals/index.ts new file mode 100644 index 000000000..baaee48b7 --- /dev/null +++ b/frontend/src/modules/modals/index.ts @@ -0,0 +1,3 @@ +export * from "./components"; +export * from "./hooks"; +export { default as withModal } from "./WithModal"; diff --git a/frontend/src/modules/redux/hooks/modal.ts b/frontend/src/modules/redux/hooks/modal.ts deleted file mode 100644 index ea8db3659..000000000 --- a/frontend/src/modules/redux/hooks/modal.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { - hideModalAction, - showModalAction, -} from "@/modules/redux/actions/modal"; -import { useReduxAction, useReduxStore } from "@/modules/redux/hooks/base"; -import { useCallback, useMemo } from "react"; - -export function useModalControl() { - const showModal = useReduxAction(showModalAction); - - const show = useCallback( - (key: string, payload?: unknown) => { - showModal({ key, payload }); - }, - [showModal] - ); - - const hide = useReduxAction(hideModalAction); - - return { show, hide }; -} - -export function useIsShowed(key: string) { - const stack = useReduxStore((s) => s.modal.stack); - - return useMemo(() => stack.findIndex((m) => m.key === key), [stack, key]); -} - -export function usePayload(key: string): T | null { - const stack = useReduxStore((s) => s.modal.stack); - - return useMemo( - () => (stack.find((m) => m.key === key)?.payload as T) ?? null, - [stack, key] - ); -} diff --git a/frontend/src/pages/Episodes/index.tsx b/frontend/src/pages/Episodes/index.tsx index a4efa7c4f..9915a6293 100644 --- a/frontend/src/pages/Episodes/index.tsx +++ b/frontend/src/pages/Episodes/index.tsx @@ -5,14 +5,14 @@ import { useSeriesById, useSeriesModification, } from "@/apis/hooks"; +import { ContentHeader, LoadingIndicator } from "@/components"; +import ItemOverview from "@/components/ItemOverview"; import { - ContentHeader, ItemEditorModal, - LoadingIndicator, SeriesUploadModal, -} from "@/components"; -import ItemOverview from "@/components/ItemOverview"; -import { useModalControl } from "@/modules/redux/hooks/modal"; + SubtitleToolModal, +} from "@/components/modals"; +import { useModalControl } from "@/modules/modals"; import { createAndDispatchTask } from "@/modules/task/utilities"; import { useLanguageProfileBy } from "@/utilities/languages"; import { @@ -109,7 +109,7 @@ const SeriesEpisodesView: FunctionComponent = () => { show("tools", episodes)} + onClick={() => show(SubtitleToolModal, episodes)} > Tools @@ -120,14 +120,14 @@ const SeriesEpisodesView: FunctionComponent = () => { !available } icon={faCloudUploadAlt} - onClick={() => show("upload", series)} + onClick={() => show(SeriesUploadModal, series)} > Upload show("edit", series)} + onClick={() => show(ItemEditorModal, series)} > Edit Series @@ -158,11 +158,8 @@ const SeriesEpisodesView: FunctionComponent = () => { > )} - - + + ); }; diff --git a/frontend/src/pages/Episodes/table.tsx b/frontend/src/pages/Episodes/table.tsx index d519285af..4a9326201 100644 --- a/frontend/src/pages/Episodes/table.tsx +++ b/frontend/src/pages/Episodes/table.tsx @@ -1,14 +1,9 @@ import { useDownloadEpisodeSubtitles, useEpisodesProvider } from "@/apis/hooks"; -import { - ActionButton, - EpisodeHistoryModal, - GroupTable, - SubtitleToolModal, - TextPopover, -} from "@/components"; -import { ManualSearchModal } from "@/components/modals/ManualSearchModal"; +import { ActionButton, GroupTable, TextPopover } from "@/components"; +import { EpisodeHistoryModal, SubtitleToolModal } from "@/components/modals"; +import { EpisodeSearchModal } from "@/components/modals/ManualSearchModal"; +import { useModalControl } from "@/modules/modals"; import { useShowOnlyDesired } from "@/modules/redux/hooks"; -import { useModalControl } from "@/modules/redux/hooks/modal"; import { BuildKey, filterSubtitleBy } from "@/utilities"; import { useProfileItemsToLanguages } from "@/utilities/languages"; import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons"; @@ -166,21 +161,21 @@ const Table: FunctionComponent = ({ icon={faUser} disabled={series?.profileId === null || disabled} onClick={() => { - show("manual-search", row.original); + show(EpisodeSearchModal, row.original); }} > { - show("history", row.original); + show(EpisodeHistoryModal, row.original); }} > { - show("tools", [row.original]); + show(SubtitleToolModal, [row.original]); }} > @@ -214,13 +209,12 @@ const Table: FunctionComponent = ({ }} emptyText="No Episode Found For This Series" > - - - + + + > ); }; diff --git a/frontend/src/pages/Movies/Details/index.tsx b/frontend/src/pages/Movies/Details/index.tsx index d6731f4de..41efde6c5 100644 --- a/frontend/src/pages/Movies/Details/index.tsx +++ b/frontend/src/pages/Movies/Details/index.tsx @@ -8,17 +8,16 @@ import { useMovieById, useMovieModification, } from "@/apis/hooks/movies"; +import { ContentHeader, LoadingIndicator } from "@/components"; +import ItemOverview from "@/components/ItemOverview"; import { - ContentHeader, ItemEditorModal, - LoadingIndicator, MovieHistoryModal, MovieUploadModal, SubtitleToolModal, -} from "@/components"; -import ItemOverview from "@/components/ItemOverview"; -import { ManualSearchModal } from "@/components/modals/ManualSearchModal"; -import { useModalControl } from "@/modules/redux/hooks/modal"; +} from "@/components/modals"; +import { MovieSearchModal } from "@/components/modals/ManualSearchModal"; +import { useModalControl } from "@/modules/modals"; import { createAndDispatchTask } from "@/modules/task/utilities"; import { useLanguageProfileBy } from "@/utilities/languages"; import { @@ -122,20 +121,20 @@ const MovieDetailView: FunctionComponent = () => { show("manual-search", movie)} + onClick={() => show(MovieSearchModal, movie)} > Manual show("history", movie)} + onClick={() => show(MovieHistoryModal, movie)} > History show("tools", [movie])} + onClick={() => show(SubtitleToolModal, [movie])} > Tools @@ -145,14 +144,14 @@ const MovieDetailView: FunctionComponent = () => { show("upload", movie)} + onClick={() => show(MovieUploadModal, movie)} > Upload show("edit", movie)} + onClick={() => show(ItemEditorModal, movie)} > Edit Movie @@ -174,15 +173,14 @@ const MovieDetailView: FunctionComponent = () => {
- - - - - + + + + + > ); }; diff --git a/frontend/src/pages/Movies/index.tsx b/frontend/src/pages/Movies/index.tsx index 2939ee2a0..5daac19f6 100644 --- a/frontend/src/pages/Movies/index.tsx +++ b/frontend/src/pages/Movies/index.tsx @@ -1,9 +1,10 @@ import { useMovieModification, useMoviesPagination } from "@/apis/hooks"; -import { ActionBadge, ItemEditorModal, TextPopover } from "@/components"; +import { ActionBadge, TextPopover } from "@/components"; import Language from "@/components/bazarr/Language"; import LanguageProfile from "@/components/bazarr/LanguageProfile"; +import { ItemEditorModal } from "@/components/modals"; import ItemView from "@/components/views/ItemView"; -import { useModalControl } from "@/modules/redux/hooks/modal"; +import { useModalControl } from "@/modules/modals"; import { BuildKey } from "@/utilities"; import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons"; import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons"; @@ -90,7 +91,7 @@ const MovieView: FunctionComponent = () => { return ( show("edit", row.original)} + onClick={() => show(ItemEditorModal, row.original)} > ); }, @@ -105,7 +106,7 @@ const MovieView: FunctionComponent = () => { Movies - Bazarr - + ); }; diff --git a/frontend/src/pages/Series/index.tsx b/frontend/src/pages/Series/index.tsx index 5f96f1d75..e03807a7f 100644 --- a/frontend/src/pages/Series/index.tsx +++ b/frontend/src/pages/Series/index.tsx @@ -1,8 +1,9 @@ import { useSeriesModification, useSeriesPagination } from "@/apis/hooks"; -import { ActionBadge, ItemEditorModal } from "@/components"; +import { ActionBadge } from "@/components"; import LanguageProfile from "@/components/bazarr/LanguageProfile"; +import { ItemEditorModal } from "@/components/modals"; import ItemView from "@/components/views/ItemView"; -import { useModalControl } from "@/modules/redux/hooks/modal"; +import { useModalControl } from "@/modules/modals"; import { BuildKey } from "@/utilities"; import { faWrench } from "@fortawesome/free-solid-svg-icons"; import { FunctionComponent, useMemo } from "react"; @@ -92,7 +93,7 @@ const SeriesView: FunctionComponent = () => { return ( show("edit", original)} + onClick={() => show(ItemEditorModal, original)} > ); }, @@ -107,7 +108,7 @@ const SeriesView: FunctionComponent = () => { Series - Bazarr - + ); }; diff --git a/frontend/src/pages/Settings/Languages/modal.tsx b/frontend/src/pages/Settings/Languages/modal.tsx index 1c8413c83..066418a76 100644 --- a/frontend/src/pages/Settings/Languages/modal.tsx +++ b/frontend/src/pages/Settings/Languages/modal.tsx @@ -1,14 +1,17 @@ import { ActionButton, - BaseModal, - BaseModalProps, Chips, LanguageSelector, Selector, SelectorOption, SimpleTable, } from "@/components"; -import { useModalControl, usePayload } from "@/modules/redux/hooks/modal"; +import { + useModal, + useModalControl, + usePayload, + withModal, +} from "@/modules/modals"; import { BuildKey } from "@/utilities"; import { LOG } from "@/utilities/console"; import { faTrash } from "@fortawesome/free-solid-svg-icons"; @@ -17,7 +20,6 @@ import { FunctionComponent, useCallback, useContext, - useEffect, useMemo, useState, } from "react"; @@ -53,12 +55,8 @@ function createDefaultProfile(): Language.Profile { }; } -const LanguagesProfileModal: FunctionComponent = ( - props -) => { - const { update, ...modal } = props; - - const profile = usePayload(modal.modalKey); +const LanguagesProfileModal: FunctionComponent = ({ update }) => { + const profile = usePayload(); const { hide } = useModalControl(); @@ -66,13 +64,12 @@ const LanguagesProfileModal: FunctionComponent = ( const [current, setProfile] = useState(createDefaultProfile); - useEffect(() => { - if (profile) { - setProfile(profile); - } else { - setProfile(createDefaultProfile); - } - }, [profile]); + const Modal = useModal({ + size: "lg", + onMounted: () => { + setProfile(profile ?? createDefaultProfile); + }, + }); const cutoff: SelectorOption[] = useMemo(() => { const options = [...cutoffOptions]; @@ -134,18 +131,6 @@ const LanguagesProfileModal: FunctionComponent = ( const canSave = current.name.length > 0 && current.items.length > 0; - const footer = ( - - ); - const columns = useMemo[]>( () => [ { @@ -253,8 +238,20 @@ const LanguagesProfileModal: FunctionComponent = ( [languages] ); + const footer = ( + + ); + return ( - + = ( > Download subtitle file without format conversion - + ); }; -export default LanguagesProfileModal; +export default withModal(LanguagesProfileModal, "languages-profile-editor"); diff --git a/frontend/src/pages/Settings/Languages/table.tsx b/frontend/src/pages/Settings/Languages/table.tsx index ed87274da..bc2cd2c4e 100644 --- a/frontend/src/pages/Settings/Languages/table.tsx +++ b/frontend/src/pages/Settings/Languages/table.tsx @@ -1,5 +1,5 @@ import { ActionButton, SimpleTable } from "@/components"; -import { useModalControl } from "@/modules/redux/hooks/modal"; +import { useModalControl } from "@/modules/modals"; import { LOG } from "@/utilities/console"; import { faTrash, faWrench } from "@fortawesome/free-solid-svg-icons"; import { cloneDeep } from "lodash"; @@ -69,7 +69,7 @@ const Table: FunctionComponent = () => { const mutateRow = useCallback( (index, item) => { if (item) { - show("profile", cloneDeep(item)); + show(Modal, cloneDeep(item)); } else { const list = [...profiles]; list.splice(index, 1); @@ -185,12 +185,12 @@ const Table: FunctionComponent = () => { mustNotContain: [], originalFormat: false, }; - show("profile", profile); + show(Modal, profile); }} > {canAdd ? "Add New Profile" : "No Enabled Languages"} - + ); }; diff --git a/frontend/src/pages/Settings/Notifications/components.tsx b/frontend/src/pages/Settings/Notifications/components.tsx index f53ab7132..fff722c19 100644 --- a/frontend/src/pages/Settings/Notifications/components.tsx +++ b/frontend/src/pages/Settings/Notifications/components.tsx @@ -1,32 +1,22 @@ import api from "@/apis/raw"; +import { AsyncButton, Selector, SelectorOption } from "@/components"; import { - AsyncButton, - BaseModal, - BaseModalProps, - Selector, - SelectorOption, -} from "@/components"; -import { useModalControl, usePayload } from "@/modules/redux/hooks/modal"; + useModal, + useModalControl, + usePayload, + withModal, +} from "@/modules/modals"; import { BuildKey } from "@/utilities"; -import { - FunctionComponent, - useCallback, - useEffect, - useMemo, - useState, -} from "react"; +import { FunctionComponent, useCallback, useMemo, useState } from "react"; import { Button, Col, Container, Form, Row } from "react-bootstrap"; import { ColCard, useLatestArray, useUpdateArray } from "../components"; import { notificationsKey } from "../keys"; -interface ModalProps { +interface Props { selections: readonly Settings.NotificationInfo[]; } -const NotificationModal: FunctionComponent = ({ - selections, - ...modal -}) => { +const NotificationTool: FunctionComponent = ({ selections }) => { const options = useMemo[]>( () => selections @@ -43,16 +33,11 @@ const NotificationModal: FunctionComponent = ({ "name" ); - const payload = usePayload(modal.modalKey); - const { hide } = useModalControl(); + const payload = usePayload(); const [current, setCurrent] = useState>(payload); - useEffect(() => { - setCurrent(payload); - }, [payload]); - const updateUrl = useCallback((url: string) => { setCurrent((current) => { if (current) { @@ -69,55 +54,60 @@ const NotificationModal: FunctionComponent = ({ const canSave = current !== null && current?.url !== null && current?.url.length !== 0; - const footer = useMemo( - () => ( - <> - { - if (current && current.url) { - return api.system.testNotification(current.url); - } else { - return null; - } - }} - > - Test - - - - - ), - [canSave, payload, current, hide, update] - ); - const getLabel = useCallback((v: Settings.NotificationInfo) => v.name, []); + const Modal = useModal({ + onMounted: () => { + setCurrent(payload); + }, + }); + + const { hide } = useModalControl(); + + const footer = ( + <> + { + if (current && current.url) { + return api.system.testNotification(current.url); + } else { + return null; + } + }} + > + Test + + + + + ); + return ( - + @@ -145,10 +135,12 @@ const NotificationModal: FunctionComponent = ({ - + ); }; +const NotificationModal = withModal(NotificationTool, "notification-tool"); + export const NotificationView: FunctionComponent = () => { const notifications = useLatestArray( notificationsKey, @@ -165,7 +157,7 @@ export const NotificationView: FunctionComponent = () => { show("notifications", v)} + onClick={() => show(NotificationModal, v)} > )); }, [notifications, show]); @@ -174,12 +166,9 @@ export const NotificationView: FunctionComponent = () => { {elements}{" "} - show("notifications")}> + show(NotificationModal)}> - + ); }; diff --git a/frontend/src/pages/Settings/Providers/components.tsx b/frontend/src/pages/Settings/Providers/components.tsx index 4af98d95c..f01012ba3 100644 --- a/frontend/src/pages/Settings/Providers/components.tsx +++ b/frontend/src/pages/Settings/Providers/components.tsx @@ -1,10 +1,10 @@ +import { Selector, SelectorComponents, SelectorOption } from "@/components"; import { - BaseModal, - Selector, - SelectorComponents, - SelectorOption, -} from "@/components"; -import { useModalControl, usePayload } from "@/modules/redux/hooks/modal"; + useModal, + useModalControl, + usePayload, + withModal, +} from "@/modules/modals"; import { BuildKey, isReactText } from "@/utilities"; import { capitalize, isArray, isBoolean } from "lodash"; import { @@ -27,7 +27,6 @@ import { } from "../components"; import { ProviderInfo, ProviderList } from "./list"; -const ModalKey = "provider-modal"; const ProviderKey = "settings-general-enabled_providers"; export const ProviderView: FunctionComponent = () => { @@ -37,7 +36,7 @@ export const ProviderView: FunctionComponent = () => { const select = useCallback( (v?: ProviderInfo) => { - show(ModalKey, v ?? null); + show(ProviderModal, v ?? null); }, [show] ); @@ -72,12 +71,14 @@ export const ProviderView: FunctionComponent = () => { {cards} + ); }; -export const ProviderModal: FunctionComponent = () => { - const payload = usePayload(ModalKey); +const ProviderTool: FunctionComponent = () => { + const payload = usePayload(); + const Modal = useModal(); const { hide } = useModalControl(); const [staged, setChange] = useState({}); @@ -121,20 +122,6 @@ export const ProviderModal: FunctionComponent = () => { const canSave = info !== null; - const footer = useMemo( - () => ( - <> - - - - ), - [canSave, payload, deletePayload, addProvider] - ); - const onSelect = useCallback((item: Nullable) => { if (item) { setInfo(item); @@ -237,8 +224,19 @@ export const ProviderModal: FunctionComponent = () => { [] ); + const footer = ( + <> + + + + ); + return ( - + @@ -266,6 +264,8 @@ export const ProviderModal: FunctionComponent = () => { - + ); }; + +const ProviderModal = withModal(ProviderTool, "provider-tool"); diff --git a/frontend/src/pages/Settings/Providers/index.tsx b/frontend/src/pages/Settings/Providers/index.tsx index 7ea651f6f..991973ab0 100644 --- a/frontend/src/pages/Settings/Providers/index.tsx +++ b/frontend/src/pages/Settings/Providers/index.tsx @@ -1,6 +1,6 @@ import { FunctionComponent } from "react"; import { Group, Input, Layout } from "../components"; -import { ProviderModal, ProviderView } from "./components"; +import { ProviderView } from "./components"; const SettingsProvidersView: FunctionComponent = () => { return ( @@ -10,7 +10,6 @@ const SettingsProvidersView: FunctionComponent = () => { - ); }; diff --git a/frontend/src/pages/System/Backups/BackupDeleteModal.tsx b/frontend/src/pages/System/Backups/BackupDeleteModal.tsx index 1f6ea6ece..19070dde7 100644 --- a/frontend/src/pages/System/Backups/BackupDeleteModal.tsx +++ b/frontend/src/pages/System/Backups/BackupDeleteModal.tsx @@ -1,16 +1,20 @@ -import { AsyncButton, BaseModal, BaseModalProps } from "@/components"; -import { useModalControl, usePayload } from "@/modules/redux/hooks/modal"; +import { AsyncButton } from "@/components"; +import { + useModal, + useModalControl, + usePayload, + withModal, +} from "@/modules/modals"; import React, { FunctionComponent } from "react"; import { Button } from "react-bootstrap"; import { useDeleteBackups } from "../../../apis/hooks"; -interface Props extends BaseModalProps {} - -const SystemBackupDeleteModal: FunctionComponent = ({ ...modal }) => { +const SystemBackupDeleteModal: FunctionComponent = () => { const { mutateAsync } = useDeleteBackups(); - const result = usePayload(modal.modalKey); + const result = usePayload(); + const Modal = useModal(); const { hide } = useModalControl(); const footer = ( @@ -19,9 +23,7 @@ const SystemBackupDeleteModal: FunctionComponent = ({ ...modal }) => { @@ -34,7 +36,7 @@ const SystemBackupDeleteModal: FunctionComponent = ({ ...modal }) => { return null; } }} - onSuccess={() => hide(modal.modalKey)} + onSuccess={() => hide()} > Delete @@ -43,10 +45,10 @@ const SystemBackupDeleteModal: FunctionComponent = ({ ...modal }) => { ); return ( - - Are you sure you want to delete the backup '{result}'? - + + Are you sure you want to delete the backup '{result}'? + ); }; -export default SystemBackupDeleteModal; +export default withModal(SystemBackupDeleteModal, "delete"); diff --git a/frontend/src/pages/System/Backups/BackupRestoreModal.tsx b/frontend/src/pages/System/Backups/BackupRestoreModal.tsx index 69d6ae12d..80015c120 100644 --- a/frontend/src/pages/System/Backups/BackupRestoreModal.tsx +++ b/frontend/src/pages/System/Backups/BackupRestoreModal.tsx @@ -1,27 +1,29 @@ import { useRestoreBackups } from "@/apis/hooks/system"; -import { AsyncButton, BaseModal, BaseModalProps } from "@/components"; -import { useModalControl, usePayload } from "@/modules/redux/hooks/modal"; +import { AsyncButton } from "@/components"; +import { + useModal, + useModalControl, + usePayload, + withModal, +} from "@/modules/modals"; import React, { FunctionComponent } from "react"; import { Button } from "react-bootstrap"; -interface Props extends BaseModalProps {} +const SystemBackupRestoreModal: FunctionComponent = () => { + const result = usePayload(); -const SystemBackupRestoreModal: FunctionComponent = ({ ...modal }) => { - const result = usePayload(modal.modalKey); + const Modal = useModal(); + const { hide } = useModalControl(); const { mutateAsync } = useRestoreBackups(); - const { hide } = useModalControl(); - const footer = (

@@ -34,7 +36,7 @@ const SystemBackupRestoreModal: FunctionComponent = ({ ...modal }) => { return null; } }} - onSuccess={() => hide(modal.modalKey)} + onSuccess={() => hide()} > Restore @@ -43,11 +45,13 @@ const SystemBackupRestoreModal: FunctionComponent = ({ ...modal }) => { ); return ( - - Are you sure you want to restore the backup '{result}'? Bazarr will - automatically restart and reload the UI during the restore process. - + + + Are you sure you want to restore the backup '{result}'? Bazarr will + automatically restart and reload the UI during the restore process. + + ); }; -export default SystemBackupRestoreModal; +export default withModal(SystemBackupRestoreModal, "restore"); diff --git a/frontend/src/pages/System/Backups/table.tsx b/frontend/src/pages/System/Backups/table.tsx index 4beeb5e7f..d986dbaa1 100644 --- a/frontend/src/pages/System/Backups/table.tsx +++ b/frontend/src/pages/System/Backups/table.tsx @@ -1,5 +1,5 @@ import { ActionButton, PageTable } from "@/components"; -import { useModalControl } from "@/modules/redux/hooks/modal"; +import { useModalControl } from "@/modules/modals"; import { faClock, faHistory, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React, { FunctionComponent, useMemo } from "react"; @@ -42,11 +42,15 @@ const Table: FunctionComponent = ({ backups }) => { show("restore", row.row.original.filename)} + onClick={() => + show(SystemBackupRestoreModal, row.row.original.filename) + } > show("delete", row.row.original.filename)} + onClick={() => + show(SystemBackupDeleteModal, row.row.original.filename) + } > ); @@ -59,14 +63,8 @@ const Table: FunctionComponent = ({ backups }) => { return ( - - + + ); }; diff --git a/frontend/src/pages/System/Logs/modal.tsx b/frontend/src/pages/System/Logs/modal.tsx index 946ddf51d..5632eb412 100644 --- a/frontend/src/pages/System/Logs/modal.tsx +++ b/frontend/src/pages/System/Logs/modal.tsx @@ -1,9 +1,11 @@ -import { BaseModal, BaseModalProps } from "@/components"; -import { usePayload } from "@/modules/redux/hooks/modal"; +import { useModal, usePayload, withModal } from "@/modules/modals"; import { FunctionComponent, useMemo } from "react"; -const SystemLogModal: FunctionComponent = ({ ...modal }) => { - const stack = usePayload(modal.modalKey); +const SystemLogModal: FunctionComponent = () => { + const stack = usePayload(); + + const Modal = useModal(); + const result = useMemo( () => stack?.split("\\n").map((v, idx) => ( @@ -13,13 +15,14 @@ const SystemLogModal: FunctionComponent = ({ ...modal }) => { )), [stack] ); + return ( - +
         {result}
       
-
+ ); }; -export default SystemLogModal; +export default withModal(SystemLogModal, "system-log"); diff --git a/frontend/src/pages/System/Logs/table.tsx b/frontend/src/pages/System/Logs/table.tsx index 04a965ed1..d08c7fcbf 100644 --- a/frontend/src/pages/System/Logs/table.tsx +++ b/frontend/src/pages/System/Logs/table.tsx @@ -1,5 +1,5 @@ import { ActionButton, PageTable } from "@/components"; -import { useModalControl } from "@/modules/redux/hooks/modal"; +import { useModalControl } from "@/modules/modals"; import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { faBug, @@ -60,7 +60,7 @@ const Table: FunctionComponent = ({ logs }) => { return ( show("system-log", value)} + onClick={() => show(SystemLogModal, value)} > ); } else { @@ -75,7 +75,7 @@ const Table: FunctionComponent = ({ logs }) => { return ( <> - + ); }; From e18657e4261cae67d6fe5a235a001dede26721c5 Mon Sep 17 00:00:00 2001 From: LASER-Yi Date: Sun, 27 Mar 2022 16:03:04 +0800 Subject: [PATCH 3/4] 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" > - + {
- + Date: Sun, 27 Mar 2022 16:05:01 +0800 Subject: [PATCH 4/4] no log: Cleanup imports --- .../modals/subtitle-tools/ColorTool.tsx | 2 +- .../modals/subtitle-tools/Translation.tsx | 2 +- .../components/modals/subtitle-tools/tools.ts | 177 ++++++++++++++++++ frontend/src/components/modals/toolOptions.ts | 177 ------------------ 4 files changed, 179 insertions(+), 179 deletions(-) delete mode 100644 frontend/src/components/modals/toolOptions.ts diff --git a/frontend/src/components/modals/subtitle-tools/ColorTool.tsx b/frontend/src/components/modals/subtitle-tools/ColorTool.tsx index b5ae20acc..f110164d4 100644 --- a/frontend/src/components/modals/subtitle-tools/ColorTool.tsx +++ b/frontend/src/components/modals/subtitle-tools/ColorTool.tsx @@ -3,8 +3,8 @@ 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"; +import { colorOptions } from "./tools"; const ColorTool: FunctionComponent = () => { const [selection, setSelection] = useState>(null); diff --git a/frontend/src/components/modals/subtitle-tools/Translation.tsx b/frontend/src/components/modals/subtitle-tools/Translation.tsx index 5f87c3121..f759f5c29 100644 --- a/frontend/src/components/modals/subtitle-tools/Translation.tsx +++ b/frontend/src/components/modals/subtitle-tools/Translation.tsx @@ -3,8 +3,8 @@ 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"; +import { availableTranslation } from "./tools"; const TranslationTool: FunctionComponent = () => { const { data: languages } = useEnabledLanguages(); diff --git a/frontend/src/components/modals/subtitle-tools/tools.ts b/frontend/src/components/modals/subtitle-tools/tools.ts index 4310e9791..3b2e86719 100644 --- a/frontend/src/components/modals/subtitle-tools/tools.ts +++ b/frontend/src/components/modals/subtitle-tools/tools.ts @@ -1,3 +1,4 @@ +import { SelectorOption } from "@/components"; import { faClock, faCode, @@ -78,3 +79,179 @@ export const tools: ToolOptions[] = [ modal: Translation, }, ]; + +export const availableTranslation = { + af: "afrikaans", + sq: "albanian", + am: "amharic", + ar: "arabic", + hy: "armenian", + az: "azerbaijani", + eu: "basque", + be: "belarusian", + bn: "bengali", + bs: "bosnian", + bg: "bulgarian", + ca: "catalan", + ceb: "cebuano", + ny: "chichewa", + zh: "chinese (simplified)", + zt: "chinese (traditional)", + co: "corsican", + hr: "croatian", + cs: "czech", + da: "danish", + nl: "dutch", + en: "english", + eo: "esperanto", + et: "estonian", + tl: "filipino", + fi: "finnish", + fr: "french", + fy: "frisian", + gl: "galician", + ka: "georgian", + de: "german", + el: "greek", + gu: "gujarati", + ht: "haitian creole", + ha: "hausa", + haw: "hawaiian", + iw: "hebrew", + hi: "hindi", + hmn: "hmong", + hu: "hungarian", + is: "icelandic", + ig: "igbo", + id: "indonesian", + ga: "irish", + it: "italian", + ja: "japanese", + jw: "javanese", + kn: "kannada", + kk: "kazakh", + km: "khmer", + ko: "korean", + ku: "kurdish (kurmanji)", + ky: "kyrgyz", + lo: "lao", + la: "latin", + lv: "latvian", + lt: "lithuanian", + lb: "luxembourgish", + mk: "macedonian", + mg: "malagasy", + ms: "malay", + ml: "malayalam", + mt: "maltese", + mi: "maori", + mr: "marathi", + mn: "mongolian", + my: "myanmar (burmese)", + ne: "nepali", + no: "norwegian", + ps: "pashto", + fa: "persian", + pl: "polish", + pt: "portuguese", + pa: "punjabi", + ro: "romanian", + ru: "russian", + sm: "samoan", + gd: "scots gaelic", + sr: "serbian", + st: "sesotho", + sn: "shona", + sd: "sindhi", + si: "sinhala", + sk: "slovak", + sl: "slovenian", + so: "somali", + es: "spanish", + su: "sundanese", + sw: "swahili", + sv: "swedish", + tg: "tajik", + ta: "tamil", + te: "telugu", + th: "thai", + tr: "turkish", + uk: "ukrainian", + ur: "urdu", + uz: "uzbek", + vi: "vietnamese", + cy: "welsh", + xh: "xhosa", + yi: "yiddish", + yo: "yoruba", + zu: "zulu", + fil: "Filipino", + he: "Hebrew", +}; + +export const colorOptions: SelectorOption[] = [ + { + label: "White", + value: "white", + }, + { + label: "Light Gray", + value: "light-gray", + }, + { + label: "Red", + value: "red", + }, + { + label: "Green", + value: "green", + }, + { + label: "Yellow", + value: "yellow", + }, + { + label: "Blue", + value: "blue", + }, + { + label: "Magenta", + value: "magenta", + }, + { + label: "Cyan", + value: "cyan", + }, + { + label: "Black", + value: "black", + }, + { + label: "Dark Red", + value: "dark-red", + }, + { + label: "Dark Green", + value: "dark-green", + }, + { + label: "Dark Yellow", + value: "dark-yellow", + }, + { + label: "Dark Blue", + value: "dark-blue", + }, + { + label: "Dark Magenta", + value: "dark-magenta", + }, + { + label: "Dark Cyan", + value: "dark-cyan", + }, + { + label: "Dark Grey", + value: "dark-grey", + }, +]; diff --git a/frontend/src/components/modals/toolOptions.ts b/frontend/src/components/modals/toolOptions.ts deleted file mode 100644 index 6acee1a6a..000000000 --- a/frontend/src/components/modals/toolOptions.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { SelectorOption } from ".."; - -export const availableTranslation = { - af: "afrikaans", - sq: "albanian", - am: "amharic", - ar: "arabic", - hy: "armenian", - az: "azerbaijani", - eu: "basque", - be: "belarusian", - bn: "bengali", - bs: "bosnian", - bg: "bulgarian", - ca: "catalan", - ceb: "cebuano", - ny: "chichewa", - zh: "chinese (simplified)", - zt: "chinese (traditional)", - co: "corsican", - hr: "croatian", - cs: "czech", - da: "danish", - nl: "dutch", - en: "english", - eo: "esperanto", - et: "estonian", - tl: "filipino", - fi: "finnish", - fr: "french", - fy: "frisian", - gl: "galician", - ka: "georgian", - de: "german", - el: "greek", - gu: "gujarati", - ht: "haitian creole", - ha: "hausa", - haw: "hawaiian", - iw: "hebrew", - hi: "hindi", - hmn: "hmong", - hu: "hungarian", - is: "icelandic", - ig: "igbo", - id: "indonesian", - ga: "irish", - it: "italian", - ja: "japanese", - jw: "javanese", - kn: "kannada", - kk: "kazakh", - km: "khmer", - ko: "korean", - ku: "kurdish (kurmanji)", - ky: "kyrgyz", - lo: "lao", - la: "latin", - lv: "latvian", - lt: "lithuanian", - lb: "luxembourgish", - mk: "macedonian", - mg: "malagasy", - ms: "malay", - ml: "malayalam", - mt: "maltese", - mi: "maori", - mr: "marathi", - mn: "mongolian", - my: "myanmar (burmese)", - ne: "nepali", - no: "norwegian", - ps: "pashto", - fa: "persian", - pl: "polish", - pt: "portuguese", - pa: "punjabi", - ro: "romanian", - ru: "russian", - sm: "samoan", - gd: "scots gaelic", - sr: "serbian", - st: "sesotho", - sn: "shona", - sd: "sindhi", - si: "sinhala", - sk: "slovak", - sl: "slovenian", - so: "somali", - es: "spanish", - su: "sundanese", - sw: "swahili", - sv: "swedish", - tg: "tajik", - ta: "tamil", - te: "telugu", - th: "thai", - tr: "turkish", - uk: "ukrainian", - ur: "urdu", - uz: "uzbek", - vi: "vietnamese", - cy: "welsh", - xh: "xhosa", - yi: "yiddish", - yo: "yoruba", - zu: "zulu", - fil: "Filipino", - he: "Hebrew", -}; - -export const colorOptions: SelectorOption[] = [ - { - label: "White", - value: "white", - }, - { - label: "Light Gray", - value: "light-gray", - }, - { - label: "Red", - value: "red", - }, - { - label: "Green", - value: "green", - }, - { - label: "Yellow", - value: "yellow", - }, - { - label: "Blue", - value: "blue", - }, - { - label: "Magenta", - value: "magenta", - }, - { - label: "Cyan", - value: "cyan", - }, - { - label: "Black", - value: "black", - }, - { - label: "Dark Red", - value: "dark-red", - }, - { - label: "Dark Green", - value: "dark-green", - }, - { - label: "Dark Yellow", - value: "dark-yellow", - }, - { - label: "Dark Blue", - value: "dark-blue", - }, - { - label: "Dark Magenta", - value: "dark-magenta", - }, - { - label: "Dark Cyan", - value: "dark-cyan", - }, - { - label: "Dark Grey", - value: "dark-grey", - }, -];