diff --git a/package.json b/package.json index 542abc68..2384cbce 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@redux-devtools/extension": "3.3.0", "@vibrant/color": "4.0.0", "apollo-upload-client": "18.0.1", + "awaitable-component": "0.1.0", "csstype": "3.1.3", "dayjs": "1.11.18", "fast-average-color": "9.5.0", diff --git a/src/App.tsx b/src/App.tsx index 67f91c77..3e1542e5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,7 @@ import { Navigate, Outlet, Route, Routes, useLocation, useNavigate } from 'react import { loadErrorMessages, loadDevMessages } from '@apollo/client/dev'; import { loadable } from 'react-lazily/loadable'; import Box from '@mui/material/Box'; +import { AwaitableComponent } from 'awaitable-component'; import { AppContext } from '@/base/contexts/AppContext.tsx'; import { DefaultNavBar } from '@/features/navigation-bar/components/DefaultNavBar.tsx'; import { requestManager } from '@/lib/requests/RequestManager.ts'; @@ -29,7 +30,6 @@ import { AuthGuard } from '@/features/authentication/components/AuthGuard.tsx'; import { SearchParam } from '@/base/Base.types.ts'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { ReactRouter } from '@/lib/react-router/ReactRouter.ts'; -import { GlobalDialog } from '@/base/global-dialog/GlobalDialog.tsx'; import { AuthManager } from '@/features/authentication/AuthManager.ts'; const { Browse } = loadable(() => import('@/features/browse/screens/Browse.tsx'), lazyLoadFallback); @@ -286,7 +286,7 @@ const ReaderApp = () => ( export const App: React.FC = () => ( - + diff --git a/src/base/AppAwaitableComponent.ts b/src/base/AppAwaitableComponent.ts new file mode 100644 index 00000000..fff5cfc1 --- /dev/null +++ b/src/base/AppAwaitableComponent.ts @@ -0,0 +1,12 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { AwaitableComponent } from 'awaitable-component'; +import { ConfirmDialog } from '@/base/components/modals/ConfirmDialog.tsx'; + +export const Confirmation = AwaitableComponent.create(ConfirmDialog); diff --git a/src/base/components/feedback/SnackbarWithDescription.tsx b/src/base/components/feedback/SnackbarWithDescription.tsx index 75e0328c..75d9522e 100644 --- a/src/base/components/feedback/SnackbarWithDescription.tsx +++ b/src/base/components/feedback/SnackbarWithDescription.tsx @@ -17,7 +17,7 @@ import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts' import { MediaQuery } from '@/base/utils/MediaQuery.tsx'; import { extractGraphqlExceptionInfo } from '@/lib/HelperFunctions.ts'; import { TranslationKey } from '@/base/Base.types.ts'; -import { GlobalDialogManager } from '@/base/global-dialog/GlobalDialogManager.tsx'; +import { Confirmation } from '@/base/AppAwaitableComponent.ts'; const MAX_DESCRIPTION_LENGTH = 200; @@ -83,7 +83,7 @@ export const SnackbarWithDescription = memo( {isDescriptionTooLong || (isGraphqlException && graphqlStackTrace) ? ( )} {actions.confirm.show && ( - )} diff --git a/src/base/global-dialog/GlobalDialog.tsx b/src/base/global-dialog/GlobalDialog.tsx deleted file mode 100644 index d2dfd554..00000000 --- a/src/base/global-dialog/GlobalDialog.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { useSyncExternalStore } from 'react'; -import { GlobalDialogManager } from '@/base/global-dialog/GlobalDialogManager.tsx'; - -export const GlobalDialog = () => - useSyncExternalStore(GlobalDialogManager.subscribe.bind(GlobalDialogManager), () => - GlobalDialogManager.getActive(), - ); diff --git a/src/base/global-dialog/GlobalDialogManager.tsx b/src/base/global-dialog/GlobalDialogManager.tsx deleted file mode 100644 index a7448dee..00000000 --- a/src/base/global-dialog/GlobalDialogManager.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { ComponentProps, ComponentType, ReactNode } from 'react'; -import { ControlledPromise } from '@/lib/ControlledPromise.ts'; -import { ConfirmDialog } from '@/base/components/modals/ConfirmDialog.tsx'; - -type Id = string; - -export interface DialogProps { - onCancel: () => void; - onConfirm: (value: T) => void; -} - -type DialogPropsParam = Omit; - -type ExtractDialogReturnType = T extends DialogProps ? U : never; - -export class GlobalDialogManager { - private static subscriberCounter = 0; - - private static queue = new Set(); - - private static dialogs = new Map(); - - private static subscribers = new Map void>(); - - private static getNextId = (): Id => { - this.subscriberCounter = (this.subscriberCounter + 1) % Number.MAX_SAFE_INTEGER; - - return this.subscriberCounter.toString(); - }; - - private static notify(): void { - this.subscribers.forEach((callback) => { - callback(); - }); - } - - private static add(id: Id, dialog: ReactNode): void { - this.queue.delete(id); - this.queue.add(id); - this.dialogs.set(id, dialog); - - const showDialog = !(this.dialogs.size - 1); - if (showDialog) { - this.notify(); - } - } - - private static remove(id: Id): void { - this.queue.delete(id); - this.dialogs.delete(id); - this.notify(); - } - - private static unsubscribe(key: Id): void { - this.subscribers.delete(key); - } - - static subscribe(callback: () => void): () => void { - const key = this.getNextId(); - this.subscribers.set(key, callback); - - return () => this.unsubscribe(key); - } - - static getActive(): ReactNode { - const id = [...this.queue].slice(-1)[0]; - - return this.dialogs.get(id); - } - - static async show( - Dialog: ComponentType, - ...args: HasRequiredKeys> extends true - ? [props: DialogPropsParam] - : [props?: DialogPropsParam] - ): Promise>; - static async show( - id: Id, - Dialog: ComponentType, - ...args: HasRequiredKeys> extends true - ? [props: DialogPropsParam] - : [props?: DialogPropsParam] - ): Promise>; - static async show( - idOrDialog: Id | ComponentType, - dialogOrProps?: ComponentType | DialogPropsParam, - propsOrNothing?: DialogPropsParam | never, - ): Promise> { - const dialogPromise = new ControlledPromise>(); - - const wasIdPassed = typeof idOrDialog === 'string'; - const id = wasIdPassed ? idOrDialog : this.getNextId(); - const Dialog = (wasIdPassed ? dialogOrProps : idOrDialog) as ComponentType; - const props = (wasIdPassed ? propsOrNothing : dialogOrProps) as DialogPropsParam | undefined; - - const dialog = ( - dialogPromise.reject(new Error('Dialog was cancelled')), - onConfirm: dialogPromise.resolve.bind(dialogPromise), - } as unknown as AllProps)} - /> - ); - - this.add(id, dialog); - - try { - return await dialogPromise.promise; - } finally { - this.remove(id); - } - } - - static confirm( - props: DialogPropsParam>, - ): Promise>>; - static confirm( - id: Id, - props: DialogPropsParam>, - ): Promise>>; - static confirm( - idOrProps: Id | DialogPropsParam>, - propsOrNothing?: DialogPropsParam>, - ): Promise>> { - if (typeof idOrProps === 'string') { - return this.show(idOrProps, ConfirmDialog, propsOrNothing!); - } - - return this.show(ConfirmDialog, idOrProps); - } -} diff --git a/src/features/backup/component/BackupFlagInclusionDialog.tsx b/src/features/backup/component/BackupFlagInclusionDialog.tsx index 7055c444..33f9fa4d 100644 --- a/src/features/backup/component/BackupFlagInclusionDialog.tsx +++ b/src/features/backup/component/BackupFlagInclusionDialog.tsx @@ -16,8 +16,8 @@ import { useState } from 'react'; import Button from '@mui/material/Button'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; +import { AwaitableComponentProps } from 'awaitable-component'; import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx'; -import { DialogProps } from '@/base/global-dialog/GlobalDialogManager.tsx'; import { BACKUP_FLAG_GROUP_TO_TRANSLATION, BACKUP_FLAGS, @@ -27,10 +27,12 @@ import { import { BackupFlagGroup, BackupFlagInclusionState } from '@/features/backup/Backup.types.ts'; export const BackupFlagInclusionDialog = ({ - onCancel, - onConfirm, + onDismiss, + onSubmit, + isVisible, + onExitComplete, title, -}: DialogProps & { title: string }) => { +}: AwaitableComponentProps & { title: string }) => { const { t } = useTranslation(); const [includeStateByFlag, setIncludeStateByFlag] = useState( @@ -38,7 +40,14 @@ export const BackupFlagInclusionDialog = ({ ); return ( - + {title} @@ -63,10 +72,10 @@ export const BackupFlagInclusionDialog = ({ - - diff --git a/src/features/backup/component/BackupValidationDialog.tsx b/src/features/backup/component/BackupValidationDialog.tsx index 503232f0..cb1b9fea 100644 --- a/src/features/backup/component/BackupValidationDialog.tsx +++ b/src/features/backup/component/BackupValidationDialog.tsx @@ -16,20 +16,22 @@ import Button from '@mui/material/Button'; import ListItem from '@mui/material/ListItem'; import { Link } from 'react-router-dom'; import Stack from '@mui/material/Stack'; +import { AwaitableComponentProps } from 'awaitable-component'; import { BrowseTab } from '@/features/browse/Browse.types.ts'; import { AppRoutes } from '@/base/AppRoute.constants.ts'; -import { DialogProps } from '@/base/global-dialog/GlobalDialogManager.tsx'; import { ValidateBackupResult } from '@/lib/graphql/generated/graphql.ts'; export const BackupValidationDialog = ({ validationResult, - onCancel, - onConfirm, -}: DialogProps & { validationResult: ValidateBackupResult }) => { + onDismiss, + onSubmit, + isVisible, + onExitComplete, +}: AwaitableComponentProps & { validationResult: ValidateBackupResult }) => { const { t } = useTranslation(); return ( - + {t('settings.backup.action.validate.dialog.title')} {!!validationResult?.missingSources.length && ( @@ -67,7 +69,7 @@ export const BackupValidationDialog = ({ > {!!validationResult?.missingSources.length && ( +