Refactor modal system

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

@ -131,6 +131,5 @@ export * from "./buttons";
export * from "./header"; export * from "./header";
export * from "./inputs"; export * from "./inputs";
export * from "./LanguageSelector"; export * from "./LanguageSelector";
export * from "./modals";
export * from "./SearchBar"; export * from "./SearchBar";
export * from "./tables"; export * from "./tables";

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

@ -4,18 +4,17 @@ import {
useMovieAddBlacklist, useMovieAddBlacklist,
useMovieHistory, useMovieHistory,
} from "@/apis/hooks"; } from "@/apis/hooks";
import { usePayload } from "@/modules/redux/hooks/modal"; import { useModal, usePayload, withModal } from "@/modules/modals";
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Column } from "react-table"; import { Column } from "react-table";
import { HistoryIcon, PageTable, QueryOverlay, TextPopover } from ".."; import { HistoryIcon, PageTable, QueryOverlay, TextPopover } from "..";
import Language from "../bazarr/Language"; import Language from "../bazarr/Language";
import { BlacklistButton } from "../inputs/blacklist"; import { BlacklistButton } from "../inputs/blacklist";
import BaseModal, { BaseModalProps } from "./BaseModal";
export const MovieHistoryModal: FunctionComponent<BaseModalProps> = (props) => { const MovieHistoryView: FunctionComponent = () => {
const { ...modal } = props; const movie = usePayload<Item.Movie>();
const movie = usePayload<Item.Movie>(modal.modalKey); const Modal = useModal({ size: "lg" });
const history = useMovieHistory(movie?.radarrId); const history = useMovieHistory(movie?.radarrId);
@ -84,7 +83,7 @@ export const MovieHistoryModal: FunctionComponent<BaseModalProps> = (props) => {
); );
return ( return (
<BaseModal title={`History - ${movie?.title ?? ""}`} {...modal}> <Modal title={`History - ${movie?.title ?? ""}`}>
<QueryOverlay result={history}> <QueryOverlay result={history}>
<PageTable <PageTable
emptyText="No History Found" emptyText="No History Found"
@ -92,14 +91,16 @@ export const MovieHistoryModal: FunctionComponent<BaseModalProps> = (props) => {
data={data ?? []} data={data ?? []}
></PageTable> ></PageTable>
</QueryOverlay> </QueryOverlay>
</BaseModal> </Modal>
); );
}; };
export const EpisodeHistoryModal: FunctionComponent<BaseModalProps> = ( export const MovieHistoryModal = withModal(MovieHistoryView, "movie-history");
props
) => { const EpisodeHistoryView: FunctionComponent = () => {
const episode = usePayload<Item.Episode>(props.modalKey); const episode = usePayload<Item.Episode>();
const Modal = useModal({ size: "lg" });
const history = useEpisodeHistory(episode?.sonarrEpisodeId); const history = useEpisodeHistory(episode?.sonarrEpisodeId);
@ -175,7 +176,7 @@ export const EpisodeHistoryModal: FunctionComponent<BaseModalProps> = (
); );
return ( return (
<BaseModal title={`History - ${episode?.title ?? ""}`} {...props}> <Modal title={`History - ${episode?.title ?? ""}`}>
<QueryOverlay result={history}> <QueryOverlay result={history}>
<PageTable <PageTable
emptyText="No History Found" emptyText="No History Found"
@ -183,6 +184,11 @@ export const EpisodeHistoryModal: FunctionComponent<BaseModalProps> = (
data={data ?? []} data={data ?? []}
></PageTable> ></PageTable>
</QueryOverlay> </QueryOverlay>
</BaseModal> </Modal>
); );
}; };
export const EpisodeHistoryModal = withModal(
EpisodeHistoryView,
"episode-history"
);

@ -1,26 +1,28 @@
import { useIsAnyActionRunning, useLanguageProfiles } from "@/apis/hooks"; 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 { GetItemId } from "@/utilities";
import { FunctionComponent, useEffect, useMemo, useState } from "react"; import { FunctionComponent, useMemo, useState } from "react";
import { Container, Form } from "react-bootstrap"; import { Container, Form } from "react-bootstrap";
import { UseMutationResult } from "react-query"; import { UseMutationResult } from "react-query";
import { AsyncButton, Selector, SelectorOption } from ".."; import { AsyncButton, Selector, SelectorOption } from "..";
import BaseModal, { BaseModalProps } from "./BaseModal";
interface Props { interface Props {
mutation: UseMutationResult<void, unknown, FormType.ModifyItem, unknown>; mutation: UseMutationResult<void, unknown, FormType.ModifyItem, unknown>;
} }
const Editor: FunctionComponent<Props & BaseModalProps> = (props) => { const Editor: FunctionComponent<Props> = ({ mutation }) => {
const { mutation, ...modal } = props;
const { data: profiles } = useLanguageProfiles(); const { data: profiles } = useLanguageProfiles();
const payload = usePayload<Item.Base>(modal.modalKey); const payload = usePayload<Item.Base>();
const { hide } = useModalControl();
const { mutateAsync, isLoading } = mutation; const { mutateAsync, isLoading } = mutation;
const { hide } = useModalControl();
const hasTask = useIsAnyActionRunning(); const hasTask = useIsAnyActionRunning();
const profileOptions = useMemo<SelectorOption<number>[]>( const profileOptions = useMemo<SelectorOption<number>[]>(
@ -33,9 +35,12 @@ const Editor: FunctionComponent<Props & BaseModalProps> = (props) => {
const [id, setId] = useState<Nullable<number>>(payload?.profileId ?? null); const [id, setId] = useState<Nullable<number>>(payload?.profileId ?? null);
useEffect(() => { const Modal = useModal({
setId(payload?.profileId ?? null); closeable: !isLoading,
}, [payload]); onMounted: () => {
setId(payload?.profileId ?? null);
},
});
const footer = ( const footer = (
<AsyncButton <AsyncButton
@ -56,21 +61,14 @@ const Editor: FunctionComponent<Props & BaseModalProps> = (props) => {
return null; return null;
} }
}} }}
onSuccess={() => { onSuccess={() => hide()}
hide();
}}
> >
Save Save
</AsyncButton> </AsyncButton>
); );
return ( return (
<BaseModal <Modal title={payload?.title ?? "Item Editor"} footer={footer}>
closeable={!isLoading}
footer={footer}
title={payload?.title}
{...modal}
>
<Container fluid> <Container fluid>
<Form> <Form>
<Form.Group> <Form.Group>
@ -95,8 +93,8 @@ const Editor: FunctionComponent<Props & BaseModalProps> = (props) => {
</Form.Group> </Form.Group>
</Form> </Form>
</Container> </Container>
</BaseModal> </Modal>
); );
}; };
export default Editor; export default withModal(Editor, "edit");

@ -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 { createAndDispatchTask } from "@/modules/task/utilities";
import { GetItemId, isMovie } from "@/utilities"; import { GetItemId, isMovie } from "@/utilities";
import { import {
@ -10,13 +10,7 @@ import {
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import clsx from "clsx"; import clsx from "clsx";
import { import { FunctionComponent, useCallback, useMemo, useState } from "react";
FunctionComponent,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { import {
Badge, Badge,
Button, Button,
@ -29,7 +23,7 @@ import {
} from "react-bootstrap"; } from "react-bootstrap";
import { UseQueryResult } from "react-query"; import { UseQueryResult } from "react-query";
import { Column } from "react-table"; import { Column } from "react-table";
import { BaseModal, BaseModalProps, LoadingIndicator, PageTable } from ".."; import { LoadingIndicator, PageTable } from "..";
import Language from "../bazarr/Language"; import Language from "../bazarr/Language";
type SupportType = Item.Movie | Item.Episode; type SupportType = Item.Movie | Item.Episode;
@ -41,24 +35,15 @@ interface Props<T extends SupportType> {
) => UseQueryResult<SearchResultType[] | undefined, unknown>; ) => UseQueryResult<SearchResultType[] | undefined, unknown>;
} }
export function ManualSearchModal<T extends SupportType>( function ManualSearchView<T extends SupportType>(props: Props<T>) {
props: Props<T> & BaseModalProps const { download, query: useSearch } = props;
) {
const { download, query: useSearch, ...modal } = props;
const item = usePayload<T>(modal.modalKey); const item = usePayload<T>();
const itemId = useMemo(() => GetItemId(item ?? {}), [item]); const itemId = useMemo(() => GetItemId(item ?? {}), [item]);
const [id, setId] = useState<number | undefined>(undefined); 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 results = useSearch(id);
const isStale = results.data === undefined; 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(() => { const title = useMemo(() => {
let title = "Unknown"; let title = "Unknown";
@ -246,19 +225,39 @@ export function ManualSearchModal<T extends SupportType>(
return `Search - ${title}`; return `Search - ${title}`;
}, [item]); }, [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 ( return (
<BaseModal <Modal title={title} footer={footer}>
closeable={results.isFetching === false}
size="xl"
title={title}
footer={footer}
{...modal}
>
{content()} {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[] }> = ({ const StateIcon: FunctionComponent<{ matches: string[]; dont: string[] }> = ({
matches, matches,
dont, dont,

@ -1,21 +1,18 @@
import { useMovieSubtitleModification } from "@/apis/hooks"; 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 { createTask, dispatchTask } from "@/modules/task/utilities";
import { import {
useLanguageProfileBy, useLanguageProfileBy,
useProfileItemsToLanguages, useProfileItemsToLanguages,
} from "@/utilities/languages"; } from "@/utilities/languages";
import { FunctionComponent, useCallback } from "react"; import { FunctionComponent, useCallback } from "react";
import { BaseModalProps } from "./BaseModal"; import SubtitleUploader, {
import SubtitleUploadModal, {
PendingSubtitle, PendingSubtitle,
Validator, Validator,
} from "./SubtitleUploadModal"; } from "./SubtitleUploadModal";
const MovieUploadModal: FunctionComponent<BaseModalProps> = (props) => { const MovieUploadModal: FunctionComponent = () => {
const modal = props; const payload = usePayload<Item.Movie>();
const payload = usePayload<Item.Movie>(modal.modalKey);
const profile = useLanguageProfileBy(payload?.profileId); const profile = useLanguageProfileBy(payload?.profileId);
@ -87,7 +84,7 @@ const MovieUploadModal: FunctionComponent<BaseModalProps> = (props) => {
); );
return ( return (
<SubtitleUploadModal <SubtitleUploader
hideAllLanguages hideAllLanguages
initial={{ forced: false }} initial={{ forced: false }}
availableLanguages={availableLanguages} availableLanguages={availableLanguages}
@ -95,9 +92,8 @@ const MovieUploadModal: FunctionComponent<BaseModalProps> = (props) => {
upload={upload} upload={upload}
update={update} update={update}
validate={validate} validate={validate}
{...modal} ></SubtitleUploader>
></SubtitleUploadModal>
); );
}; };
export default MovieUploadModal; export default withModal(MovieUploadModal, "movie-upload");

@ -1,6 +1,6 @@
import { useEpisodeSubtitleModification } from "@/apis/hooks"; import { useEpisodeSubtitleModification } from "@/apis/hooks";
import api from "@/apis/raw"; 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 { createTask, dispatchTask } from "@/modules/task/utilities";
import { import {
useLanguageProfileBy, useLanguageProfileBy,
@ -9,8 +9,7 @@ import {
import { FunctionComponent, useCallback, useMemo } from "react"; import { FunctionComponent, useCallback, useMemo } from "react";
import { Column } from "react-table"; import { Column } from "react-table";
import { Selector, SelectorOption } from "../inputs"; import { Selector, SelectorOption } from "../inputs";
import { BaseModalProps } from "./BaseModal"; import SubtitleUploader, {
import SubtitleUploadModal, {
PendingSubtitle, PendingSubtitle,
useRowMutation, useRowMutation,
Validator, Validator,
@ -24,11 +23,8 @@ interface SeriesProps {
episodes: readonly Item.Episode[]; episodes: readonly Item.Episode[];
} }
const SeriesUploadModal: FunctionComponent<SeriesProps & BaseModalProps> = ({ const SeriesUploadModal: FunctionComponent<SeriesProps> = ({ episodes }) => {
episodes, const payload = usePayload<Item.Series>();
...modal
}) => {
const payload = usePayload<Item.Series>(modal.modalKey);
const profile = useLanguageProfileBy(payload?.profileId); const profile = useLanguageProfileBy(payload?.profileId);
@ -165,16 +161,15 @@ const SeriesUploadModal: FunctionComponent<SeriesProps & BaseModalProps> = ({
); );
return ( return (
<SubtitleUploadModal <SubtitleUploader
columns={columns} columns={columns}
initial={{ instance: null }} initial={{ instance: null }}
availableLanguages={availableLanguages} availableLanguages={availableLanguages}
upload={upload} upload={upload}
update={update} update={update}
validate={validate} validate={validate}
{...modal} ></SubtitleUploader>
></SubtitleUploadModal>
); );
}; };
export default SeriesUploadModal; export default withModal(SeriesUploadModal, "series-upload");

@ -1,5 +1,10 @@
import { useSubtitleAction } from "@/apis/hooks"; 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 { createTask, dispatchTask } from "@/modules/task/utilities";
import { isMovie, submodProcessColor } from "@/utilities"; import { isMovie, submodProcessColor } from "@/utilities";
import { LOG } from "@/utilities/console"; import { LOG } from "@/utilities/console";
@ -45,7 +50,6 @@ import {
} from ".."; } from "..";
import Language from "../bazarr/Language"; import Language from "../bazarr/Language";
import { useCustomSelection } from "../tables/plugins"; import { useCustomSelection } from "../tables/plugins";
import BaseModal, { BaseModalProps } from "./BaseModal";
import { availableTranslation, colorOptions } from "./toolOptions"; import { availableTranslation, colorOptions } from "./toolOptions";
type SupportType = Item.Episode | Item.Movie; type SupportType = Item.Episode | Item.Movie;
@ -77,12 +81,11 @@ interface ToolModalProps {
) => void; ) => void;
} }
const AddColorModal: FunctionComponent<BaseModalProps & ToolModalProps> = ( const ColorTool: FunctionComponent<ToolModalProps> = ({ process }) => {
props
) => {
const { process, ...modal } = props;
const [selection, setSelection] = useState<Nullable<string>>(null); const [selection, setSelection] = useState<Nullable<string>>(null);
const Modal = useModal();
const submit = useCallback(() => { const submit = useCallback(() => {
if (selection) { if (selection) {
const action = submodProcessColor(selection); const action = submodProcessColor(selection);
@ -90,31 +93,29 @@ const AddColorModal: FunctionComponent<BaseModalProps & ToolModalProps> = (
} }
}, [selection, process]); }, [selection, process]);
const footer = useMemo( const footer = (
() => ( <Button disabled={selection === null} onClick={submit}>
<Button disabled={selection === null} onClick={submit}> Save
Save </Button>
</Button>
),
[selection, submit]
); );
return ( return (
<BaseModal title="Choose Color" footer={footer} {...modal}> <Modal title="Choose Color" footer={footer}>
<Selector options={colorOptions} onChange={setSelection}></Selector> <Selector options={colorOptions} onChange={setSelection}></Selector>
</BaseModal> </Modal>
); );
}; };
const FrameRateModal: FunctionComponent<BaseModalProps & ToolModalProps> = ( const ColorToolModal = withModal(ColorTool, "color-tool");
props
) => {
const { process, ...modal } = props;
const FrameRateTool: FunctionComponent<ToolModalProps> = ({ process }) => {
const [from, setFrom] = useState<Nullable<number>>(null); const [from, setFrom] = useState<Nullable<number>>(null);
const [to, setTo] = useState<Nullable<number>>(null); const [to, setTo] = useState<Nullable<number>>(null);
const canSave = from !== null && to !== null && from !== to; const canSave = from !== null && to !== null && from !== to;
const Modal = useModal();
const submit = useCallback(() => { const submit = useCallback(() => {
if (canSave) { if (canSave) {
const action = submodProcessFrameRate(from, to); const action = submodProcessFrameRate(from, to);
@ -129,7 +130,7 @@ const FrameRateModal: FunctionComponent<BaseModalProps & ToolModalProps> = (
); );
return ( return (
<BaseModal title="Change Frame Rate" footer={footer} {...modal}> <Modal title="Change Frame Rate" footer={footer}>
<InputGroup className="px-2"> <InputGroup className="px-2">
<Form.Control <Form.Control
placeholder="From" placeholder="From"
@ -156,20 +157,20 @@ const FrameRateModal: FunctionComponent<BaseModalProps & ToolModalProps> = (
}} }}
></Form.Control> ></Form.Control>
</InputGroup> </InputGroup>
</BaseModal> </Modal>
); );
}; };
const AdjustTimesModal: FunctionComponent<BaseModalProps & ToolModalProps> = ( const FrameRateModal = withModal(FrameRateTool, "frame-rate-tool");
props
) => {
const { process, ...modal } = props;
const TimeAdjustmentTool: FunctionComponent<ToolModalProps> = ({ process }) => {
const [isPlus, setPlus] = useState(true); const [isPlus, setPlus] = useState(true);
const [offset, setOffset] = useState<[number, number, number, number]>([ const [offset, setOffset] = useState<[number, number, number, number]>([
0, 0, 0, 0, 0, 0, 0, 0,
]); ]);
const Modal = useModal();
const updateOffset = useCallback( const updateOffset = useCallback(
(idx: number): ChangeEventHandler<HTMLInputElement> => { (idx: number): ChangeEventHandler<HTMLInputElement> => {
return (e) => { return (e) => {
@ -200,17 +201,14 @@ const AdjustTimesModal: FunctionComponent<BaseModalProps & ToolModalProps> = (
} }
}, [process, canSave, offset, isPlus]); }, [process, canSave, offset, isPlus]);
const footer = useMemo( const footer = (
() => ( <Button disabled={!canSave} onClick={submit}>
<Button disabled={!canSave} onClick={submit}> Save
Save </Button>
</Button>
),
[submit, canSave]
); );
return ( return (
<BaseModal title="Adjust Times" footer={footer} {...modal}> <Modal title="Adjust Times" footer={footer}>
<InputGroup> <InputGroup>
<InputGroup.Prepend> <InputGroup.Prepend>
<Button <Button
@ -242,14 +240,13 @@ const AdjustTimesModal: FunctionComponent<BaseModalProps & ToolModalProps> = (
onChange={updateOffset(3)} onChange={updateOffset(3)}
></Form.Control> ></Form.Control>
</InputGroup> </InputGroup>
</BaseModal> </Modal>
); );
}; };
const TranslateModal: FunctionComponent<BaseModalProps & ToolModalProps> = ({ const TimeAdjustmentModal = withModal(TimeAdjustmentTool, "time-adjust-tool");
process,
...modal const TranslationTool: FunctionComponent<ToolModalProps> = ({ process }) => {
}) => {
const { data: languages } = useEnabledLanguages(); const { data: languages } = useEnabledLanguages();
const available = useMemo( const available = useMemo(
@ -257,6 +254,8 @@ const TranslateModal: FunctionComponent<BaseModalProps & ToolModalProps> = ({
[languages] [languages]
); );
const Modal = useModal();
const [selectedLanguage, setLanguage] = const [selectedLanguage, setLanguage] =
useState<Nullable<Language.Info>>(null); useState<Nullable<Language.Info>>(null);
@ -266,17 +265,13 @@ const TranslateModal: FunctionComponent<BaseModalProps & ToolModalProps> = ({
} }
}, [selectedLanguage, process]); }, [selectedLanguage, process]);
const footer = useMemo( const footer = (
() => ( <Button disabled={!selectedLanguage} onClick={submit}>
<Button disabled={!selectedLanguage} onClick={submit}> Translate
Translate </Button>
</Button>
),
[submit, selectedLanguage]
); );
return ( return (
<BaseModal title="Translate to" footer={footer} {...modal}> <Modal title="Translation" footer={footer}>
<Form.Label> <Form.Label>
Enabled languages not listed here are unsupported by Google Translate. Enabled languages not listed here are unsupported by Google Translate.
</Form.Label> </Form.Label>
@ -284,18 +279,21 @@ const TranslateModal: FunctionComponent<BaseModalProps & ToolModalProps> = ({
options={available} options={available}
onChange={setLanguage} onChange={setLanguage}
></LanguageSelector> ></LanguageSelector>
</BaseModal> </Modal>
); );
}; };
const TranslationModal = withModal(TranslationTool, "translate-tool");
const CanSelectSubtitle = (item: TableColumnType) => { const CanSelectSubtitle = (item: TableColumnType) => {
return item.path.endsWith(".srt"); return item.path.endsWith(".srt");
}; };
const STM: FunctionComponent<BaseModalProps> = ({ ...props }) => { const STM: FunctionComponent = () => {
const payload = usePayload<SupportType[]>(props.modalKey); const payload = usePayload<SupportType[]>();
const [selections, setSelections] = useState<TableColumnType[]>([]); const [selections, setSelections] = useState<TableColumnType[]>([]);
const Modal = useModal({ size: "xl" });
const { hide } = useModalControl(); const { hide } = useModalControl();
const { mutateAsync } = useSubtitleAction(); const { mutateAsync } = useSubtitleAction();
@ -303,8 +301,7 @@ const STM: FunctionComponent<BaseModalProps> = ({ ...props }) => {
const process = useCallback( const process = useCallback(
(action: string, override?: Partial<FormType.ModifySubtitle>) => { (action: string, override?: Partial<FormType.ModifySubtitle>) => {
LOG("info", "executing action", action); LOG("info", "executing action", action);
hide(props.modalKey); hide();
const tasks = selections.map((s) => { const tasks = selections.map((s) => {
const form: FormType.ModifySubtitle = { const form: FormType.ModifySubtitle = {
id: s.id, id: s.id,
@ -318,7 +315,7 @@ const STM: FunctionComponent<BaseModalProps> = ({ ...props }) => {
dispatchTask(tasks, "modify-subtitles"); dispatchTask(tasks, "modify-subtitles");
}, },
[hide, props.modalKey, selections, mutateAsync] [hide, selections, mutateAsync]
); );
const { show } = useModalControl(); const { show } = useModalControl();
@ -383,92 +380,74 @@ const STM: FunctionComponent<BaseModalProps> = ({ ...props }) => {
const plugins = [useRowSelect, useCustomSelection]; const plugins = [useRowSelect, useCustomSelection];
const footer = useMemo( const footer = (
() => ( <Dropdown as={ButtonGroup} onSelect={(k) => k && process(k)}>
<Dropdown as={ButtonGroup} onSelect={(k) => k && process(k)}> <ActionButton
<ActionButton size="sm"
size="sm" disabled={selections.length === 0}
disabled={selections.length === 0} icon={faPlay}
icon={faPlay} onClick={() => process("sync")}
onClick={() => process("sync")} >
> Sync
Sync </ActionButton>
</ActionButton> <Dropdown.Toggle
<Dropdown.Toggle disabled={selections.length === 0}
disabled={selections.length === 0} split
split variant="light"
variant="light" size="sm"
size="sm" className="px-2"
className="px-2" ></Dropdown.Toggle>
></Dropdown.Toggle> <Dropdown.Menu>
<Dropdown.Menu> <Dropdown.Item eventKey="remove_HI">
<Dropdown.Item eventKey="remove_HI"> <ActionButtonItem icon={faDeaf}>Remove HI Tags</ActionButtonItem>
<ActionButtonItem icon={faDeaf}>Remove HI Tags</ActionButtonItem> </Dropdown.Item>
</Dropdown.Item> <Dropdown.Item eventKey="remove_tags">
<Dropdown.Item eventKey="remove_tags"> <ActionButtonItem icon={faCode}>Remove Style Tags</ActionButtonItem>
<ActionButtonItem icon={faCode}>Remove Style Tags</ActionButtonItem> </Dropdown.Item>
</Dropdown.Item> <Dropdown.Item eventKey="OCR_fixes">
<Dropdown.Item eventKey="OCR_fixes"> <ActionButtonItem icon={faImage}>OCR Fixes</ActionButtonItem>
<ActionButtonItem icon={faImage}>OCR Fixes</ActionButtonItem> </Dropdown.Item>
</Dropdown.Item> <Dropdown.Item eventKey="common">
<Dropdown.Item eventKey="common"> <ActionButtonItem icon={faMagic}>Common Fixes</ActionButtonItem>
<ActionButtonItem icon={faMagic}>Common Fixes</ActionButtonItem> </Dropdown.Item>
</Dropdown.Item> <Dropdown.Item eventKey="fix_uppercase">
<Dropdown.Item eventKey="fix_uppercase"> <ActionButtonItem icon={faTextHeight}>Fix Uppercase</ActionButtonItem>
<ActionButtonItem icon={faTextHeight}> </Dropdown.Item>
Fix Uppercase <Dropdown.Item eventKey="reverse_rtl">
</ActionButtonItem> <ActionButtonItem icon={faExchangeAlt}>Reverse RTL</ActionButtonItem>
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item eventKey="reverse_rtl"> <Dropdown.Item onSelect={() => show(ColorToolModal)}>
<ActionButtonItem icon={faExchangeAlt}> <ActionButtonItem icon={faPaintBrush}>Add Color</ActionButtonItem>
Reverse RTL </Dropdown.Item>
</ActionButtonItem> <Dropdown.Item onSelect={() => show(FrameRateModal)}>
</Dropdown.Item> <ActionButtonItem icon={faFilm}>Change Frame Rate</ActionButtonItem>
<Dropdown.Item onSelect={() => show("add-color")}> </Dropdown.Item>
<ActionButtonItem icon={faPaintBrush}>Add Color</ActionButtonItem> <Dropdown.Item onSelect={() => show(TimeAdjustmentModal)}>
</Dropdown.Item> <ActionButtonItem icon={faClock}>Adjust Times</ActionButtonItem>
<Dropdown.Item onSelect={() => show("change-frame-rate")}> </Dropdown.Item>
<ActionButtonItem icon={faFilm}>Change Frame Rate</ActionButtonItem> <Dropdown.Item onSelect={() => show(TranslationModal)}>
</Dropdown.Item> <ActionButtonItem icon={faLanguage}>Translate</ActionButtonItem>
<Dropdown.Item onSelect={() => show("adjust-times")}> </Dropdown.Item>
<ActionButtonItem icon={faClock}>Adjust Times</ActionButtonItem> </Dropdown.Menu>
</Dropdown.Item> </Dropdown>
<Dropdown.Item onSelect={() => show("translate-sub")}>
<ActionButtonItem icon={faLanguage}>Translate</ActionButtonItem>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
),
[selections.length, process, show]
); );
return ( return (
<> <Modal title="Subtitle Tools" footer={footer}>
<BaseModal title={"Subtitle Tools"} footer={footer} {...props}> <SimpleTable
<SimpleTable emptyText="No External Subtitles Found"
emptyText="No External Subtitles Found" plugins={plugins}
plugins={plugins} columns={columns}
columns={columns} onSelect={setSelections}
onSelect={setSelections} canSelect={CanSelectSubtitle}
canSelect={CanSelectSubtitle} data={data}
data={data} ></SimpleTable>
></SimpleTable> <ColorToolModal process={process}></ColorToolModal>
</BaseModal> <FrameRateModal process={process}></FrameRateModal>
<AddColorModal process={process} modalKey="add-color"></AddColorModal> <TimeAdjustmentModal process={process}></TimeAdjustmentModal>
<FrameRateModal <TranslationModal process={process}></TranslationModal>
process={process} </Modal>
modalKey="change-frame-rate"
></FrameRateModal>
<AdjustTimesModal
process={process}
modalKey="adjust-times"
></AdjustTimesModal>
<TranslateModal
process={process}
modalKey="translate-sub"
></TranslateModal>
</>
); );
}; };
export default STM; export default withModal(STM, "subtitle-tools");

@ -1,4 +1,4 @@
import { useModalControl } from "@/modules/redux/hooks/modal"; import { useModal, useModalControl } from "@/modules/modals";
import { BuildKey } from "@/utilities"; import { BuildKey } from "@/utilities";
import { LOG } from "@/utilities/console"; import { LOG } from "@/utilities/console";
import { import {
@ -23,7 +23,6 @@ import { Column } from "react-table";
import { LanguageSelector, MessageIcon } from ".."; import { LanguageSelector, MessageIcon } from "..";
import { FileForm } from "../inputs"; import { FileForm } from "../inputs";
import { SimpleTable } from "../tables"; import { SimpleTable } from "../tables";
import BaseModal, { BaseModalProps } from "./BaseModal";
type ModifyFn<T> = (index: number, info?: PendingSubtitle<T>) => void; type ModifyFn<T> = (index: number, info?: PendingSubtitle<T>) => void;
@ -59,10 +58,7 @@ interface Props<T = unknown> {
hideAllLanguages?: boolean; hideAllLanguages?: boolean;
} }
type ComponentProps<T> = Props<T> & function SubtitleUploader<T>(props: Props<T>) {
Omit<BaseModalProps, "footer" | "title" | "size">;
function SubtitleUploadModal<T>(props: ComponentProps<T>) {
const { const {
initial, initial,
columns, columns,
@ -73,10 +69,16 @@ function SubtitleUploadModal<T>(props: ComponentProps<T>) {
hideAllLanguages, hideAllLanguages,
} = props; } = props;
const { hide } = useModalControl();
const [pending, setPending] = useState<PendingSubtitle<T>[]>([]); 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 fileList = useMemo(() => pending.map((v) => v.file), [pending]);
const initialRef = useRef(initial); const initialRef = useRef(initial);
@ -281,8 +283,6 @@ function SubtitleUploadModal<T>(props: ComponentProps<T>) {
[columns, availableLanguages] [columns, availableLanguages]
); );
const showTable = pending.length > 0;
const canUpload = useMemo( const canUpload = useMemo(
() => () =>
pending.length > 0 && pending.length > 0 &&
@ -332,12 +332,7 @@ function SubtitleUploadModal<T>(props: ComponentProps<T>) {
); );
return ( return (
<BaseModal <Modal title="Update Subtitles" footer={footer}>
size={showTable ? "xl" : "lg"}
title="Upload Subtitles"
footer={footer}
{...props}
>
<Container fluid className="flex-column"> <Container fluid className="flex-column">
<Form> <Form>
<Form.Group> <Form.Group>
@ -360,8 +355,8 @@ function SubtitleUploadModal<T>(props: ComponentProps<T>) {
</RowContext.Provider> </RowContext.Provider>
</div> </div>
</Container> </Container>
</BaseModal> </Modal>
); );
} }
export default SubtitleUploadModal; export default SubtitleUploader;

@ -1,4 +1,3 @@
export * from "./BaseModal";
export * from "./HistoryModal"; export * from "./HistoryModal";
export { default as ItemEditorModal } from "./ItemEditorModal"; export { default as ItemEditorModal } from "./ItemEditorModal";
export { default as MovieUploadModal } from "./MovieUploadModal"; export { default as MovieUploadModal } from "./MovieUploadModal";

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

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

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

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

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

@ -0,0 +1,3 @@
export * from "./components";
export * from "./hooks";
export { default as withModal } from "./WithModal";

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

@ -5,14 +5,14 @@ import {
useSeriesById, useSeriesById,
useSeriesModification, useSeriesModification,
} from "@/apis/hooks"; } from "@/apis/hooks";
import { ContentHeader, LoadingIndicator } from "@/components";
import ItemOverview from "@/components/ItemOverview";
import { import {
ContentHeader,
ItemEditorModal, ItemEditorModal,
LoadingIndicator,
SeriesUploadModal, SeriesUploadModal,
} from "@/components"; SubtitleToolModal,
import ItemOverview from "@/components/ItemOverview"; } from "@/components/modals";
import { useModalControl } from "@/modules/redux/hooks/modal"; import { useModalControl } from "@/modules/modals";
import { createAndDispatchTask } from "@/modules/task/utilities"; import { createAndDispatchTask } from "@/modules/task/utilities";
import { useLanguageProfileBy } from "@/utilities/languages"; import { useLanguageProfileBy } from "@/utilities/languages";
import { import {
@ -109,7 +109,7 @@ const SeriesEpisodesView: FunctionComponent = () => {
<ContentHeader.Button <ContentHeader.Button
disabled={series.episodeFileCount === 0 || !available || hasTask} disabled={series.episodeFileCount === 0 || !available || hasTask}
icon={faBriefcase} icon={faBriefcase}
onClick={() => show("tools", episodes)} onClick={() => show(SubtitleToolModal, episodes)}
> >
Tools Tools
</ContentHeader.Button> </ContentHeader.Button>
@ -120,14 +120,14 @@ const SeriesEpisodesView: FunctionComponent = () => {
!available !available
} }
icon={faCloudUploadAlt} icon={faCloudUploadAlt}
onClick={() => show("upload", series)} onClick={() => show(SeriesUploadModal, series)}
> >
Upload Upload
</ContentHeader.Button> </ContentHeader.Button>
<ContentHeader.Button <ContentHeader.Button
icon={faWrench} icon={faWrench}
disabled={hasTask} disabled={hasTask}
onClick={() => show("edit", series)} onClick={() => show(ItemEditorModal, series)}
> >
Edit Series Edit Series
</ContentHeader.Button> </ContentHeader.Button>
@ -158,11 +158,8 @@ const SeriesEpisodesView: FunctionComponent = () => {
></Table> ></Table>
)} )}
</Row> </Row>
<ItemEditorModal modalKey="edit" mutation={mutation}></ItemEditorModal> <ItemEditorModal mutation={mutation}></ItemEditorModal>
<SeriesUploadModal <SeriesUploadModal episodes={episodes ?? []}></SeriesUploadModal>
modalKey="upload"
episodes={episodes ?? []}
></SeriesUploadModal>
</Container> </Container>
); );
}; };

@ -1,14 +1,9 @@
import { useDownloadEpisodeSubtitles, useEpisodesProvider } from "@/apis/hooks"; import { useDownloadEpisodeSubtitles, useEpisodesProvider } from "@/apis/hooks";
import { import { ActionButton, GroupTable, TextPopover } from "@/components";
ActionButton, import { EpisodeHistoryModal, SubtitleToolModal } from "@/components/modals";
EpisodeHistoryModal, import { EpisodeSearchModal } from "@/components/modals/ManualSearchModal";
GroupTable, import { useModalControl } from "@/modules/modals";
SubtitleToolModal,
TextPopover,
} from "@/components";
import { ManualSearchModal } from "@/components/modals/ManualSearchModal";
import { useShowOnlyDesired } from "@/modules/redux/hooks"; import { useShowOnlyDesired } from "@/modules/redux/hooks";
import { useModalControl } from "@/modules/redux/hooks/modal";
import { BuildKey, filterSubtitleBy } from "@/utilities"; import { BuildKey, filterSubtitleBy } from "@/utilities";
import { useProfileItemsToLanguages } from "@/utilities/languages"; import { useProfileItemsToLanguages } from "@/utilities/languages";
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons"; import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
@ -166,21 +161,21 @@ const Table: FunctionComponent<Props> = ({
icon={faUser} icon={faUser}
disabled={series?.profileId === null || disabled} disabled={series?.profileId === null || disabled}
onClick={() => { onClick={() => {
show("manual-search", row.original); show(EpisodeSearchModal, row.original);
}} }}
></ActionButton> ></ActionButton>
<ActionButton <ActionButton
icon={faHistory} icon={faHistory}
disabled={disabled} disabled={disabled}
onClick={() => { onClick={() => {
show("history", row.original); show(EpisodeHistoryModal, row.original);
}} }}
></ActionButton> ></ActionButton>
<ActionButton <ActionButton
icon={faBriefcase} icon={faBriefcase}
disabled={disabled} disabled={disabled}
onClick={() => { onClick={() => {
show("tools", [row.original]); show(SubtitleToolModal, [row.original]);
}} }}
></ActionButton> ></ActionButton>
</ButtonGroup> </ButtonGroup>
@ -214,13 +209,12 @@ const Table: FunctionComponent<Props> = ({
}} }}
emptyText="No Episode Found For This Series" emptyText="No Episode Found For This Series"
></GroupTable> ></GroupTable>
<SubtitleToolModal modalKey="tools" size="lg"></SubtitleToolModal> <SubtitleToolModal></SubtitleToolModal>
<EpisodeHistoryModal modalKey="history" size="lg"></EpisodeHistoryModal> <EpisodeHistoryModal></EpisodeHistoryModal>
<ManualSearchModal <EpisodeSearchModal
modalKey="manual-search"
download={download} download={download}
query={useEpisodesProvider} query={useEpisodesProvider}
></ManualSearchModal> ></EpisodeSearchModal>
</> </>
); );
}; };

@ -8,17 +8,16 @@ import {
useMovieById, useMovieById,
useMovieModification, useMovieModification,
} from "@/apis/hooks/movies"; } from "@/apis/hooks/movies";
import { ContentHeader, LoadingIndicator } from "@/components";
import ItemOverview from "@/components/ItemOverview";
import { import {
ContentHeader,
ItemEditorModal, ItemEditorModal,
LoadingIndicator,
MovieHistoryModal, MovieHistoryModal,
MovieUploadModal, MovieUploadModal,
SubtitleToolModal, SubtitleToolModal,
} from "@/components"; } from "@/components/modals";
import ItemOverview from "@/components/ItemOverview"; import { MovieSearchModal } from "@/components/modals/ManualSearchModal";
import { ManualSearchModal } from "@/components/modals/ManualSearchModal"; import { useModalControl } from "@/modules/modals";
import { useModalControl } from "@/modules/redux/hooks/modal";
import { createAndDispatchTask } from "@/modules/task/utilities"; import { createAndDispatchTask } from "@/modules/task/utilities";
import { useLanguageProfileBy } from "@/utilities/languages"; import { useLanguageProfileBy } from "@/utilities/languages";
import { import {
@ -122,20 +121,20 @@ const MovieDetailView: FunctionComponent = () => {
<ContentHeader.Button <ContentHeader.Button
icon={faUser} icon={faUser}
disabled={movie.profileId === null || hasTask} disabled={movie.profileId === null || hasTask}
onClick={() => show("manual-search", movie)} onClick={() => show(MovieSearchModal, movie)}
> >
Manual Manual
</ContentHeader.Button> </ContentHeader.Button>
<ContentHeader.Button <ContentHeader.Button
icon={faHistory} icon={faHistory}
onClick={() => show("history", movie)} onClick={() => show(MovieHistoryModal, movie)}
> >
History History
</ContentHeader.Button> </ContentHeader.Button>
<ContentHeader.Button <ContentHeader.Button
icon={faToolbox} icon={faToolbox}
disabled={hasTask} disabled={hasTask}
onClick={() => show("tools", [movie])} onClick={() => show(SubtitleToolModal, [movie])}
> >
Tools Tools
</ContentHeader.Button> </ContentHeader.Button>
@ -145,14 +144,14 @@ const MovieDetailView: FunctionComponent = () => {
<ContentHeader.Button <ContentHeader.Button
disabled={!allowEdit || movie.profileId === null || hasTask} disabled={!allowEdit || movie.profileId === null || hasTask}
icon={faCloudUploadAlt} icon={faCloudUploadAlt}
onClick={() => show("upload", movie)} onClick={() => show(MovieUploadModal, movie)}
> >
Upload Upload
</ContentHeader.Button> </ContentHeader.Button>
<ContentHeader.Button <ContentHeader.Button
icon={faWrench} icon={faWrench}
disabled={hasTask} disabled={hasTask}
onClick={() => show("edit", movie)} onClick={() => show(ItemEditorModal, movie)}
> >
Edit Movie Edit Movie
</ContentHeader.Button> </ContentHeader.Button>
@ -174,15 +173,14 @@ const MovieDetailView: FunctionComponent = () => {
<Row> <Row>
<Table movie={movie} profile={profile} disabled={hasTask}></Table> <Table movie={movie} profile={profile} disabled={hasTask}></Table>
</Row> </Row>
<ItemEditorModal modalKey="edit" mutation={mutation}></ItemEditorModal> <ItemEditorModal mutation={mutation}></ItemEditorModal>
<SubtitleToolModal modalKey="tools" size="lg"></SubtitleToolModal> <SubtitleToolModal></SubtitleToolModal>
<MovieHistoryModal modalKey="history" size="lg"></MovieHistoryModal> <MovieHistoryModal></MovieHistoryModal>
<MovieUploadModal modalKey="upload" size="lg"></MovieUploadModal> <MovieUploadModal></MovieUploadModal>
<ManualSearchModal <MovieSearchModal
modalKey="manual-search"
download={download} download={download}
query={useMoviesProvider} query={useMoviesProvider}
></ManualSearchModal> ></MovieSearchModal>
</Container> </Container>
); );
}; };

@ -1,9 +1,10 @@
import { useMovieModification, useMoviesPagination } from "@/apis/hooks"; import { useMovieModification, useMoviesPagination } from "@/apis/hooks";
import { ActionBadge, ItemEditorModal, TextPopover } from "@/components"; import { ActionBadge, TextPopover } from "@/components";
import Language from "@/components/bazarr/Language"; import Language from "@/components/bazarr/Language";
import LanguageProfile from "@/components/bazarr/LanguageProfile"; import LanguageProfile from "@/components/bazarr/LanguageProfile";
import { ItemEditorModal } from "@/components/modals";
import ItemView from "@/components/views/ItemView"; import ItemView from "@/components/views/ItemView";
import { useModalControl } from "@/modules/redux/hooks/modal"; import { useModalControl } from "@/modules/modals";
import { BuildKey } from "@/utilities"; import { BuildKey } from "@/utilities";
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons"; import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons"; import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons";
@ -90,7 +91,7 @@ const MovieView: FunctionComponent = () => {
return ( return (
<ActionBadge <ActionBadge
icon={faWrench} icon={faWrench}
onClick={() => show("edit", row.original)} onClick={() => show(ItemEditorModal, row.original)}
></ActionBadge> ></ActionBadge>
); );
}, },
@ -105,7 +106,7 @@ const MovieView: FunctionComponent = () => {
<title>Movies - Bazarr</title> <title>Movies - Bazarr</title>
</Helmet> </Helmet>
<ItemView query={query} columns={columns}></ItemView> <ItemView query={query} columns={columns}></ItemView>
<ItemEditorModal modalKey="edit" mutation={mutation}></ItemEditorModal> <ItemEditorModal mutation={mutation}></ItemEditorModal>
</Container> </Container>
); );
}; };

@ -1,8 +1,9 @@
import { useSeriesModification, useSeriesPagination } from "@/apis/hooks"; import { useSeriesModification, useSeriesPagination } from "@/apis/hooks";
import { ActionBadge, ItemEditorModal } from "@/components"; import { ActionBadge } from "@/components";
import LanguageProfile from "@/components/bazarr/LanguageProfile"; import LanguageProfile from "@/components/bazarr/LanguageProfile";
import { ItemEditorModal } from "@/components/modals";
import ItemView from "@/components/views/ItemView"; import ItemView from "@/components/views/ItemView";
import { useModalControl } from "@/modules/redux/hooks/modal"; import { useModalControl } from "@/modules/modals";
import { BuildKey } from "@/utilities"; import { BuildKey } from "@/utilities";
import { faWrench } from "@fortawesome/free-solid-svg-icons"; import { faWrench } from "@fortawesome/free-solid-svg-icons";
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
@ -92,7 +93,7 @@ const SeriesView: FunctionComponent = () => {
return ( return (
<ActionBadge <ActionBadge
icon={faWrench} icon={faWrench}
onClick={() => show("edit", original)} onClick={() => show(ItemEditorModal, original)}
></ActionBadge> ></ActionBadge>
); );
}, },
@ -107,7 +108,7 @@ const SeriesView: FunctionComponent = () => {
<title>Series - Bazarr</title> <title>Series - Bazarr</title>
</Helmet> </Helmet>
<ItemView query={query} columns={columns}></ItemView> <ItemView query={query} columns={columns}></ItemView>
<ItemEditorModal modalKey="edit" mutation={mutation}></ItemEditorModal> <ItemEditorModal mutation={mutation}></ItemEditorModal>
</Container> </Container>
); );
}; };

@ -1,14 +1,17 @@
import { import {
ActionButton, ActionButton,
BaseModal,
BaseModalProps,
Chips, Chips,
LanguageSelector, LanguageSelector,
Selector, Selector,
SelectorOption, SelectorOption,
SimpleTable, SimpleTable,
} from "@/components"; } from "@/components";
import { useModalControl, usePayload } from "@/modules/redux/hooks/modal"; import {
useModal,
useModalControl,
usePayload,
withModal,
} from "@/modules/modals";
import { BuildKey } from "@/utilities"; import { BuildKey } from "@/utilities";
import { LOG } from "@/utilities/console"; import { LOG } from "@/utilities/console";
import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { faTrash } from "@fortawesome/free-solid-svg-icons";
@ -17,7 +20,6 @@ import {
FunctionComponent, FunctionComponent,
useCallback, useCallback,
useContext, useContext,
useEffect,
useMemo, useMemo,
useState, useState,
} from "react"; } from "react";
@ -53,12 +55,8 @@ function createDefaultProfile(): Language.Profile {
}; };
} }
const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = ( const LanguagesProfileModal: FunctionComponent<Props> = ({ update }) => {
props const profile = usePayload<Language.Profile>();
) => {
const { update, ...modal } = props;
const profile = usePayload<Language.Profile>(modal.modalKey);
const { hide } = useModalControl(); const { hide } = useModalControl();
@ -66,13 +64,12 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = (
const [current, setProfile] = useState(createDefaultProfile); const [current, setProfile] = useState(createDefaultProfile);
useEffect(() => { const Modal = useModal({
if (profile) { size: "lg",
setProfile(profile); onMounted: () => {
} else { setProfile(profile ?? createDefaultProfile);
setProfile(createDefaultProfile); },
} });
}, [profile]);
const cutoff: SelectorOption<number>[] = useMemo(() => { const cutoff: SelectorOption<number>[] = useMemo(() => {
const options = [...cutoffOptions]; const options = [...cutoffOptions];
@ -134,18 +131,6 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = (
const canSave = current.name.length > 0 && current.items.length > 0; 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>[]>( const columns = useMemo<Column<Language.ProfileItem>[]>(
() => [ () => [
{ {
@ -253,8 +238,20 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = (
[languages] [languages]
); );
const footer = (
<Button
disabled={!canSave}
onClick={() => {
hide();
update(current);
}}
>
Save
</Button>
);
return ( return (
<BaseModal size="lg" title="Languages Profile" footer={footer} {...modal}> <Modal title="Languages Profile" footer={footer}>
<Input> <Input>
<Form.Control <Form.Control
type="text" type="text"
@ -319,8 +316,8 @@ const LanguagesProfileModal: FunctionComponent<Props & BaseModalProps> = (
></Selector> ></Selector>
<Message>Download subtitle file without format conversion</Message> <Message>Download subtitle file without format conversion</Message>
</Input> </Input>
</BaseModal> </Modal>
); );
}; };
export default LanguagesProfileModal; export default withModal(LanguagesProfileModal, "languages-profile-editor");

@ -1,5 +1,5 @@
import { ActionButton, SimpleTable } from "@/components"; import { ActionButton, SimpleTable } from "@/components";
import { useModalControl } from "@/modules/redux/hooks/modal"; import { useModalControl } from "@/modules/modals";
import { LOG } from "@/utilities/console"; import { LOG } from "@/utilities/console";
import { faTrash, faWrench } from "@fortawesome/free-solid-svg-icons"; import { faTrash, faWrench } from "@fortawesome/free-solid-svg-icons";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
@ -69,7 +69,7 @@ const Table: FunctionComponent = () => {
const mutateRow = useCallback<ModifyFn>( const mutateRow = useCallback<ModifyFn>(
(index, item) => { (index, item) => {
if (item) { if (item) {
show("profile", cloneDeep(item)); show(Modal, cloneDeep(item));
} else { } else {
const list = [...profiles]; const list = [...profiles];
list.splice(index, 1); list.splice(index, 1);
@ -185,12 +185,12 @@ const Table: FunctionComponent = () => {
mustNotContain: [], mustNotContain: [],
originalFormat: false, originalFormat: false,
}; };
show("profile", profile); show(Modal, profile);
}} }}
> >
{canAdd ? "Add New Profile" : "No Enabled Languages"} {canAdd ? "Add New Profile" : "No Enabled Languages"}
</Button> </Button>
<Modal update={updateProfile} modalKey="profile"></Modal> <Modal update={updateProfile}></Modal>
</> </>
); );
}; };

@ -1,32 +1,22 @@
import api from "@/apis/raw"; import api from "@/apis/raw";
import { AsyncButton, Selector, SelectorOption } from "@/components";
import { import {
AsyncButton, useModal,
BaseModal, useModalControl,
BaseModalProps, usePayload,
Selector, withModal,
SelectorOption, } from "@/modules/modals";
} from "@/components";
import { useModalControl, usePayload } from "@/modules/redux/hooks/modal";
import { BuildKey } from "@/utilities"; import { BuildKey } from "@/utilities";
import { import { FunctionComponent, useCallback, useMemo, useState } from "react";
FunctionComponent,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { Button, Col, Container, Form, Row } from "react-bootstrap"; import { Button, Col, Container, Form, Row } from "react-bootstrap";
import { ColCard, useLatestArray, useUpdateArray } from "../components"; import { ColCard, useLatestArray, useUpdateArray } from "../components";
import { notificationsKey } from "../keys"; import { notificationsKey } from "../keys";
interface ModalProps { interface Props {
selections: readonly Settings.NotificationInfo[]; selections: readonly Settings.NotificationInfo[];
} }
const NotificationModal: FunctionComponent<ModalProps & BaseModalProps> = ({ const NotificationTool: FunctionComponent<Props> = ({ selections }) => {
selections,
...modal
}) => {
const options = useMemo<SelectorOption<Settings.NotificationInfo>[]>( const options = useMemo<SelectorOption<Settings.NotificationInfo>[]>(
() => () =>
selections selections
@ -43,16 +33,11 @@ const NotificationModal: FunctionComponent<ModalProps & BaseModalProps> = ({
"name" "name"
); );
const payload = usePayload<Settings.NotificationInfo>(modal.modalKey); const payload = usePayload<Settings.NotificationInfo>();
const { hide } = useModalControl();
const [current, setCurrent] = const [current, setCurrent] =
useState<Nullable<Settings.NotificationInfo>>(payload); useState<Nullable<Settings.NotificationInfo>>(payload);
useEffect(() => {
setCurrent(payload);
}, [payload]);
const updateUrl = useCallback((url: string) => { const updateUrl = useCallback((url: string) => {
setCurrent((current) => { setCurrent((current) => {
if (current) { if (current) {
@ -69,55 +54,60 @@ const NotificationModal: FunctionComponent<ModalProps & BaseModalProps> = ({
const canSave = const canSave =
current !== null && current?.url !== null && current?.url.length !== 0; 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 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 ( return (
<BaseModal title="Notification" footer={footer} {...modal}> <Modal title="Notification" footer={footer}>
<Container fluid> <Container fluid>
<Row> <Row>
<Col xs={12}> <Col xs={12}>
@ -145,10 +135,12 @@ const NotificationModal: FunctionComponent<ModalProps & BaseModalProps> = ({
</Col> </Col>
</Row> </Row>
</Container> </Container>
</BaseModal> </Modal>
); );
}; };
const NotificationModal = withModal(NotificationTool, "notification-tool");
export const NotificationView: FunctionComponent = () => { export const NotificationView: FunctionComponent = () => {
const notifications = useLatestArray<Settings.NotificationInfo>( const notifications = useLatestArray<Settings.NotificationInfo>(
notificationsKey, notificationsKey,
@ -165,7 +157,7 @@ export const NotificationView: FunctionComponent = () => {
<ColCard <ColCard
key={BuildKey(idx, v.name)} key={BuildKey(idx, v.name)}
header={v.name} header={v.name}
onClick={() => show("notifications", v)} onClick={() => show(NotificationModal, v)}
></ColCard> ></ColCard>
)); ));
}, [notifications, show]); }, [notifications, show]);
@ -174,12 +166,9 @@ export const NotificationView: FunctionComponent = () => {
<Container fluid> <Container fluid>
<Row> <Row>
{elements}{" "} {elements}{" "}
<ColCard plus onClick={() => show("notifications")}></ColCard> <ColCard plus onClick={() => show(NotificationModal)}></ColCard>
</Row> </Row>
<NotificationModal <NotificationModal selections={notifications ?? []}></NotificationModal>
selections={notifications ?? []}
modalKey="notifications"
></NotificationModal>
</Container> </Container>
); );
}; };

@ -1,10 +1,10 @@
import { Selector, SelectorComponents, SelectorOption } from "@/components";
import { import {
BaseModal, useModal,
Selector, useModalControl,
SelectorComponents, usePayload,
SelectorOption, withModal,
} from "@/components"; } from "@/modules/modals";
import { useModalControl, usePayload } from "@/modules/redux/hooks/modal";
import { BuildKey, isReactText } from "@/utilities"; import { BuildKey, isReactText } from "@/utilities";
import { capitalize, isArray, isBoolean } from "lodash"; import { capitalize, isArray, isBoolean } from "lodash";
import { import {
@ -27,7 +27,6 @@ import {
} from "../components"; } from "../components";
import { ProviderInfo, ProviderList } from "./list"; import { ProviderInfo, ProviderList } from "./list";
const ModalKey = "provider-modal";
const ProviderKey = "settings-general-enabled_providers"; const ProviderKey = "settings-general-enabled_providers";
export const ProviderView: FunctionComponent = () => { export const ProviderView: FunctionComponent = () => {
@ -37,7 +36,7 @@ export const ProviderView: FunctionComponent = () => {
const select = useCallback( const select = useCallback(
(v?: ProviderInfo) => { (v?: ProviderInfo) => {
show(ModalKey, v ?? null); show(ProviderModal, v ?? null);
}, },
[show] [show]
); );
@ -72,12 +71,14 @@ export const ProviderView: FunctionComponent = () => {
{cards} {cards}
<ColCard key="add-card" plus onClick={select}></ColCard> <ColCard key="add-card" plus onClick={select}></ColCard>
</Row> </Row>
<ProviderModal></ProviderModal>
</Container> </Container>
); );
}; };
export const ProviderModal: FunctionComponent = () => { const ProviderTool: FunctionComponent = () => {
const payload = usePayload<ProviderInfo>(ModalKey); const payload = usePayload<ProviderInfo>();
const Modal = useModal();
const { hide } = useModalControl(); const { hide } = useModalControl();
const [staged, setChange] = useState<LooseObject>({}); const [staged, setChange] = useState<LooseObject>({});
@ -121,20 +122,6 @@ export const ProviderModal: FunctionComponent = () => {
const canSave = info !== null; 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>) => { const onSelect = useCallback((item: Nullable<ProviderInfo>) => {
if (item) { if (item) {
setInfo(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 ( return (
<BaseModal title="Provider" footer={footer} modalKey={ModalKey}> <Modal title="Provider" footer={footer}>
<StagedChangesContext.Provider value={[staged, setChange]}> <StagedChangesContext.Provider value={[staged, setChange]}>
<Container> <Container>
<Row> <Row>
@ -266,6 +264,8 @@ export const ProviderModal: FunctionComponent = () => {
</Row> </Row>
</Container> </Container>
</StagedChangesContext.Provider> </StagedChangesContext.Provider>
</BaseModal> </Modal>
); );
}; };
const ProviderModal = withModal(ProviderTool, "provider-tool");

@ -1,6 +1,6 @@
import { FunctionComponent } from "react"; import { FunctionComponent } from "react";
import { Group, Input, Layout } from "../components"; import { Group, Input, Layout } from "../components";
import { ProviderModal, ProviderView } from "./components"; import { ProviderView } from "./components";
const SettingsProvidersView: FunctionComponent = () => { const SettingsProvidersView: FunctionComponent = () => {
return ( return (
@ -10,7 +10,6 @@ const SettingsProvidersView: FunctionComponent = () => {
<ProviderView></ProviderView> <ProviderView></ProviderView>
</Input> </Input>
</Group> </Group>
<ProviderModal></ProviderModal>
</Layout> </Layout>
); );
}; };

@ -1,16 +1,20 @@
import { AsyncButton, BaseModal, BaseModalProps } from "@/components"; import { AsyncButton } from "@/components";
import { useModalControl, usePayload } from "@/modules/redux/hooks/modal"; import {
useModal,
useModalControl,
usePayload,
withModal,
} from "@/modules/modals";
import React, { FunctionComponent } from "react"; import React, { FunctionComponent } from "react";
import { Button } from "react-bootstrap"; import { Button } from "react-bootstrap";
import { useDeleteBackups } from "../../../apis/hooks"; import { useDeleteBackups } from "../../../apis/hooks";
interface Props extends BaseModalProps {} const SystemBackupDeleteModal: FunctionComponent = () => {
const SystemBackupDeleteModal: FunctionComponent<Props> = ({ ...modal }) => {
const { mutateAsync } = useDeleteBackups(); const { mutateAsync } = useDeleteBackups();
const result = usePayload<string>(modal.modalKey); const result = usePayload<string>();
const Modal = useModal();
const { hide } = useModalControl(); const { hide } = useModalControl();
const footer = ( const footer = (
@ -19,9 +23,7 @@ const SystemBackupDeleteModal: FunctionComponent<Props> = ({ ...modal }) => {
<Button <Button
variant="outline-secondary" variant="outline-secondary"
className="mr-2" className="mr-2"
onClick={() => { onClick={() => hide()}
hide(modal.modalKey);
}}
> >
Cancel Cancel
</Button> </Button>
@ -34,7 +36,7 @@ const SystemBackupDeleteModal: FunctionComponent<Props> = ({ ...modal }) => {
return null; return null;
} }
}} }}
onSuccess={() => hide(modal.modalKey)} onSuccess={() => hide()}
> >
Delete Delete
</AsyncButton> </AsyncButton>
@ -43,10 +45,10 @@ const SystemBackupDeleteModal: FunctionComponent<Props> = ({ ...modal }) => {
); );
return ( return (
<BaseModal title="Delete Backup" footer={footer} {...modal}> <Modal title="Delete Backup" footer={footer}>
Are you sure you want to delete the backup '{result}'? <span>Are you sure you want to delete the backup '{result}'?</span>
</BaseModal> </Modal>
); );
}; };
export default SystemBackupDeleteModal; export default withModal(SystemBackupDeleteModal, "delete");

@ -1,27 +1,29 @@
import { useRestoreBackups } from "@/apis/hooks/system"; import { useRestoreBackups } from "@/apis/hooks/system";
import { AsyncButton, BaseModal, BaseModalProps } from "@/components"; import { AsyncButton } from "@/components";
import { useModalControl, usePayload } from "@/modules/redux/hooks/modal"; import {
useModal,
useModalControl,
usePayload,
withModal,
} from "@/modules/modals";
import React, { FunctionComponent } from "react"; import React, { FunctionComponent } from "react";
import { Button } from "react-bootstrap"; import { Button } from "react-bootstrap";
interface Props extends BaseModalProps {} const SystemBackupRestoreModal: FunctionComponent = () => {
const result = usePayload<string>();
const SystemBackupRestoreModal: FunctionComponent<Props> = ({ ...modal }) => { const Modal = useModal();
const result = usePayload<string>(modal.modalKey); const { hide } = useModalControl();
const { mutateAsync } = useRestoreBackups(); const { mutateAsync } = useRestoreBackups();
const { hide } = useModalControl();
const footer = ( const footer = (
<div className="d-flex flex-row-reverse flex-grow-1 justify-content-between"> <div className="d-flex flex-row-reverse flex-grow-1 justify-content-between">
<div> <div>
<Button <Button
variant="outline-secondary" variant="outline-secondary"
className="mr-2" className="mr-2"
onClick={() => { onClick={() => hide()}
hide(modal.modalKey);
}}
> >
Cancel Cancel
</Button> </Button>
@ -34,7 +36,7 @@ const SystemBackupRestoreModal: FunctionComponent<Props> = ({ ...modal }) => {
return null; return null;
} }
}} }}
onSuccess={() => hide(modal.modalKey)} onSuccess={() => hide()}
> >
Restore Restore
</AsyncButton> </AsyncButton>
@ -43,11 +45,13 @@ const SystemBackupRestoreModal: FunctionComponent<Props> = ({ ...modal }) => {
); );
return ( return (
<BaseModal title="Restore Backup" footer={footer} {...modal}> <Modal title="Restore Backup" footer={footer}>
Are you sure you want to restore the backup '{result}'? Bazarr will <span>
automatically restart and reload the UI during the restore process. Are you sure you want to restore the backup '{result}'? Bazarr will
</BaseModal> automatically restart and reload the UI during the restore process.
</span>
</Modal>
); );
}; };
export default SystemBackupRestoreModal; export default withModal(SystemBackupRestoreModal, "restore");

@ -1,5 +1,5 @@
import { ActionButton, PageTable } from "@/components"; 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 { faClock, faHistory, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import React, { FunctionComponent, useMemo } from "react"; import React, { FunctionComponent, useMemo } from "react";
@ -42,11 +42,15 @@ const Table: FunctionComponent<Props> = ({ backups }) => {
<ButtonGroup> <ButtonGroup>
<ActionButton <ActionButton
icon={faHistory} icon={faHistory}
onClick={() => show("restore", row.row.original.filename)} onClick={() =>
show(SystemBackupRestoreModal, row.row.original.filename)
}
></ActionButton> ></ActionButton>
<ActionButton <ActionButton
icon={faTrash} icon={faTrash}
onClick={() => show("delete", row.row.original.filename)} onClick={() =>
show(SystemBackupDeleteModal, row.row.original.filename)
}
></ActionButton> ></ActionButton>
</ButtonGroup> </ButtonGroup>
); );
@ -59,14 +63,8 @@ const Table: FunctionComponent<Props> = ({ backups }) => {
return ( return (
<React.Fragment> <React.Fragment>
<PageTable columns={columns} data={backups}></PageTable> <PageTable columns={columns} data={backups}></PageTable>
<SystemBackupRestoreModal <SystemBackupRestoreModal></SystemBackupRestoreModal>
modalKey="restore" <SystemBackupDeleteModal></SystemBackupDeleteModal>
size="lg"
></SystemBackupRestoreModal>
<SystemBackupDeleteModal
modalKey="delete"
size="lg"
></SystemBackupDeleteModal>
</React.Fragment> </React.Fragment>
); );
}; };

@ -1,9 +1,11 @@
import { BaseModal, BaseModalProps } from "@/components"; import { useModal, usePayload, withModal } from "@/modules/modals";
import { usePayload } from "@/modules/redux/hooks/modal";
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
const SystemLogModal: FunctionComponent<BaseModalProps> = ({ ...modal }) => { const SystemLogModal: FunctionComponent = () => {
const stack = usePayload<string>(modal.modalKey); const stack = usePayload<string>();
const Modal = useModal();
const result = useMemo( const result = useMemo(
() => () =>
stack?.split("\\n").map((v, idx) => ( stack?.split("\\n").map((v, idx) => (
@ -13,13 +15,14 @@ const SystemLogModal: FunctionComponent<BaseModalProps> = ({ ...modal }) => {
)), )),
[stack] [stack]
); );
return ( return (
<BaseModal title="Stack traceback" {...modal}> <Modal title="Stack traceback">
<pre> <pre>
<code>{result}</code> <code>{result}</code>
</pre> </pre>
</BaseModal> </Modal>
); );
}; };
export default SystemLogModal; export default withModal(SystemLogModal, "system-log");

@ -1,5 +1,5 @@
import { ActionButton, PageTable } from "@/components"; 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 { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { import {
faBug, faBug,
@ -60,7 +60,7 @@ const Table: FunctionComponent<Props> = ({ logs }) => {
return ( return (
<ActionButton <ActionButton
icon={faLayerGroup} icon={faLayerGroup}
onClick={() => show("system-log", value)} onClick={() => show(SystemLogModal, value)}
></ActionButton> ></ActionButton>
); );
} else { } else {
@ -75,7 +75,7 @@ const Table: FunctionComponent<Props> = ({ logs }) => {
return ( return (
<> <>
<PageTable columns={columns} data={logs}></PageTable> <PageTable columns={columns} data={logs}></PageTable>
<SystemLogModal size="xl" modalKey="system-log"></SystemLogModal> <SystemLogModal></SystemLogModal>
</> </>
); );
}; };

Loading…
Cancel
Save