From 4c6d50740500d2823d7990c0192f9aebefea2575 Mon Sep 17 00:00:00 2001
From: schroda <50052685+schroda@users.noreply.github.com>
Date: Sun, 29 Oct 2023 01:49:27 +0200
Subject: [PATCH] Feature/show backup restore progress (#431)
* Extract "Progress" component
* Show backup restore progress
---
src/components/library/UpdateChecker.tsx | 19 +-----
src/components/util/Progress.tsx | 20 ++++++
src/lib/requests/RequestManager.ts | 10 ++-
src/screens/settings/Backup.tsx | 80 ++++++++++++++++++++----
4 files changed, 99 insertions(+), 30 deletions(-)
create mode 100644 src/components/util/Progress.tsx
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}