Skip to content

Commit

Permalink
Feature/show backup restore progress (#431)
Browse files Browse the repository at this point in the history
* Extract "Progress" component

* Show backup restore progress
  • Loading branch information
schroda authored Oct 28, 2023
1 parent 30fd8b0 commit 4c6d507
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 30 deletions.
19 changes: 1 addition & 18 deletions src/components/library/UpdateChecker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Box sx={{ display: 'grid', placeItems: 'center', position: 'relative' }}>
<CircularProgress variant="determinate" value={progress} />
<Box sx={{ position: 'absolute' }}>
<Typography fontSize="0.8rem">{`${Math.round(progress)}%`}</Typography>
</Box>
</Box>
);
}
import { Progress } from '@/components/util/Progress';

const calcProgress = (status: UpdaterSubscription['updateStatusChanged'] | undefined) => {
if (!status) {
Expand Down
20 changes: 20 additions & 0 deletions src/components/util/Progress.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<Box sx={{ display: 'grid', placeItems: 'center', position: 'relative' }}>
<CircularProgress variant="determinate" value={progress} />
<Box sx={{ position: 'absolute' }}>
<Typography fontSize="0.8rem">{`${Math.round(progress)}%`}</Typography>
</Box>
</Box>
);
10 changes: 9 additions & 1 deletion src/lib/requests/RequestManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ import {
GetMangaQueryVariables,
GetMangasQuery,
GetMangasQueryVariables,
GetRestoreStatusQuery,
GetRestoreStatusQueryVariables,
GetServerSettingsQuery,
GetSourceMangasFetchMutation,
GetSourceMangasFetchMutationVariables,
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -1591,6 +1593,12 @@ export class RequestManager {
return this.doRequest(GQLMethod.QUERY, VALIDATE_BACKUP, { backup: file }, options);
}

public useGetBackupRestoreStatus(
options?: QueryHookOptions<GetRestoreStatusQuery, GetRestoreStatusQueryVariables>,
): AbortableApolloUseQueryResponse<GetRestoreStatusQuery, GetRestoreStatusQueryVariables> {
return this.doRequest(GQLMethod.USE_QUERY, GET_RESTORE_STATUS, undefined, options);
}

public getExportBackupUrl(): string {
return this.getValidUrlFor('backup/export/file');
}
Expand Down
80 changes: 69 additions & 11 deletions src/screens/settings/Backup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -27,13 +30,57 @@ export function Backup() {

useSetDefaultBackTo('settings');

const submitBackup = (file: File) => {
const [isRestoring, setIsRestoring] = useState<boolean | null>(null);
const { data } = requestManager.useGetBackupRestoreStatus({
skip: isRestoring !== null ? !isRestoring : false,
pollInterval: 1000,
});
const prevRestoreStatusRef = useRef<GetRestoreStatusQuery['restoreStatus'] | null>(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 {
Expand All @@ -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);
};
}, []);

Expand All @@ -77,11 +127,19 @@ export function Backup() {
secondary={t('settings.backup.label.create_backup_info')}
/>
</ListItemLink>
<ListItemButton onClick={() => document.getElementById('backup-file')?.click()}>
<ListItemButton
onClick={() => document.getElementById('backup-file')?.click()}
disabled={!!isRestoring}
>
<ListItemText
primary={t('settings.backup.label.restore_backup')}
secondary={t('settings.backup.label.restore_backup_info')}
/>
{isRestoring ? (
<ListItemIcon>
<Progress progress={restoreProgress} />
</ListItemIcon>
) : null}
</ListItemButton>
</List>
<input type="file" id="backup-file" style={{ display: 'none' }} />
Expand Down

0 comments on commit 4c6d507

Please sign in to comment.