diff options
author | LASER-Yi <[email protected]> | 2022-03-27 14:42:28 +0800 |
---|---|---|
committer | LASER-Yi <[email protected]> | 2022-03-27 14:42:28 +0800 |
commit | 658237dd5076a3d4823552ad17c101d3ba6177fc (patch) | |
tree | 1449c8378e36f5884cb022dd92132532fa15d3cf /frontend/src | |
parent | 87c5d0d9defdc3f01865eeb844dfe191934411fb (diff) | |
download | bazarr-658237dd5076a3d4823552ad17c101d3ba6177fc.tar.gz bazarr-658237dd5076a3d4823552ad17c101d3ba6177fc.zip |
Refactor modal system
Diffstat (limited to 'frontend/src')
32 files changed, 674 insertions, 586 deletions
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<BaseModalProps> = (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 ( - <Modal - centered - size={size} - show={isShowed && !needExit} - onHide={hide} - onExited={exit} - backdrop={closeable ? undefined : "static"} - className={clsx(`index-${showIndex}`)} - backdropClassName={clsx(`index-${showIndex}`)} - > - <Modal.Header closeButton={closeable}>{title}</Modal.Header> - <Modal.Body>{children}</Modal.Body> - <Modal.Footer hidden={footer === undefined}>{footer}</Modal.Footer> - </Modal> - ); -}; - -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<BaseModalProps> = (props) => { - const { ...modal } = props; +const MovieHistoryView: FunctionComponent = () => { + const movie = usePayload<Item.Movie>(); - const movie = usePayload<Item.Movie>(modal.modalKey); + const Modal = useModal({ size: "lg" }); const history = useMovieHistory(movie?.radarrId); @@ -84,7 +83,7 @@ export const MovieHistoryModal: FunctionComponent<BaseModalProps> = (props) => { ); return ( - <BaseModal title={`History - ${movie?.title ?? ""}`} {...modal}> + <Modal title={`History - ${movie?.title ?? ""}`}> <QueryOverlay result={history}> <PageTable emptyText="No History Found" @@ -92,14 +91,16 @@ export const MovieHistoryModal: FunctionComponent<BaseModalProps> = (props) => { data={data ?? []} ></PageTable> </QueryOverlay> - </BaseModal> + </Modal> ); }; -export const EpisodeHistoryModal: FunctionComponent<BaseModalProps> = ( - props -) => { - const episode = usePayload<Item.Episode>(props.modalKey); +export const MovieHistoryModal = withModal(MovieHistoryView, "movie-history"); + +const EpisodeHistoryView: FunctionComponent = () => { + const episode = usePayload<Item.Episode>(); + + const Modal = useModal({ size: "lg" }); const history = useEpisodeHistory(episode?.sonarrEpisodeId); @@ -175,7 +176,7 @@ export const EpisodeHistoryModal: FunctionComponent<BaseModalProps> = ( ); return ( - <BaseModal title={`History - ${episode?.title ?? ""}`} {...props}> + <Modal title={`History - ${episode?.title ?? ""}`}> <QueryOverlay result={history}> <PageTable emptyText="No History Found" @@ -183,6 +184,11 @@ export const EpisodeHistoryModal: FunctionComponent<BaseModalProps> = ( data={data ?? []} ></PageTable> </QueryOverlay> - </BaseModal> + </Modal> ); }; + +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<void, unknown, FormType.ModifyItem, unknown>; } -const Editor: FunctionComponent<Props & BaseModalProps> = (props) => { - const { mutation, ...modal } = props; - +const Editor: FunctionComponent<Props> = ({ mutation }) => { const { data: profiles } = useLanguageProfiles(); - const payload = usePayload<Item.Base>(modal.modalKey); - const { hide } = useModalControl(); - + const payload = usePayload<Item.Base>(); const { mutateAsync, isLoading } = mutation; + const { hide } = useModalControl(); + const hasTask = useIsAnyActionRunning(); const profileOptions = useMemo<SelectorOption<number>[]>( @@ -33,9 +35,12 @@ const Editor: FunctionComponent<Props & BaseModalProps> = (props) => { const [id, setId] = useState<Nullable<number>>(payload?.profileId ?? null); - useEffect(() => { - setId(payload?.profileId ?? null); - }, [payload]); + const Modal = useModal({ + closeable: !isLoading, + onMounted: () => { + setId(payload?.profileId ?? null); + }, + }); const footer = ( <AsyncButton @@ -56,21 +61,14 @@ const Editor: FunctionComponent<Props & BaseModalProps> = (props) => { return null; } }} - onSuccess={() => { - hide(); - }} + onSuccess={() => hide()} > Save </AsyncButton> ); return ( - <BaseModal - closeable={!isLoading} - footer={footer} - title={payload?.title} - {...modal} - > + <Modal title={payload?.title ?? "Item Editor"} footer={footer}> <Container fluid> <Form> <Form.Group> @@ -95,8 +93,8 @@ const Editor: FunctionComponent<Props & BaseModalProps> = (props) => { </Form.Group> </Form> </Container> - </BaseModal> + </Modal> ); }; -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<T extends SupportType> { ) => UseQueryResult<SearchResultType[] | undefined, unknown>; } -export function ManualSearchModal<T extends SupportType>( - props: Props<T> & BaseModalProps -) { - const { download, query: useSearch, ...modal } = props; +function ManualSearchView<T extends SupportType>(props: Props<T>) { + const { download, query: useSearch } = props; - const item = usePayload<T>(modal.modalKey); + const item = usePayload<T>(); const itemId = useMemo(() => GetItemId(item ?? {}), [item]); const [id, setId] = useState<number | undefined>(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<T extends SupportType>( } }; - const footer = ( - <Button variant="light" hidden={isStale} onClick={search}> - Search Again - </Button> - ); - const title = useMemo(() => { let title = "Unknown"; @@ -246,19 +225,39 @@ export function ManualSearchModal<T extends SupportType>( 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 = ( + <Button variant="light" hidden={isStale} onClick={search}> + Search Again + </Button> + ); + return ( - <BaseModal - closeable={results.isFetching === false} - size="xl" - title={title} - footer={footer} - {...modal} - > + <Modal title={title} footer={footer}> {content()} - </BaseModal> + </Modal> ); } +export const MovieSearchModal = withModal<Props<Item.Movie>>( + ManualSearchView, + "movie-manual-search" +); +export const EpisodeSearchModal = withModal<Props<Item.Episode>>( + 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<BaseModalProps> = (props) => { - const modal = props; - - const payload = usePayload<Item.Movie>(modal.modalKey); +const MovieUploadModal: FunctionComponent = () => { + const payload = usePayload<Item.Movie>(); const profile = useLanguageProfileBy(payload?.profileId); @@ -87,7 +84,7 @@ const MovieUploadModal: FunctionComponent<BaseModalProps> = (props) => { ); return ( - <SubtitleUploadModal + <SubtitleUploader hideAllLanguages initial={{ forced: false }} availableLanguages={availableLanguages} @@ -95,9 +92,8 @@ const MovieUploadModal: FunctionComponent<BaseModalProps> = (props) => { upload={upload} update={update} validate={validate} - {...modal} - ></SubtitleUploadModal> + ></SubtitleUploader> ); }; -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<SeriesProps & BaseModalProps> = ({ - episodes, - ...modal -}) => { - const payload = usePayload<Item.Series>(modal.modalKey); +const SeriesUploadModal: FunctionComponent<SeriesProps> = ({ episodes }) => { + const payload = usePayload<Item.Series>(); const profile = useLanguageProfileBy(payload?.profileId); @@ -165,16 +161,15 @@ const SeriesUploadModal: FunctionComponent<SeriesProps & BaseModalProps> = ({ ); return ( - <SubtitleUploadModal + <SubtitleUploader columns={columns} initial={{ instance: null }} availableLanguages={availableLanguages} upload={upload} update={update} validate={validate} - {...modal} - ></SubtitleUploadModal> + ></SubtitleUploader> ); }; -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<BaseModalProps & ToolModalProps> = ( - props -) => { - const { process, ...modal } = props; +const ColorTool: FunctionComponent<ToolModalProps> = ({ process }) => { const [selection, setSelection] = useState<Nullable<string>>(null); + const Modal = useModal(); + const submit = useCallback(() => { if (selection) { const action = submodProcessColor(selection); @@ -90,31 +93,29 @@ const AddColorModal: FunctionComponent<BaseModalProps & ToolModalProps> = ( } }, [selection, process]); - const footer = useMemo( - () => ( - <Button disabled={selection === null} onClick={submit}> - Save - </Button> - ), - [selection, submit] + const footer = ( + <Button disabled={selection === null} onClick={submit}> + Save + </Button> ); + return ( - <BaseModal title="Choose Color" footer={footer} {...modal}> + <Modal title="Choose Color" footer={footer}> <Selector options={colorOptions} onChange={setSelection}></Selector> - </BaseModal> + </Modal> ); }; -const FrameRateModal: FunctionComponent<BaseModalProps & ToolModalProps> = ( - props -) => { - const { process, ...modal } = props; +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); @@ -129,7 +130,7 @@ const FrameRateModal: FunctionComponent<BaseModalProps & ToolModalProps> = ( ); return ( - <BaseModal title="Change Frame Rate" footer={footer} {...modal}> + <Modal title="Change Frame Rate" footer={footer}> <InputGroup className="px-2"> <Form.Control placeholder="From" @@ -156,20 +157,20 @@ const FrameRateModal: FunctionComponent<BaseModalProps & ToolModalProps> = ( }} ></Form.Control> </InputGroup> - </BaseModal> + </Modal> ); }; -const AdjustTimesModal: FunctionComponent<BaseModalProps & ToolModalProps> = ( - props -) => { - const { process, ...modal } = props; +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) => { @@ -200,17 +201,14 @@ const AdjustTimesModal: FunctionComponent<BaseModalProps & ToolModalProps> = ( } }, [process, canSave, offset, isPlus]); - const footer = useMemo( - () => ( - <Button disabled={!canSave} onClick={submit}> - Save - </Button> - ), - [submit, canSave] + const footer = ( + <Button disabled={!canSave} onClick={submit}> + Save + </Button> ); return ( - <BaseModal title="Adjust Times" footer={footer} {...modal}> + <Modal title="Adjust Times" footer={footer}> <InputGroup> <InputGroup.Prepend> <Button @@ -242,14 +240,13 @@ const AdjustTimesModal: FunctionComponent<BaseModalProps & ToolModalProps> = ( onChange={updateOffset(3)} ></Form.Control> </InputGroup> - </BaseModal> + </Modal> ); }; -const TranslateModal: FunctionComponent<BaseModalProps & ToolModalProps> = ({ - process, - ...modal -}) => { +const TimeAdjustmentModal = withModal(TimeAdjustmentTool, "time-adjust-tool"); + +const TranslationTool: FunctionComponent<ToolModalProps> = ({ process }) => { const { data: languages } = useEnabledLanguages(); const available = useMemo( @@ -257,6 +254,8 @@ const TranslateModal: FunctionComponent<BaseModalProps & ToolModalProps> = ({ [languages] ); + const Modal = useModal(); + const [selectedLanguage, setLanguage] = useState<Nullable<Language.Info>>(null); @@ -266,17 +265,13 @@ const TranslateModal: FunctionComponent<BaseModalProps & ToolModalProps> = ({ } }, [selectedLanguage, process]); - const footer = useMemo( - () => ( - <Button disabled={!selectedLanguage} onClick={submit}> - Translate - </Button> - ), - [submit, selectedLanguage] + const footer = ( + <Button disabled={!selectedLanguage} onClick={submit}> + Translate + </Button> ); - return ( - <BaseModal title="Translate to" footer={footer} {...modal}> + <Modal title="Translation" footer={footer}> <Form.Label> Enabled languages not listed here are unsupported by Google Translate. </Form.Label> @@ -284,18 +279,21 @@ const TranslateModal: FunctionComponent<BaseModalProps & ToolModalProps> = ({ options={available} onChange={setLanguage} ></LanguageSelector> - </BaseModal> + </Modal> ); }; +const TranslationModal = withModal(TranslationTool, "translate-tool"); + const CanSelectSubtitle = (item: TableColumnType) => { return item.path.endsWith(".srt"); }; -const STM: FunctionComponent<BaseModalProps> = ({ ...props }) => { - const payload = usePayload<SupportType[]>(props.modalKey); +const STM: FunctionComponent = () => { + const payload = usePayload<SupportType[]>(); const [selections, setSelections] = useState<TableColumnType[]>([]); + const Modal = useModal({ size: "xl" }); const { hide } = useModalControl(); const { mutateAsync } = useSubtitleAction(); @@ -303,8 +301,7 @@ const STM: FunctionComponent<BaseModalProps> = ({ ...props }) => { const process = useCallback( (action: string, override?: Partial<FormType.ModifySubtitle>) => { 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<BaseModalProps> = ({ ...props }) => { dispatchTask(tasks, "modify-subtitles"); }, - [hide, props.modalKey, selections, mutateAsync] + [hide, selections, mutateAsync] ); const { show } = useModalControl(); @@ -383,92 +380,74 @@ const STM: FunctionComponent<BaseModalProps> = ({ ...props }) => { const plugins = [useRowSelect, useCustomSelection]; - const footer = useMemo( - () => ( - <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("add-color")}> - <ActionButtonItem icon={faPaintBrush}>Add Color</ActionButtonItem> - </Dropdown.Item> - <Dropdown.Item onSelect={() => show("change-frame-rate")}> - <ActionButtonItem icon={faFilm}>Change Frame Rate</ActionButtonItem> - </Dropdown.Item> - <Dropdown.Item onSelect={() => show("adjust-times")}> - <ActionButtonItem icon={faClock}>Adjust Times</ActionButtonItem> - </Dropdown.Item> - <Dropdown.Item onSelect={() => show("translate-sub")}> - <ActionButtonItem icon={faLanguage}>Translate</ActionButtonItem> - </Dropdown.Item> - </Dropdown.Menu> - </Dropdown> - ), - [selections.length, process, show] + 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 ( - <> - <BaseModal title={"Subtitle Tools"} footer={footer} {...props}> - <SimpleTable - emptyText="No External Subtitles Found" - plugins={plugins} - columns={columns} - onSelect={setSelections} - canSelect={CanSelectSubtitle} - data={data} - ></SimpleTable> - </BaseModal> - <AddColorModal process={process} modalKey="add-color"></AddColorModal> - <FrameRateModal - process={process} - modalKey="change-frame-rate" - ></FrameRateModal> - <AdjustTimesModal - process={process} - modalKey="adjust-times" - ></AdjustTimesModal> - <TranslateModal - process={process} - modalKey="translate-sub" - ></TranslateModal> - </> + <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 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<T> = (index: number, info?: PendingSubtitle<T>) => void; @@ -59,10 +58,7 @@ interface Props<T = unknown> { hideAllLanguages?: boolean; } -type ComponentProps<T> = Props<T> & - Omit<BaseModalProps, "footer" | "title" | "size">; - -function SubtitleUploadModal<T>(props: ComponentProps<T>) { +function SubtitleUploader<T>(props: Props<T>) { const { initial, columns, @@ -73,10 +69,16 @@ function SubtitleUploadModal<T>(props: ComponentProps<T>) { hideAllLanguages, } = props; - const { hide } = useModalControl(); - const [pending, setPending] = useState<PendingSubtitle<T>[]>([]); + 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<T>(props: ComponentProps<T>) { [columns, availableLanguages] ); - const showTable = pending.length > 0; - const canUpload = useMemo( () => pending.length > 0 && @@ -332,12 +332,7 @@ function SubtitleUploadModal<T>(props: ComponentProps<T>) { ); return ( - <BaseModal - size={showTable ? "xl" : "lg"} - title="Upload Subtitles" - footer={footer} - {...props} - > + <Modal title="Update Subtitles" footer={footer}> <Container fluid className="flex-column"> <Form> <Form.Group> @@ -360,8 +355,8 @@ function SubtitleUploadModal<T>(props: ComponentProps<T>) { </RowContext.Provider> </div> </Container> - </BaseModal> + </Modal> ); } -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<ModalData, "key">]: Dispatch<SetStateAction<ModalData[P]>>; +}; + +export const ModalDataContext = createContext<ModalData | null>(null); +export const ModalSetterContext = createContext<ModalSetter | null>(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<Props> = ({ 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 ( + <Modal + centered + size={size} + show={isShowed && !needExit} + onHide={hide} + onExited={exit} + backdrop={closeable ? undefined : "static"} + className={clsx(`index-${layer}`)} + backdropClassName={clsx(`index-${layer}`)} + > + {children} + </Modal> + ); +}; + +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<P> = FunctionComponent<P> & { + modalKey: string; +}; + +export default function withModal<T>( + Content: FunctionComponent<T>, + key: string +) { + const Comp: ModalComponent<T> = (props: ModalProps & T) => { + const [closeable, setCloseable] = useState(true); + const [size, setSize] = useState<ModalData["size"]>(undefined); + const data: ModalData = useMemo( + () => ({ + key, + size, + closeable, + }), + [closeable, size] + ); + + const setter: ModalSetter = useMemo( + () => ({ + closeable: setCloseable, + size: setSize, + }), + [] + ); + + return ( + <ModalDataContext.Provider value={data}> + <ModalSetterContext.Provider value={setter}> + <ModalWrapper> + <Content {...props}></Content> + </ModalWrapper> + </ModalSetterContext.Provider> + </ModalDataContext.Provider> + ); + }; + 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<StandardModalProps> = ({ + children, + footer, + title, +}) => { + const { closeable } = useModalData(); + return ( + <> + <Modal.Header closeButton={closeable}>{title}</Modal.Header> + <Modal.Body>{children}</Modal.Body> + <Modal.Footer hidden={footer === undefined}>{footer}</Modal.Footer> + </> + ); +}; 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<Omit<ModalData, "key">> & { + 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<ModalProps["onMounted"]>(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( + <P>(comp: ModalComponent<P>, 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>(): 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<T>(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 = () => { <ContentHeader.Button disabled={series.episodeFileCount === 0 || !available || hasTask} icon={faBriefcase} - onClick={() => show("tools", episodes)} + onClick={() => show(SubtitleToolModal, episodes)} > Tools </ContentHeader.Button> @@ -120,14 +120,14 @@ const SeriesEpisodesView: FunctionComponent = () => { !available } icon={faCloudUploadAlt} - onClick={() => show("upload", series)} + onClick={() => show(SeriesUploadModal, series)} > Upload </ContentHeader.Button> <ContentHeader.Button icon={faWrench} disabled={hasTask} - onClick={() => show("edit", series)} + onClick={() => show(ItemEditorModal, series)} > Edit Series </ContentHeader.Button> @@ -158,11 +158,8 @@ const SeriesEpisodesView: FunctionComponent = () => { ></Table> )} </Row> - <ItemEditorModal modalKey="edit" mutation={mutation}></ItemEditorModal> - <SeriesUploadModal - modalKey="upload" - episodes={episodes ?? []} - ></SeriesUploadModal> + <ItemEditorModal mutation={mutation}></ItemEditorModal> + <SeriesUploadModal episodes={episodes ?? []}></SeriesUploadModal> </Container> ); }; 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<Props> = ({ icon={faUser} disabled={series?.profileId === null || disabled} onClick={() => { - show("manual-search", row.original); + show(EpisodeSearchModal, row.original); }} ></ActionButton> <ActionButton icon={faHistory} disabled={disabled} onClick={() => { - show("history", row.original); + show(EpisodeHistoryModal, row.original); }} ></ActionButton> <ActionButton icon={faBriefcase} disabled={disabled} onClick={() => { - show("tools", [row.original]); + show(SubtitleToolModal, [row.original]); }} ></ActionButton> </ButtonGroup> @@ -214,13 +209,12 @@ const Table: FunctionComponent<Props> = ({ }} emptyText="No Episode Found For This Series" ></GroupTable> - <SubtitleToolModal modalKey="tools" size="lg"></SubtitleToolModal> - <EpisodeHistoryModal modalKey="history" size="lg"></EpisodeHistoryModal> - <ManualSearchModal - modalKey="manual-search" + <SubtitleToolModal></SubtitleToolModal> + <EpisodeHistoryModal></EpisodeHistoryModal> + <EpisodeSearchModal download={download} query={useEpisodesProvider} - ></ManualSearchModal> + ></EpisodeSearchModal> </> ); }; 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 = () => { <ContentHeader.Button icon={faUser} disabled={movie.profileId === null || hasTask} - onClick={() => show("manual-search", movie)} + onClick={() => show(MovieSearchModal, movie)} > Manual </ContentHeader.Button> <ContentHeader.Button icon={faHistory} - onClick={() => show("history", movie)} + onClick={() => show(MovieHistoryModal, movie)} > History </ContentHeader.Button> <ContentHeader.Button icon={faToolbox} disabled={hasTask} - onClick={() => show("tools", [movie])} + onClick={() => show(SubtitleToolModal, [movie])} > Tools </ContentHeader.Button> @@ -145,14 +144,14 @@ const MovieDetailView: FunctionComponent = () => { <ContentHeader.Button disabled={!allowEdit || movie.profileId === null || hasTask} icon={faCloudUploadAlt} - onClick={() => show("upload", movie)} + onClick={() => show(MovieUploadModal, movie)} > Upload </ContentHeader.Button> <ContentHeader.Button icon={faWrench} disabled={hasTask} - onClick={() => show("edit", movie)} + onClick={() => show(ItemEditorModal, movie)} > Edit Movie </ContentHeader.Button> @@ -174,15 +173,14 @@ const MovieDetailView: FunctionComponent = () => { <Row> <Table movie={movie} profile={profile} disabled={hasTask}></Table> </Row> - <ItemEditorModal modalKey="edit" mutation={mutation}></ItemEditorModal> - <SubtitleToolModal modalKey="tools" size="lg"></SubtitleToolModal> - <MovieHistoryModal modalKey="history" size="lg"></MovieHistoryModal> - <MovieUploadModal modalKey="upload" size="lg"></MovieUploadModal> - <ManualSearchModal - modalKey="manual-search" + <ItemEditorModal mutation={mutation}></ItemEditorModal> + <SubtitleToolModal></SubtitleToolModal> + <MovieHistoryModal></MovieHistoryModal> + <MovieUploadModal></MovieUploadModal> + <MovieSearchModal download={download} query={useMoviesProvider} - ></ManualSearchModal> + ></MovieSearchModal> </Container> ); }; 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 ( <ActionBadge icon={faWrench} - onClick={() => show("edit", row.original)} + onClick={() => show(ItemEditorModal, row.original)} ></ActionBadge> ); }, @@ -105,7 +106,7 @@ const MovieView: FunctionComponent = () => { <title>Movies - Bazarr</title> </Helmet> <ItemView query={query} columns={columns}></ItemView> - <ItemEditorModal modalKey="edit" mutation={mutation}></ItemEditorModal> + <ItemEditorModal mutation={mutation}></ItemEditorModal> </Container> ); }; 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 ( <ActionBadge icon={faWrench} - onClick={() => show("edit", original)} + onClick={() => show(ItemEditorModal, original)} ></ActionBadge> ); }, @@ -107,7 +108,7 @@ const SeriesView: FunctionComponent = () => { <title>Series - Bazarr</title> </Helmet> <ItemView query={query} columns={columns}></ItemView> - <ItemEditorModal modalKey="edit" mutation={mutation}></ItemEditorModal> + <ItemEditorModal mutation={mutation}></ItemEditorModal> </Container> ); }; 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 & BaseModalProps> = ( - props -) => { - const { update, ...modal } = props; - - const profile = usePayload<Language.Profile>(modal.modalKey); +const LanguagesProfileModal: FunctionComponent<Props> = ({ update }) => { + const profile = usePayload<Language.Profile>(); const { hide } = useModalControl(); @@ -66,13 +64,12 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = ( 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<number>[] = useMemo(() => { const options = [...cutoffOptions]; @@ -134,18 +131,6 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = ( const canSave = current.name.length > 0 && current.items.length > 0; - const footer = ( - <Button - disabled={!canSave} - onClick={() => { - hide(); - update(current); - }} - > - Save - </Button> - ); - const columns = useMemo<Column<Language.ProfileItem>[]>( () => [ { @@ -253,8 +238,20 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = ( [languages] ); + const footer = ( + <Button + disabled={!canSave} + onClick={() => { + hide(); + update(current); + }} + > + Save + </Button> + ); + return ( - <BaseModal size="lg" title="Languages Profile" footer={footer} {...modal}> + <Modal title="Languages Profile" footer={footer}> <Input> <Form.Control type="text" @@ -319,8 +316,8 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = ( ></Selector> <Message>Download subtitle file without format conversion</Message> </Input> - </BaseModal> + </Modal> ); }; -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<ModifyFn>( (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"} </Button> - <Modal update={updateProfile} modalKey="profile"></Modal> + <Modal update={updateProfile}></Modal> </> ); }; 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<ModalProps & BaseModalProps> = ({ - selections, - ...modal -}) => { +const NotificationTool: FunctionComponent<Props> = ({ selections }) => { const options = useMemo<SelectorOption<Settings.NotificationInfo>[]>( () => selections @@ -43,16 +33,11 @@ const NotificationModal: FunctionComponent<ModalProps & BaseModalProps> = ({ "name" ); - const payload = usePayload<Settings.NotificationInfo>(modal.modalKey); - const { hide } = useModalControl(); + const payload = usePayload<Settings.NotificationInfo>(); const [current, setCurrent] = useState<Nullable<Settings.NotificationInfo>>(payload); - useEffect(() => { - setCurrent(payload); - }, [payload]); - const updateUrl = useCallback((url: string) => { setCurrent((current) => { if (current) { @@ -69,55 +54,60 @@ const NotificationModal: FunctionComponent<ModalProps & BaseModalProps> = ({ const canSave = current !== null && current?.url !== null && current?.url.length !== 0; - const footer = useMemo( - () => ( - <> - <AsyncButton - className="mr-auto" - disabled={!canSave} - variant="outline-secondary" - promise={() => { - if (current && current.url) { - return api.system.testNotification(current.url); - } else { - return null; - } - }} - > - Test - </AsyncButton> - <Button - hidden={payload === null} - variant="danger" - onClick={() => { - if (current) { - update({ ...current, enabled: false }); - } - hide(); - }} - > - Remove - </Button> - <Button - disabled={!canSave} - onClick={() => { - if (current) { - update({ ...current, enabled: true }); - } - hide(); - }} - > - Save - </Button> - </> - ), - [canSave, payload, current, hide, update] - ); - const getLabel = useCallback((v: Settings.NotificationInfo) => v.name, []); + const Modal = useModal({ + onMounted: () => { + setCurrent(payload); + }, + }); + + const { hide } = useModalControl(); + + const footer = ( + <> + <AsyncButton + className="mr-auto" + disabled={!canSave} + variant="outline-secondary" + promise={() => { + if (current && current.url) { + return api.system.testNotification(current.url); + } else { + return null; + } + }} + > + Test + </AsyncButton> + <Button + hidden={payload === null} + variant="danger" + onClick={() => { + if (current) { + update({ ...current, enabled: false }); + } + hide(); + }} + > + Remove + </Button> + <Button + disabled={!canSave} + onClick={() => { + if (current) { + update({ ...current, enabled: true }); + } + hide(); + }} + > + Save + </Button> + </> + ); + return ( - <BaseModal title="Notification" footer={footer} {...modal}> + <Modal title="Notification" footer={footer}> <Container fluid> <Row> <Col xs={12}> @@ -145,10 +135,12 @@ const NotificationModal: FunctionComponent<ModalProps & BaseModalProps> = ({ </Col> </Row> </Container> - </BaseModal> + </Modal> ); }; +const NotificationModal = withModal(NotificationTool, "notification-tool"); + export const NotificationView: FunctionComponent = () => { const notifications = useLatestArray<Settings.NotificationInfo>( notificationsKey, @@ -165,7 +157,7 @@ export const NotificationView: FunctionComponent = () => { <ColCard key={BuildKey(idx, v.name)} header={v.name} - onClick={() => show("notifications", v)} + onClick={() => show(NotificationModal, v)} ></ColCard> )); }, [notifications, show]); @@ -174,12 +166,9 @@ export const NotificationView: FunctionComponent = () => { <Container fluid> <Row> {elements}{" "} - <ColCard plus onClick={() => show("notifications")}></ColCard> + <ColCard plus onClick={() => show(NotificationModal)}></ColCard> </Row> - <NotificationModal - selections={notifications ?? []} - modalKey="notifications" - ></NotificationModal> + <NotificationModal selections={notifications ?? []}></NotificationModal> </Container> ); }; 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} <ColCard key="add-card" plus onClick={select}></ColCard> </Row> + <ProviderModal></ProviderModal> </Container> ); }; -export const ProviderModal: FunctionComponent = () => { - const payload = usePayload<ProviderInfo>(ModalKey); +const ProviderTool: FunctionComponent = () => { + const payload = usePayload<ProviderInfo>(); + const Modal = useModal(); const { hide } = useModalControl(); const [staged, setChange] = useState<LooseObject>({}); @@ -121,20 +122,6 @@ export const ProviderModal: FunctionComponent = () => { const canSave = info !== null; - const footer = useMemo( - () => ( - <> - <Button hidden={!payload} variant="danger" onClick={deletePayload}> - Delete - </Button> - <Button disabled={!canSave} onClick={addProvider}> - Save - </Button> - </> - ), - [canSave, payload, deletePayload, addProvider] - ); - const onSelect = useCallback((item: Nullable<ProviderInfo>) => { if (item) { setInfo(item); @@ -237,8 +224,19 @@ export const ProviderModal: FunctionComponent = () => { [] ); + const footer = ( + <> + <Button hidden={!payload} variant="danger" onClick={deletePayload}> + Delete + </Button> + <Button disabled={!canSave} onClick={addProvider}> + Save + </Button> + </> + ); + return ( - <BaseModal title="Provider" footer={footer} modalKey={ModalKey}> + <Modal title="Provider" footer={footer}> <StagedChangesContext.Provider value={[staged, setChange]}> <Container> <Row> @@ -266,6 +264,8 @@ export const ProviderModal: FunctionComponent = () => { </Row> </Container> </StagedChangesContext.Provider> - </BaseModal> + </Modal> ); }; + +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 = () => { <ProviderView></ProviderView> </Input> </Group> - <ProviderModal></ProviderModal> </Layout> ); }; 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<Props> = ({ ...modal }) => { +const SystemBackupDeleteModal: FunctionComponent = () => { const { mutateAsync } = useDeleteBackups(); - const result = usePayload<string>(modal.modalKey); + const result = usePayload<string>(); + const Modal = useModal(); const { hide } = useModalControl(); const footer = ( @@ -19,9 +23,7 @@ const SystemBackupDeleteModal: FunctionComponent<Props> = ({ ...modal }) => { <Button variant="outline-secondary" className="mr-2" - onClick={() => { - hide(modal.modalKey); - }} + onClick={() => hide()} > Cancel </Button> @@ -34,7 +36,7 @@ const SystemBackupDeleteModal: FunctionComponent<Props> = ({ ...modal }) => { return null; } }} - onSuccess={() => hide(modal.modalKey)} + onSuccess={() => hide()} > Delete </AsyncButton> @@ -43,10 +45,10 @@ const SystemBackupDeleteModal: FunctionComponent<Props> = ({ ...modal }) => { ); return ( - <BaseModal title="Delete Backup" footer={footer} {...modal}> - Are you sure you want to delete the backup '{result}'? - </BaseModal> + <Modal title="Delete Backup" footer={footer}> + <span>Are you sure you want to delete the backup '{result}'?</span> + </Modal> ); }; -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<string>(); -const SystemBackupRestoreModal: FunctionComponent<Props> = ({ ...modal }) => { - const result = usePayload<string>(modal.modalKey); + const Modal = useModal(); + const { hide } = useModalControl(); const { mutateAsync } = useRestoreBackups(); - const { hide } = useModalControl(); - const footer = ( <div className="d-flex flex-row-reverse flex-grow-1 justify-content-between"> <div> <Button variant="outline-secondary" className="mr-2" - onClick={() => { - hide(modal.modalKey); - }} + onClick={() => hide()} > Cancel </Button> @@ -34,7 +36,7 @@ const SystemBackupRestoreModal: FunctionComponent<Props> = ({ ...modal }) => { return null; } }} - onSuccess={() => hide(modal.modalKey)} + onSuccess={() => hide()} > Restore </AsyncButton> @@ -43,11 +45,13 @@ const SystemBackupRestoreModal: FunctionComponent<Props> = ({ ...modal }) => { ); return ( - <BaseModal title="Restore Backup" footer={footer} {...modal}> - Are you sure you want to restore the backup '{result}'? Bazarr will - automatically restart and reload the UI during the restore process. - </BaseModal> + <Modal title="Restore Backup" footer={footer}> + <span> + Are you sure you want to restore the backup '{result}'? Bazarr will + automatically restart and reload the UI during the restore process. + </span> + </Modal> ); }; -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<Props> = ({ backups }) => { <ButtonGroup> <ActionButton icon={faHistory} - onClick={() => show("restore", row.row.original.filename)} + onClick={() => + show(SystemBackupRestoreModal, row.row.original.filename) + } ></ActionButton> <ActionButton icon={faTrash} - onClick={() => show("delete", row.row.original.filename)} + onClick={() => + show(SystemBackupDeleteModal, row.row.original.filename) + } ></ActionButton> </ButtonGroup> ); @@ -59,14 +63,8 @@ const Table: FunctionComponent<Props> = ({ backups }) => { return ( <React.Fragment> <PageTable columns={columns} data={backups}></PageTable> - <SystemBackupRestoreModal - modalKey="restore" - size="lg" - ></SystemBackupRestoreModal> - <SystemBackupDeleteModal - modalKey="delete" - size="lg" - ></SystemBackupDeleteModal> + <SystemBackupRestoreModal></SystemBackupRestoreModal> + <SystemBackupDeleteModal></SystemBackupDeleteModal> </React.Fragment> ); }; 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<BaseModalProps> = ({ ...modal }) => { - const stack = usePayload<string>(modal.modalKey); +const SystemLogModal: FunctionComponent = () => { + const stack = usePayload<string>(); + + const Modal = useModal(); + const result = useMemo( () => stack?.split("\\n").map((v, idx) => ( @@ -13,13 +15,14 @@ const SystemLogModal: FunctionComponent<BaseModalProps> = ({ ...modal }) => { )), [stack] ); + return ( - <BaseModal title="Stack traceback" {...modal}> + <Modal title="Stack traceback"> <pre> <code>{result}</code> </pre> - </BaseModal> + </Modal> ); }; -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<Props> = ({ logs }) => { return ( <ActionButton icon={faLayerGroup} - onClick={() => show("system-log", value)} + onClick={() => show(SystemLogModal, value)} ></ActionButton> ); } else { @@ -75,7 +75,7 @@ const Table: FunctionComponent<Props> = ({ logs }) => { return ( <> <PageTable columns={columns} data={logs}></PageTable> - <SystemLogModal size="xl" modalKey="system-log"></SystemLogModal> + <SystemLogModal></SystemLogModal> </> ); }; |