diff --git a/src/components/library/UpdateChecker.tsx b/src/components/library/UpdateChecker.tsx index d2cf502970..86986f2416 100644 --- a/src/components/library/UpdateChecker.tsx +++ b/src/components/library/UpdateChecker.tsx @@ -9,28 +9,11 @@ import { useMemo } from 'react'; import IconButton from '@mui/material/IconButton'; import RefreshIcon from '@mui/icons-material/Refresh'; -import CircularProgress from '@mui/material/CircularProgress'; -import { Box } from '@mui/material'; -import Typography from '@mui/material/Typography'; import { useTranslation } from 'react-i18next'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { makeToast } from '@/components/util/Toast'; import { UpdaterSubscription } from '@/lib/graphql/generated/graphql.ts'; - -interface IProgressProps { - progress: number; -} - -function Progress({ progress }: IProgressProps) { - return ( - - - - {`${Math.round(progress)}%`} - - - ); -} +import { Progress } from '@/components/util/Progress'; const calcProgress = (status: UpdaterSubscription['updateStatusChanged'] | undefined) => { if (!status) { diff --git a/src/components/util/Progress.tsx b/src/components/util/Progress.tsx new file mode 100644 index 0000000000..ef9f67fc26 --- /dev/null +++ b/src/components/util/Progress.tsx @@ -0,0 +1,20 @@ +/* + * 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 { Box } from '@mui/material'; +import CircularProgress from '@mui/material/CircularProgress'; +import Typography from '@mui/material/Typography'; + +export const Progress = ({ progress }: { progress: number }) => ( + + + + {`${Math.round(progress)}%`} + + +); diff --git a/src/lib/requests/RequestManager.ts b/src/lib/requests/RequestManager.ts index f9faa65702..6da7d485d5 100644 --- a/src/lib/requests/RequestManager.ts +++ b/src/lib/requests/RequestManager.ts @@ -83,6 +83,8 @@ import { GetMangaQueryVariables, GetMangasQuery, GetMangasQueryVariables, + GetRestoreStatusQuery, + GetRestoreStatusQueryVariables, GetServerSettingsQuery, GetSourceMangasFetchMutation, GetSourceMangasFetchMutationVariables, @@ -199,7 +201,7 @@ import { import { GET_UPDATE_STATUS } from '@/lib/graphql/queries/UpdaterQuery.ts'; import { CustomCache } from '@/lib/requests/CustomCache.ts'; import { RESTORE_BACKUP } from '@/lib/graphql/mutations/BackupMutation.ts'; -import { VALIDATE_BACKUP } from '@/lib/graphql/queries/BackupQuery.ts'; +import { GET_RESTORE_STATUS, VALIDATE_BACKUP } from '@/lib/graphql/queries/BackupQuery.ts'; import { DOWNLOAD_STATUS_SUBSCRIPTION } from '@/lib/graphql/subscriptions/DownloaderSubscription.ts'; import { UPDATER_SUBSCRIPTION } from '@/lib/graphql/subscriptions/UpdaterSubscription.ts'; import { GET_SERVER_SETTINGS } from '@/lib/graphql/queries/SettingsQuery.ts'; @@ -1591,6 +1593,12 @@ export class RequestManager { return this.doRequest(GQLMethod.QUERY, VALIDATE_BACKUP, { backup: file }, options); } + public useGetBackupRestoreStatus( + options?: QueryHookOptions, + ): AbortableApolloUseQueryResponse { + return this.doRequest(GQLMethod.USE_QUERY, GET_RESTORE_STATUS, undefined, options); + } + public getExportBackupUrl(): string { return this.getValidUrlFor('backup/export/file'); } diff --git a/src/screens/settings/Backup.tsx b/src/screens/settings/Backup.tsx index ebb903c432..6e2273aa66 100644 --- a/src/screens/settings/Backup.tsx +++ b/src/screens/settings/Backup.tsx @@ -6,16 +6,19 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useContext, useEffect } from 'react'; +import { useContext, useEffect, useRef, useState } from 'react'; import List from '@mui/material/List'; import ListItemText from '@mui/material/ListItemText'; import { fromEvent } from 'file-selector'; import { useTranslation } from 'react-i18next'; import { ListItemButton } from '@mui/material'; +import ListItemIcon from '@mui/material/ListItemIcon'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { makeToast } from '@/components/util/Toast'; import { ListItemLink } from '@/components/util/ListItemLink'; import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext'; +import { BackupRestoreState, GetRestoreStatusQuery } from '@/lib/graphql/generated/graphql.ts'; +import { Progress } from '@/components/util/Progress.tsx'; export function Backup() { const { t } = useTranslation(); @@ -27,13 +30,57 @@ export function Backup() { useSetDefaultBackTo('settings'); - const submitBackup = (file: File) => { + const [isRestoring, setIsRestoring] = useState(null); + const { data } = requestManager.useGetBackupRestoreStatus({ + skip: isRestoring !== null ? !isRestoring : false, + pollInterval: 1000, + }); + const prevRestoreStatusRef = useRef(null); + + const restoreProgress = (() => { + if (!isRestoring || !data) { + return 0; + } + + const progress = 100 * (data.restoreStatus.mangaProgress / data.restoreStatus.totalManga); + return Number.isNaN(progress) ? 0 : progress; + })(); + + useEffect(() => { + if (!data || isRestoring !== null) { + return; + } + + const isRestoreInProgress = data?.restoreStatus.state !== BackupRestoreState.Idle; + setIsRestoring(isRestoreInProgress); + }, [data?.restoreStatus.state]); + + useEffect(() => { + if (!data) { + return; + } + + const isRestoreFinished = + isRestoring && + data.restoreStatus.mangaProgress === data.restoreStatus.totalManga && + prevRestoreStatusRef.current?.state !== BackupRestoreState.Idle; + if (isRestoreFinished) { + setIsRestoring(false); + makeToast(t('settings.backup.label.restored_backup'), 'success'); + } + prevRestoreStatusRef.current = data.restoreStatus; + }, [data?.restoreStatus.state]); + + const submitBackup = async (file: File) => { if (file.name.toLowerCase().match(/proto\.gz$|tachibk$/g)) { makeToast(t('settings.backup.label.restoring_backup'), 'info'); - requestManager - .restoreBackupFile(file) - .response.then(() => makeToast(t('settings.backup.label.restored_backup'), 'success')) - .catch(() => makeToast(t('settings.backup.label.backup_restore_failed'), 'error')); + + try { + await requestManager.restoreBackupFile(file).response; + setIsRestoring(true); + } catch (e) { + makeToast(t('settings.backup.label.backup_restore_failed'), 'error'); + } } else if (file.name.toLowerCase().endsWith('json')) { makeToast(t('settings.backup.label.legacy_backup_unsupported'), 'error'); } else { @@ -56,15 +103,18 @@ export function Backup() { document.addEventListener('drop', dropHandler); document.addEventListener('dragover', dragOverHandler); - const input = document.getElementById('backup-file'); - input?.addEventListener('change', async (evt) => { - const files = await fromEvent(evt); + const handleFileSelection = async (event: Event) => { + const files = await fromEvent(event); submitBackup(files[0] as File); - }); + }; + + const input = document.getElementById('backup-file'); + input?.addEventListener('change', handleFileSelection); return () => { document.removeEventListener('drop', dropHandler); document.removeEventListener('dragover', dragOverHandler); + input?.removeEventListener('change', handleFileSelection); }; }, []); @@ -77,11 +127,19 @@ export function Backup() { secondary={t('settings.backup.label.create_backup_info')} /> - document.getElementById('backup-file')?.click()}> + document.getElementById('backup-file')?.click()} + disabled={!!isRestoring} + > + {isRestoring ? ( + + + + ) : null}