diff --git a/frontend/src/Settings/Notifications/components.tsx b/frontend/src/Settings/Notifications/components.tsx index 81a8a578c..f16e0bf70 100644 --- a/frontend/src/Settings/Notifications/components.tsx +++ b/frontend/src/Settings/Notifications/components.tsx @@ -50,9 +50,7 @@ const NotificationModal: FunctionComponent = ({ payload ?? null ); - const onShow = useCallback(() => setCurrent(payload ?? null), [payload]); - - useOnModalShow(modal.modalKey, onShow); + useOnModalShow(() => setCurrent(payload ?? null), modal.modalKey); const updateUrl = useCallback( (s: string) => { diff --git a/frontend/src/Settings/Providers/components.tsx b/frontend/src/Settings/Providers/components.tsx index d740ec50a..3522ccab6 100644 --- a/frontend/src/Settings/Providers/components.tsx +++ b/frontend/src/Settings/Providers/components.tsx @@ -84,9 +84,7 @@ export const ProviderModal: FunctionComponent = () => { const [info, setInfo] = useState>(payload ?? null); - const onShow = useCallback(() => setInfo(payload ?? null), [payload]); - - useOnModalShow(ModalKey, onShow); + useOnModalShow(() => setInfo(payload ?? null), ModalKey); const providers = useLatest(ProviderKey, isArray); diff --git a/frontend/src/components/modals/BaseModal.tsx b/frontend/src/components/modals/BaseModal.tsx index ccb79f398..8a717699b 100644 --- a/frontend/src/components/modals/BaseModal.tsx +++ b/frontend/src/components/modals/BaseModal.tsx @@ -1,7 +1,7 @@ -import React, { FunctionComponent } from "react"; +import React, { FunctionComponent, useCallback, useState } from "react"; import { Modal } from "react-bootstrap"; import { useIsModalShow } from "."; -import { useCloseModal } from "./provider"; +import { useCloseModal } from "./hooks"; export interface BaseModalProps { modalKey: string; @@ -12,22 +12,33 @@ export interface BaseModalProps { } export const BaseModal: FunctionComponent = (props) => { - const { size, closeable, modalKey, title, children, footer } = props; + const { size, modalKey, title, children, footer } = props; + const [needExit, setExit] = useState(false); const show = useIsModalShow(modalKey); - const closeModal = useCloseModal(); + const close = useCloseModal(); - const canClose = closeable !== false; + const closeable = props.closeable !== false; + + const hide = useCallback(() => { + setExit(true); + }, []); + + const exit = useCallback(() => { + close(); + setExit(false); + }, [close]); return ( - {title} + {title} {children} diff --git a/frontend/src/components/modals/HistoryModal.tsx b/frontend/src/components/modals/HistoryModal.tsx index e842b5b8d..6172d7db7 100644 --- a/frontend/src/components/modals/HistoryModal.tsx +++ b/frontend/src/components/modals/HistoryModal.tsx @@ -6,7 +6,7 @@ import { EpisodesApi, MoviesApi, useAsyncRequest } from "../../apis"; import { BlacklistButton } from "../../generic/blacklist"; import { AsyncOverlay } from "../async"; import BaseModal, { BaseModalProps } from "./BaseModal"; -import { usePayload } from "./provider"; +import { usePayload } from "./hooks"; export const MovieHistoryModal: FunctionComponent = (props) => { const { ...modal } = props; diff --git a/frontend/src/components/modals/ItemEditorModal.tsx b/frontend/src/components/modals/ItemEditorModal.tsx index b1a73a4b9..04f5021dd 100644 --- a/frontend/src/components/modals/ItemEditorModal.tsx +++ b/frontend/src/components/modals/ItemEditorModal.tsx @@ -4,7 +4,7 @@ import { AsyncButton, Selector } from "../"; import { useLanguageProfiles } from "../../@redux/hooks"; import { GetItemId } from "../../utilites"; import BaseModal, { BaseModalProps } from "./BaseModal"; -import { useCloseModal, usePayload } from "./provider"; +import { useCloseModal, usePayload } from "./hooks"; interface Props { submit: (form: FormType.ModifyItem) => Promise; diff --git a/frontend/src/components/modals/SubtitleToolModal.tsx b/frontend/src/components/modals/SubtitleToolModal.tsx index a3d6b68c3..4b8dd9538 100644 --- a/frontend/src/components/modals/SubtitleToolModal.tsx +++ b/frontend/src/components/modals/SubtitleToolModal.tsx @@ -48,7 +48,7 @@ import { isMovie, submodProcessColor } from "../../utilites"; import { log } from "../../utilites/logger"; import { useCustomSelection } from "../tables/plugins"; import BaseModal, { BaseModalProps } from "./BaseModal"; -import { useCloseModalUntil } from "./provider"; +import { useCloseModalUntil } from "./hooks"; import { availableTranslation, colorOptions } from "./toolOptions"; type SupportType = Item.Episode | Item.Movie; @@ -337,12 +337,12 @@ const STM: FunctionComponent = ({ ...props }) => { const [processState, setProcessState] = useState({}); const [selections, setSelections] = useState([]); - const closeUntil = useCloseModalUntil(props.modalKey); + const closeUntil = useCloseModalUntil(); const process = useCallback( async (action: string, override?: Partial) => { log("info", "executing action", action); - closeUntil(); + closeUntil(props.modalKey); setUpdate(true); let states = selections.reduce( @@ -374,7 +374,7 @@ const STM: FunctionComponent = ({ ...props }) => { } setUpdate(false); }, - [closeUntil, selections] + [closeUntil, selections, props.modalKey] ); const showModal = useShowModal(); diff --git a/frontend/src/components/modals/hooks.tsx b/frontend/src/components/modals/hooks.tsx new file mode 100644 index 000000000..6208599bc --- /dev/null +++ b/frontend/src/components/modals/hooks.tsx @@ -0,0 +1,76 @@ +import { useCallback, useContext, useMemo } from "react"; +import { useDidUpdate } from "rooks"; +import { log } from "../../utilites/logger"; +import { ModalContext } from "./provider"; + +export function useShowModal() { + const { + control: { push }, + } = useContext(ModalContext); + + return useCallback( + (key: string, payload?: T) => { + log("info", `modal ${key} sending payload`, payload); + + push({ key, payload }); + }, + [push] + ); +} + +export function useCloseModal() { + const { + control: { pop }, + } = useContext(ModalContext); + return pop; +} + +export function useCloseModalUntil() { + const { + control: { pop, peek }, + } = useContext(ModalContext); + return useCallback( + (key: string) => { + let modal = peek(); + while (modal) { + if (modal.key === key) { + break; + } else { + modal = pop(); + } + } + }, + [pop, peek] + ); +} + +export function useIsModalShow(key: string) { + const { + control: { peek }, + } = useContext(ModalContext); + const modal = peek(); + return key === modal?.key; +} + +export function useOnModalShow(callback: () => void, key: string) { + const isShow = useIsModalShow(key); + useDidUpdate(() => { + if (isShow) { + callback(); + } + }, [isShow]); +} + +export function usePayload(key: string): T | null { + const { + control: { peek }, + } = useContext(ModalContext); + return useMemo(() => { + const modal = peek(); + if (modal && modal.key === key) { + return modal.payload as T; + } else { + return null; + } + }, [key, peek]); +} diff --git a/frontend/src/components/modals/index.ts b/frontend/src/components/modals/index.ts index 4b0a26888..3662bfe5a 100644 --- a/frontend/src/components/modals/index.ts +++ b/frontend/src/components/modals/index.ts @@ -1,7 +1,8 @@ export * from "./BaseModal"; export * from "./HistoryModal"; +export * from "./hooks"; export { default as ItemEditorModal } from "./ItemEditorModal"; export { default as MovieUploadModal } from "./MovieUploadModal"; -export * from "./provider"; +export { default as ModalProvider } from "./provider"; export { default as SeriesUploadModal } from "./SeriesUploadModal"; export { default as SubtitleToolModal } from "./SubtitleToolModal"; diff --git a/frontend/src/components/modals/provider.tsx b/frontend/src/components/modals/provider.tsx index 95ed0c6ff..2481b62bb 100644 --- a/frontend/src/components/modals/provider.tsx +++ b/frontend/src/components/modals/provider.tsx @@ -1,100 +1,48 @@ -import React, { - Dispatch, - FunctionComponent, - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from "react"; -import { log } from "../../utilites/logger"; +import React, { FunctionComponent, useMemo } from "react"; +import { useStackState } from "rooks"; -const ModalContext = React.createContext<[string[], Dispatch]>([ - [], - (s) => {}, -]); - -const PayloadContext = React.createContext<[any[], Dispatch]>([ - [], - (p) => {}, -]); - -// TODO: Performance -export function useShowModal() { - const [keys, setKeys] = useContext(ModalContext); - const [payloads, setPayloads] = useContext(PayloadContext); - return useCallback( - (key: string, payload?: T) => { - log("info", `modal ${key} sending payload`, payload); - - setKeys([...keys, key]); - setPayloads([...payloads, payload ?? null]); - }, - [keys, payloads, setKeys, setPayloads] - ); -} - -export function useCloseModal() { - const [keys, setKeys] = useContext(ModalContext); - const [payloads, setPayloads] = useContext(PayloadContext); - return useCallback(() => { - const newKey = [...keys]; - newKey.pop(); - const newPayload = [...payloads]; - newPayload.pop(); - setKeys(newKey); - setPayloads(newPayload); - }, [keys, payloads, setKeys, setPayloads]); +interface Modal { + key: string; + payload: any; } -export function useCloseModalUntil(key: string) { - const [keys, setKeys] = useContext(ModalContext); - const [payloads, setPayloads] = useContext(PayloadContext); - return useCallback(() => { - const idx = keys.findIndex((v) => v === key); - if (idx !== -1) { - const newKey = keys.slice(0, idx + 1); - const newPayload = payloads.slice(0, idx + 1); - setKeys(newKey); - setPayloads(newPayload); - } else { - log("error", "Cannot close modal, key is unavailable"); - } - }, [keys, payloads, setKeys, setPayloads, key]); +interface ModalControl { + push: (modal: Modal) => void; + peek: () => Modal | undefined; + pop: () => Modal | undefined; } -export function useIsModalShow(key: string) { - const keys = useContext(ModalContext)[0]; - return key === keys[keys.length - 1]; +interface ModalContextType { + modals: Modal[]; + control: ModalControl; } -export function useOnModalShow(key: string, show: () => void) { - const isShow = useIsModalShow(key); - useEffect(() => { - if (isShow) { - show(); - } - }, [isShow, show]); -} +export const ModalContext = React.createContext({ + modals: [], + control: { + push: () => { + throw new Error("Unimplemented"); + }, + pop: () => { + throw new Error("Unimplemented"); + }, + peek: () => { + throw new Error("Unimplemented"); + }, + }, +}); -export function usePayload(key: string): T | null { - const payloads = useContext(PayloadContext)[0]; - const keys = useContext(ModalContext)[0]; - return useMemo(() => { - const idx = keys.findIndex((v) => v === key); - return idx !== -1 ? payloads[idx] : null; - }, [keys, payloads, key]); -} +const ModalProvider: FunctionComponent = ({ children }) => { + const [stack, { push, pop, peek }] = useStackState([]); -export const ModalProvider: FunctionComponent = ({ children }) => { - const [key, setKey] = useState([]); - const [payload, setPayload] = useState([]); + const context = useMemo( + () => ({ modals: stack, control: { push, pop, peek } }), + [stack, push, pop, peek] + ); return ( - - - {children} - - + {children} ); }; + +export default ModalProvider;