From 6a8c96cc8c98fa396e2593b78e8cd4c230a4b14f Mon Sep 17 00:00:00 2001
From: viown <48097677+viown@users.noreply.github.com>
Date: Tue, 3 Jun 2025 22:04:45 +0300
Subject: [PATCH 1/3] Add backup dashboard pages
---
.../drawer/sections/AdvancedDrawerSection.tsx | 9 +
.../features/backups/api/useBackups.ts | 27 +++
.../features/backups/api/useCreateBackup.ts | 25 +++
.../features/backups/api/useRestoreBackup.ts | 19 ++
.../features/backups/components/Backup.tsx | 68 +++++++
.../backups/components/BackupInfoDialog.tsx | 135 ++++++++++++
.../components/BackupProgressDialog.tsx | 27 +++
.../backups/components/CreateBackupForm.tsx | 123 +++++++++++
.../components/RestoreConfirmationDialog.tsx | 46 +++++
.../components/RestoreProgressDialog.tsx | 32 +++
src/apps/dashboard/routes/_asyncRoutes.ts | 1 +
src/apps/dashboard/routes/backups/index.tsx | 192 ++++++++++++++++++
src/strings/en-us.json | 15 ++
13 files changed, 719 insertions(+)
create mode 100644 src/apps/dashboard/features/backups/api/useBackups.ts
create mode 100644 src/apps/dashboard/features/backups/api/useCreateBackup.ts
create mode 100644 src/apps/dashboard/features/backups/api/useRestoreBackup.ts
create mode 100644 src/apps/dashboard/features/backups/components/Backup.tsx
create mode 100644 src/apps/dashboard/features/backups/components/BackupInfoDialog.tsx
create mode 100644 src/apps/dashboard/features/backups/components/BackupProgressDialog.tsx
create mode 100644 src/apps/dashboard/features/backups/components/CreateBackupForm.tsx
create mode 100644 src/apps/dashboard/features/backups/components/RestoreConfirmationDialog.tsx
create mode 100644 src/apps/dashboard/features/backups/components/RestoreProgressDialog.tsx
create mode 100644 src/apps/dashboard/routes/backups/index.tsx
diff --git a/src/apps/dashboard/components/drawer/sections/AdvancedDrawerSection.tsx b/src/apps/dashboard/components/drawer/sections/AdvancedDrawerSection.tsx
index 97d8f2671..4d5508aba 100644
--- a/src/apps/dashboard/components/drawer/sections/AdvancedDrawerSection.tsx
+++ b/src/apps/dashboard/components/drawer/sections/AdvancedDrawerSection.tsx
@@ -1,4 +1,5 @@
import Article from '@mui/icons-material/Article';
+import Backup from '@mui/icons-material/Backup';
import Lan from '@mui/icons-material/Lan';
import Schedule from '@mui/icons-material/Schedule';
import VpnKey from '@mui/icons-material/VpnKey';
@@ -38,6 +39,14 @@ const AdvancedDrawerSection = () => {
+
+
+
+
+
+
+
+
diff --git a/src/apps/dashboard/features/backups/api/useBackups.ts b/src/apps/dashboard/features/backups/api/useBackups.ts
new file mode 100644
index 000000000..22b45d2ae
--- /dev/null
+++ b/src/apps/dashboard/features/backups/api/useBackups.ts
@@ -0,0 +1,27 @@
+import { Api } from '@jellyfin/sdk';
+import { BackupApi } from '@jellyfin/sdk/lib/generated-client/api/backup-api';
+import { useQuery } from '@tanstack/react-query';
+import type { AxiosRequestConfig } from 'axios';
+import { useApi } from 'hooks/useApi';
+
+export const QUERY_KEY = 'Backups';
+
+const fetchBackups = async (api: Api, options?: AxiosRequestConfig) => {
+ // FIXME: Replace with getBackupApi when available in SDK
+ const backupApi = new BackupApi(api.configuration, undefined, api.axiosInstance);
+
+ const response = await backupApi.listBackups(options);
+
+ return response.data;
+};
+
+export const useBackups = () => {
+ const { api } = useApi();
+
+ return useQuery({
+ queryKey: [ QUERY_KEY ],
+ queryFn: ({ signal }) =>
+ fetchBackups(api!, { signal }),
+ enabled: !!api
+ });
+};
diff --git a/src/apps/dashboard/features/backups/api/useCreateBackup.ts b/src/apps/dashboard/features/backups/api/useCreateBackup.ts
new file mode 100644
index 000000000..19861d096
--- /dev/null
+++ b/src/apps/dashboard/features/backups/api/useCreateBackup.ts
@@ -0,0 +1,25 @@
+import type { BackupOptionsDto } from '@jellyfin/sdk/lib/generated-client/models/backup-options-dto';
+import { BackupApi } from '@jellyfin/sdk/lib/generated-client/api/backup-api';
+import { useMutation } from '@tanstack/react-query';
+import { useApi } from 'hooks/useApi';
+import { QUERY_KEY } from './useBackups';
+import { queryClient } from 'utils/query/queryClient';
+
+export const useCreateBackup = () => {
+ const { api } = useApi();
+ // FIXME: Replace with getBackupApi when available in SDK
+ const backupApi = new BackupApi(api?.configuration, undefined, api?.axiosInstance);
+
+ return useMutation({
+ mutationFn: (backupOptions: BackupOptionsDto) => (
+ backupApi.createBackup({
+ backupOptionsDto: backupOptions
+ })
+ ),
+ onSuccess: () => {
+ void queryClient.invalidateQueries({
+ queryKey: [ QUERY_KEY ]
+ });
+ }
+ });
+};
diff --git a/src/apps/dashboard/features/backups/api/useRestoreBackup.ts b/src/apps/dashboard/features/backups/api/useRestoreBackup.ts
new file mode 100644
index 000000000..0ecafbfb5
--- /dev/null
+++ b/src/apps/dashboard/features/backups/api/useRestoreBackup.ts
@@ -0,0 +1,19 @@
+import { BackupApi } from '@jellyfin/sdk/lib/generated-client/api/backup-api';
+import { useMutation } from '@tanstack/react-query';
+import { useApi } from 'hooks/useApi';
+
+export const useRestoreBackup = () => {
+ const { api } = useApi();
+ // FIXME: Replace with getBackupApi when available in SDK
+ const backupApi = new BackupApi(api?.configuration, undefined, api?.axiosInstance);
+
+ return useMutation({
+ mutationFn: (fileName: string) => (
+ backupApi.startRestoreBackup({
+ backupRestoreRequestDto: {
+ ArchiveFileName: fileName
+ }
+ })
+ )
+ });
+};
diff --git a/src/apps/dashboard/features/backups/components/Backup.tsx b/src/apps/dashboard/features/backups/components/Backup.tsx
new file mode 100644
index 000000000..e7c622d3b
--- /dev/null
+++ b/src/apps/dashboard/features/backups/components/Backup.tsx
@@ -0,0 +1,68 @@
+import React, { FunctionComponent, useCallback, useState } from 'react';
+import type { BackupManifestDto } from '@jellyfin/sdk/lib/generated-client/models/backup-manifest-dto';
+import IconButton from '@mui/material/IconButton';
+import ListItem from '@mui/material/ListItem';
+import Restore from '@mui/icons-material/Restore';
+import ListItemButton from '@mui/material/ListItemButton';
+import ListItemText from '@mui/material/ListItemText';
+import Tooltip from '@mui/material/Tooltip';
+import globalize from 'lib/globalize';
+import BackupInfoDialog from './BackupInfoDialog';
+
+type BackupProps = {
+ backup: BackupManifestDto;
+ onRestore: (backup: BackupManifestDto) => void;
+};
+
+const Backup: FunctionComponent = ({ backup, onRestore }) => {
+ const [ isInfoDialogOpen, setIsInfoDialogOpen ] = useState(false);
+
+ const onDialogClose = useCallback(() => {
+ setIsInfoDialogOpen(false);
+ }, []);
+
+ const openDialog = useCallback(() => {
+ setIsInfoDialogOpen(true);
+ }, []);
+
+ const restore = useCallback(() => {
+ onRestore(backup);
+ }, [ backup, onRestore ]);
+
+ return (
+ <>
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+ >
+ );
+};
+
+export default Backup;
diff --git a/src/apps/dashboard/features/backups/components/BackupInfoDialog.tsx b/src/apps/dashboard/features/backups/components/BackupInfoDialog.tsx
new file mode 100644
index 000000000..d83a768fb
--- /dev/null
+++ b/src/apps/dashboard/features/backups/components/BackupInfoDialog.tsx
@@ -0,0 +1,135 @@
+import type { BackupManifestDto } from '@jellyfin/sdk/lib/generated-client/models/backup-manifest-dto';
+import Typography from '@mui/material/Typography';
+import Button from '@mui/material/Button';
+import Dialog from '@mui/material/Dialog';
+import DialogActions from '@mui/material/DialogActions';
+import DialogContent from '@mui/material/DialogContent';
+import DialogTitle from '@mui/material/DialogTitle';
+import Box from '@mui/material/Box';
+import globalize from 'lib/globalize';
+import React, { FunctionComponent, useCallback } from 'react';
+import Stack from '@mui/material/Stack';
+import FormGroup from '@mui/material/FormGroup';
+import FormControl from '@mui/material/FormControl';
+import FormControlLabel from '@mui/material/FormControlLabel';
+import Checkbox from '@mui/material/Checkbox';
+import ContentCopy from '@mui/icons-material/ContentCopy';
+import IconButton from '@mui/material/IconButton';
+import { copy } from 'scripts/clipboard';
+import toast from 'components/toast/toast';
+
+type IProps = {
+ backup: BackupManifestDto;
+ open: boolean;
+ onClose: () => void;
+};
+
+const BackupInfoDialog: FunctionComponent = ({ backup, open, onClose }: IProps) => {
+ const copyPath = useCallback(async () => {
+ if (backup.Path) {
+ await copy(backup.Path);
+ toast({ text: globalize.translate('Copied') });
+ }
+ }, [ backup.Path ]);
+
+ return (
+
+ );
+};
+
+export default BackupInfoDialog;
diff --git a/src/apps/dashboard/features/backups/components/BackupProgressDialog.tsx b/src/apps/dashboard/features/backups/components/BackupProgressDialog.tsx
new file mode 100644
index 000000000..bcf158d2a
--- /dev/null
+++ b/src/apps/dashboard/features/backups/components/BackupProgressDialog.tsx
@@ -0,0 +1,27 @@
+import React, { FunctionComponent } from 'react';
+import Dialog from '@mui/material/Dialog';
+import DialogContent from '@mui/material/DialogContent';
+import DialogTitle from '@mui/material/DialogTitle';
+import LinearProgress from '@mui/material/LinearProgress';
+import globalize from 'lib/globalize';
+
+type IProps = {
+ open: boolean
+};
+
+const BackupProgressDialog: FunctionComponent = ({ open }) => {
+ return (
+
+ );
+};
+
+export default BackupProgressDialog;
diff --git a/src/apps/dashboard/features/backups/components/CreateBackupForm.tsx b/src/apps/dashboard/features/backups/components/CreateBackupForm.tsx
new file mode 100644
index 000000000..5d62d9e39
--- /dev/null
+++ b/src/apps/dashboard/features/backups/components/CreateBackupForm.tsx
@@ -0,0 +1,123 @@
+import React, { FunctionComponent, useCallback } from 'react';
+import globalize from 'lib/globalize';
+import type { BackupOptionsDto } from '@jellyfin/sdk/lib/generated-client/models/backup-options-dto';
+import Dialog from '@mui/material/Dialog';
+import DialogTitle from '@mui/material/DialogTitle';
+import DialogContent from '@mui/material/DialogContent';
+import Stack from '@mui/material/Stack';
+import DialogActions from '@mui/material/DialogActions';
+import Button from '@mui/material/Button';
+import FormControl from '@mui/material/FormControl';
+import FormControlLabel from '@mui/material/FormControlLabel';
+import Checkbox from '@mui/material/Checkbox';
+import FormGroup from '@mui/material/FormGroup';
+import DialogContentText from '@mui/material/DialogContentText';
+
+type IProps = {
+ open: boolean,
+ onClose?: () => void,
+ onCreate: (backupOptions: BackupOptionsDto) => void
+};
+
+const CreateBackupForm: FunctionComponent = ({ open, onClose, onCreate }) => {
+ const onSubmit = useCallback((e: React.FormEvent) => {
+ e.preventDefault();
+
+ const formData = new FormData(e.currentTarget);
+
+ const data = Object.fromEntries(formData.entries());
+
+ const backupOptions: BackupOptionsDto = {
+ 'Metadata': data.Metadata?.toString() === 'on',
+ 'Trickplay': data.Trickplay?.toString() === 'on',
+ 'Subtitles': data.Subtitles?.toString() === 'on'
+ };
+
+ onCreate(backupOptions);
+ }, [ onCreate ]);
+
+ return (
+
+ );
+};
+
+export default CreateBackupForm;
diff --git a/src/apps/dashboard/features/backups/components/RestoreConfirmationDialog.tsx b/src/apps/dashboard/features/backups/components/RestoreConfirmationDialog.tsx
new file mode 100644
index 000000000..0456e1122
--- /dev/null
+++ b/src/apps/dashboard/features/backups/components/RestoreConfirmationDialog.tsx
@@ -0,0 +1,46 @@
+import Button from '@mui/material/Button';
+import Dialog from '@mui/material/Dialog';
+import DialogActions from '@mui/material/DialogActions';
+import DialogContent from '@mui/material/DialogContent';
+import DialogContentText from '@mui/material/DialogContentText';
+import DialogTitle from '@mui/material/DialogTitle';
+import globalize from 'lib/globalize';
+import React, { FunctionComponent } from 'react';
+
+type IProps = {
+ open: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+};
+
+const RestoreConfirmationDialog: FunctionComponent = ({ open, onClose, onConfirm }: IProps) => {
+ return (
+
+ );
+};
+
+export default RestoreConfirmationDialog;
diff --git a/src/apps/dashboard/features/backups/components/RestoreProgressDialog.tsx b/src/apps/dashboard/features/backups/components/RestoreProgressDialog.tsx
new file mode 100644
index 000000000..937aaafba
--- /dev/null
+++ b/src/apps/dashboard/features/backups/components/RestoreProgressDialog.tsx
@@ -0,0 +1,32 @@
+import React, { FunctionComponent } from 'react';
+import Dialog from '@mui/material/Dialog';
+import DialogContent from '@mui/material/DialogContent';
+import DialogTitle from '@mui/material/DialogTitle';
+import LinearProgress from '@mui/material/LinearProgress';
+import DialogContentText from '@mui/material/DialogContentText';
+import Stack from '@mui/material/Stack';
+import globalize from 'lib/globalize';
+
+type IProps = {
+ open: boolean
+};
+
+const RestoreProgressDialog: FunctionComponent = ({ open }) => {
+ return (
+
+ );
+};
+
+export default RestoreProgressDialog;
diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts
index 2ab21c5d4..10d4fb838 100644
--- a/src/apps/dashboard/routes/_asyncRoutes.ts
+++ b/src/apps/dashboard/routes/_asyncRoutes.ts
@@ -3,6 +3,7 @@ import { AppType } from 'constants/appType';
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'activity', type: AppType.Dashboard },
+ { path: 'backups', type: AppType.Dashboard },
{ path: 'branding', type: AppType.Dashboard },
{ path: 'devices', type: AppType.Dashboard },
{ path: 'settings', type: AppType.Dashboard },
diff --git a/src/apps/dashboard/routes/backups/index.tsx b/src/apps/dashboard/routes/backups/index.tsx
new file mode 100644
index 000000000..6cbf92e27
--- /dev/null
+++ b/src/apps/dashboard/routes/backups/index.tsx
@@ -0,0 +1,192 @@
+import Box from '@mui/material/Box';
+import Button from '@mui/material/Button';
+import Stack from '@mui/material/Stack';
+import Typography from '@mui/material/Typography';
+import AddIcon from '@mui/icons-material/Add';
+import { useBackups } from 'apps/dashboard/features/backups/api/useBackups';
+import Page from 'components/Page';
+import globalize from 'lib/globalize';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import Loading from 'components/loading/LoadingComponent';
+import Alert from '@mui/material/Alert';
+import List from '@mui/material/List';
+import CreateBackupForm from 'apps/dashboard/features/backups/components/CreateBackupForm';
+import type { BackupOptionsDto } from '@jellyfin/sdk/lib/generated-client/models/backup-options-dto';
+import type { BackupManifestDto } from '@jellyfin/sdk/lib/generated-client/models/backup-manifest-dto';
+import { useCreateBackup } from 'apps/dashboard/features/backups/api/useCreateBackup';
+import BackupProgressDialog from 'apps/dashboard/features/backups/components/BackupProgressDialog';
+import Backup from 'apps/dashboard/features/backups/components/Backup';
+import SimpleAlert from 'components/SimpleAlert';
+import RestoreConfirmationDialog from 'apps/dashboard/features/backups/components/RestoreConfirmationDialog';
+import { useRestoreBackup } from 'apps/dashboard/features/backups/api/useRestoreBackup';
+import RestoreProgressDialog from 'apps/dashboard/features/backups/components/RestoreProgressDialog';
+import { useApi } from 'hooks/useApi';
+import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
+
+export const Component = () => {
+ const { api } = useApi();
+ const { data: backups, isPending, isError } = useBackups();
+ const [ isCreateFormOpen, setIsCreateFormOpen ] = useState(false);
+ const [ backupInProgress, setBackupInProgress ] = useState(false);
+ const [ restoreInProgress, setRestoreInProgress ] = useState(false);
+ const [ isRestoreSuccess, setIsRestoreSuccess ] = useState(false);
+ const [ isErrorOccurred, setIsErrorOccurred ] = useState(false);
+ const [ isRestoreDialogOpen, setIsRestoreDialogOpen ] = useState(false);
+ const [ backupToRestore, setBackupToRestore ] = useState(null);
+ const createBackup = useCreateBackup();
+ const restoreBackup = useRestoreBackup();
+
+ const onCreateClick = useCallback(() => {
+ setIsCreateFormOpen(true);
+ }, []);
+
+ const onCreateFormClose = useCallback(() => {
+ setIsCreateFormOpen(false);
+ }, []);
+
+ const onRestoreDialogClose = useCallback(() => {
+ setIsRestoreDialogOpen(false);
+ }, []);
+
+ const onErrorAlertClose = useCallback(() => {
+ setIsErrorOccurred(false);
+ }, []);
+
+ const onRestoreSuccessAlertClose = useCallback(() => {
+ setIsRestoreSuccess(false);
+ }, []);
+
+ const onBackupCreate = useCallback((backupOptions: BackupOptionsDto) => {
+ setBackupInProgress(true);
+ setIsCreateFormOpen(false);
+ createBackup.mutate(backupOptions, {
+ onError: () => {
+ setIsErrorOccurred(true);
+ },
+ onSettled: () => {
+ setBackupInProgress(false);
+ }
+ });
+ }, [ createBackup ]);
+
+ const promptRestore = useCallback((backup: BackupManifestDto) => {
+ setIsRestoreDialogOpen(true);
+ setBackupToRestore(backup);
+ }, []);
+
+ const onRestoreConfirm = useCallback(() => {
+ if (backupToRestore?.Path) {
+ restoreBackup.mutate(backupToRestore?.Path, {
+ onSuccess: () => {
+ setRestoreInProgress(true);
+ },
+ onError: () => {
+ setIsErrorOccurred(true);
+ },
+ onSettled: () => {
+ setIsRestoreDialogOpen(false);
+ }
+ });
+ }
+ }, [backupToRestore?.Path, restoreBackup]);
+
+ useEffect(() => {
+ if (restoreInProgress) {
+ const serverCheckInterval = setInterval(() => {
+ void getSystemApi(api!)
+ .getPublicSystemInfo()
+ .then(() => {
+ setRestoreInProgress(false);
+ setIsRestoreSuccess(true);
+ clearInterval(serverCheckInterval);
+ });
+ }, 5000);
+
+ return () => {
+ clearInterval(serverCheckInterval);
+ };
+ }
+ }, [api, restoreInProgress]);
+
+ const sortedBackups = useMemo(() => (
+ backups?.sort((a, b) => {
+ if (a.DateCreated && b.DateCreated) {
+ return new Date(b.DateCreated).getTime() - new Date(a.DateCreated).getTime();
+ } else {
+ return 0;
+ }
+ })
+ ), [ backups ]);
+
+ if (isPending) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {isError ? (
+ {globalize.translate('BackupsPageLoadError')}
+ ) : (
+
+
+ {globalize.translate('TabBackups')}
+
+
+ {globalize.translate('HeaderBackupsHelp')}
+
+
+ }
+ onClick={onCreateClick}
+ >
+ {globalize.translate('ButtonCreateBackup')}
+
+
+
+ {backups.length > 0 && (
+
+ {sortedBackups?.map(backup => {
+ return ;
+ })}
+
+ )}
+
+
+ )}
+
+
+ );
+};
diff --git a/src/strings/en-us.json b/src/strings/en-us.json
index 5402157b2..411e56a72 100644
--- a/src/strings/en-us.json
+++ b/src/strings/en-us.json
@@ -76,6 +76,7 @@
"Backdrop": "Backdrop",
"Backdrops": "Backdrops",
"BackdropScreensaver": "Backdrop Screensaver",
+ "BackupsPageLoadError": "Failed to load backups page",
"Banner": "Banner",
"BirthDateValue": "Born: {0}",
"BirthLocation": "Birth location",
@@ -104,6 +105,7 @@
"ButtonCast": "Cast to Device",
"ButtonChangeServer": "Change Server",
"ButtonClose": "Close",
+ "ButtonCreateBackup": "Create Backup",
"ButtonEditOtherUserPreferences": "Edit this user's profile, image and personal preferences.",
"ButtonEditUser": "Edit user",
"ButtonForgotPassword": "Forgot Password",
@@ -190,6 +192,7 @@
"CopyStreamURLSuccess": "URL copied successfully.",
"CopyLogSuccess": "Log contents copied successfully.",
"CoverArtist": "Cover artist",
+ "Create": "Create",
"Creator": "Creator",
"CriticRating": "Critics rating",
"Cursive": "Cursive",
@@ -388,6 +391,7 @@
"HeaderAudioSettings": "Audio Settings",
"HeaderAutoDiscovery": "Network Discovery",
"HeaderBlockItemsWithNoRating": "Block items with no or unrecognized rating information",
+ "HeaderBackupsHelp": "Create and restore snapshots of your server which can include the database, metadata, subtitles and trickplay.",
"HeaderBranding": "Branding",
"HeaderCancelRecording": "Cancel Recording",
"HeaderCancelSeries": "Cancel Series",
@@ -617,6 +621,7 @@
"LabelAutomaticallyRefreshInternetMetadataEvery": "Automatically refresh metadata from the internet",
"LabelAutomaticDiscovery": "Enable Auto Discovery",
"LabelAutomaticDiscoveryHelp": "Allow applications to automatically detect Jellyfin by using UDP port 7359.",
+ "LabelBackupsUnavailable": "No backups available",
"LabelBaseUrl": "Base URL",
"LabelBaseUrlHelp": "Add a custom subdirectory to the server URL. For example: http://example.com/<baseurl>",
"LabelBackdropScreensaverInterval": "Backdrop Screensaver Interval",
@@ -661,6 +666,7 @@
"LabelCustomTagDelimiters": "Custom Tag Delimiter",
"LabelCustomTagDelimitersHelp": "Characters to be treated as delimiters to separate tags.",
"LabelDashboardTheme": "Server Dashboard theme",
+ "LabelDatabase": "Database",
"LabelDate": "Date",
"LabelDateAdded": "Date added",
"LabelDateAddedBehavior": "Date added behavior for new content",
@@ -881,6 +887,7 @@
"LabelRepositoryUrlHelp": "The location of the repository manifest you want to include.",
"LabelRequireHttps": "Require HTTPS",
"LabelRequireHttpsHelp": "If checked, the server will automatically redirect all requests over HTTP to HTTPS. This has no effect if the server is not listening on HTTPS.",
+ "LabelRestore": "Restore",
"LabelRuntimeMinutes": "Runtime",
"LabelSaveLocalMetadata": "Save artwork into media folders",
"LabelSaveLocalMetadataHelp": "Saving artwork into media folders will put them in a place where they can be easily edited.",
@@ -1109,6 +1116,8 @@
"MessageAddRepository": "If you wish to add a repository, click the button next to the header and fill out the requested information.",
"MessageAreYouSureDeleteSubtitles": "Are you sure you wish to delete this subtitle file?",
"MessageAreYouSureYouWishToRemoveMediaFolder": "Are you sure you wish to remove this media folder?",
+ "MessageBackupDisclaimer": "Depending on the size of your library, the backup process may take a while.",
+ "MessageBackupInProgress": "Backup in progress",
"MessageBrowsePluginCatalog": "Browse our plugin catalog to view available plugins.",
"MessageCancelSeriesTimerError": "An error occurred while canceling the series timer",
"MessageCancelTimerError": "An error occurred while canceling the timer",
@@ -1163,6 +1172,9 @@
"MessageReenableUser": "See below to reenable",
"MessageRenameMediaFolder": "Renaming a media library will cause all metadata to be lost, proceed with caution.",
"MessageRepositoryInstallDisclaimer": "WARNING: Installing a third party plugin repository carries risks. It may contain unstable or malicious code, and may change at any time. Only install repositories from authors that you trust.",
+ "MessageRestoreDisclaimer": "WARNING: You are about to restore from a previous backup. Your server will be rendered unusable for the duration of the process and any new data created since the backup will be lost.",
+ "MessageRestoreInProgress": "Restore in progress",
+ "MessageRestoreSuccess": "Restore successfully completed",
"MessageSent": "Message sent.",
"MessageSplitVersionsError": "An error occurred while splitting versions",
"MessageSyncPlayCreateGroupDenied": "Permission required to create a group.",
@@ -1185,6 +1197,7 @@
"MessageUnableToConnectToServer": "We're unable to connect to the selected server right now. Please ensure it is running and try again.",
"MessageUnauthorizedUser": "You are not authorized to access the server at this time. Please contact your server administrator for more information.",
"MessageUnsetContentHelp": "Content will be displayed as plain folders. For best results use the metadata manager to set the content types of sub-folders.",
+ "MessageWaitingForServer": "Waiting for server...",
"MetadataImagesLoadError": "Failed to load metadata settings",
"MetadataManager": "Metadata Manager",
"MetadataNfoLoadError": "Failed to load metadata NFO settings",
@@ -1565,12 +1578,14 @@
"SubtitleVerticalPositionHelp": "Line number where text appears. Positive numbers indicate top down. Negative numbers indicate bottom up.",
"SubtitleWhite": "White",
"SubtitleYellow": "Yellow",
+ "Success": "Success",
"Suggestions": "Suggestions",
"Sunday": "Sunday",
"SyncPlayAccessHelp": "The SyncPlay feature enables to sync playback with other devices. Select the level of access this user has to the SyncPlay.",
"SyncPlayGroupDefaultTitle": "{0}'s group",
"TabAccess": "Access",
"TabAdvanced": "Advanced",
+ "TabBackups": "Backups",
"TabCatalog": "Catalog",
"TabDashboard": "Dashboard",
"TabLatest": "Recently Added",
From 5029967528b28602775549af4f88096f9de9f489 Mon Sep 17 00:00:00 2001
From: viown <48097677+viown@users.noreply.github.com>
Date: Tue, 3 Jun 2025 22:47:05 +0300
Subject: [PATCH 2/3] Sort backups in hook
---
.../features/backups/api/useBackups.ts | 12 +++++++++++-
.../components/RestoreConfirmationDialog.tsx | 4 ++--
src/apps/dashboard/routes/backups/index.tsx | 18 +++++-------------
3 files changed, 18 insertions(+), 16 deletions(-)
diff --git a/src/apps/dashboard/features/backups/api/useBackups.ts b/src/apps/dashboard/features/backups/api/useBackups.ts
index 22b45d2ae..842879b4e 100644
--- a/src/apps/dashboard/features/backups/api/useBackups.ts
+++ b/src/apps/dashboard/features/backups/api/useBackups.ts
@@ -12,7 +12,17 @@ const fetchBackups = async (api: Api, options?: AxiosRequestConfig) => {
const response = await backupApi.listBackups(options);
- return response.data;
+ const backups = response.data;
+
+ backups.sort((a, b) => {
+ if (a.DateCreated && b.DateCreated) {
+ return new Date(b.DateCreated).getTime() - new Date(a.DateCreated).getTime();
+ } else {
+ return 0;
+ }
+ });
+
+ return backups;
};
export const useBackups = () => {
diff --git a/src/apps/dashboard/features/backups/components/RestoreConfirmationDialog.tsx b/src/apps/dashboard/features/backups/components/RestoreConfirmationDialog.tsx
index 0456e1122..3b51204b6 100644
--- a/src/apps/dashboard/features/backups/components/RestoreConfirmationDialog.tsx
+++ b/src/apps/dashboard/features/backups/components/RestoreConfirmationDialog.tsx
@@ -22,7 +22,7 @@ const RestoreConfirmationDialog: FunctionComponent = ({ open, onClose, o
fullWidth
>
- {globalize.translate('Restore')}
+ {globalize.translate('LabelRestore')}
@@ -36,7 +36,7 @@ const RestoreConfirmationDialog: FunctionComponent = ({ open, onClose, o
{globalize.translate('ButtonCancel')}
diff --git a/src/apps/dashboard/routes/backups/index.tsx b/src/apps/dashboard/routes/backups/index.tsx
index 6cbf92e27..79f6aed38 100644
--- a/src/apps/dashboard/routes/backups/index.tsx
+++ b/src/apps/dashboard/routes/backups/index.tsx
@@ -6,7 +6,7 @@ import AddIcon from '@mui/icons-material/Add';
import { useBackups } from 'apps/dashboard/features/backups/api/useBackups';
import Page from 'components/Page';
import globalize from 'lib/globalize';
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import Loading from 'components/loading/LoadingComponent';
import Alert from '@mui/material/Alert';
import List from '@mui/material/List';
@@ -88,7 +88,7 @@ export const Component = () => {
}
});
}
- }, [backupToRestore?.Path, restoreBackup]);
+ }, [backupToRestore, restoreBackup]);
useEffect(() => {
if (restoreInProgress) {
@@ -99,6 +99,8 @@ export const Component = () => {
setRestoreInProgress(false);
setIsRestoreSuccess(true);
clearInterval(serverCheckInterval);
+ }).catch(() => {
+ // Server is still down
});
}, 5000);
@@ -108,16 +110,6 @@ export const Component = () => {
}
}, [api, restoreInProgress]);
- const sortedBackups = useMemo(() => (
- backups?.sort((a, b) => {
- if (a.DateCreated && b.DateCreated) {
- return new Date(b.DateCreated).getTime() - new Date(a.DateCreated).getTime();
- } else {
- return 0;
- }
- })
- ), [ backups ]);
-
if (isPending) {
return ;
}
@@ -174,7 +166,7 @@ export const Component = () => {
{backups.length > 0 && (
- {sortedBackups?.map(backup => {
+ {backups.map(backup => {
return
Date: Wed, 4 Jun 2025 18:54:24 +0300
Subject: [PATCH 3/3] Apply review feedback
---
.../components/drawer/sections/AdvancedDrawerSection.tsx | 2 +-
.../features/backups/components/CreateBackupForm.tsx | 2 +-
src/apps/dashboard/routes/backups/index.tsx | 4 ++--
src/strings/en-us.json | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/apps/dashboard/components/drawer/sections/AdvancedDrawerSection.tsx b/src/apps/dashboard/components/drawer/sections/AdvancedDrawerSection.tsx
index 4d5508aba..940520528 100644
--- a/src/apps/dashboard/components/drawer/sections/AdvancedDrawerSection.tsx
+++ b/src/apps/dashboard/components/drawer/sections/AdvancedDrawerSection.tsx
@@ -44,7 +44,7 @@ const AdvancedDrawerSection = () => {
-
+
diff --git a/src/apps/dashboard/features/backups/components/CreateBackupForm.tsx b/src/apps/dashboard/features/backups/components/CreateBackupForm.tsx
index 5d62d9e39..000af98a1 100644
--- a/src/apps/dashboard/features/backups/components/CreateBackupForm.tsx
+++ b/src/apps/dashboard/features/backups/components/CreateBackupForm.tsx
@@ -112,7 +112,7 @@ const CreateBackupForm: FunctionComponent = ({ open, onClose, onCreate }
diff --git a/src/apps/dashboard/routes/backups/index.tsx b/src/apps/dashboard/routes/backups/index.tsx
index 79f6aed38..047e6fef8 100644
--- a/src/apps/dashboard/routes/backups/index.tsx
+++ b/src/apps/dashboard/routes/backups/index.tsx
@@ -117,7 +117,7 @@ export const Component = () => {
return (
@@ -149,7 +149,7 @@ export const Component = () => {
) : (
- {globalize.translate('TabBackups')}
+ {globalize.translate('HeaderBackups')}
{globalize.translate('HeaderBackupsHelp')}
diff --git a/src/strings/en-us.json b/src/strings/en-us.json
index 411e56a72..76e4d8c4b 100644
--- a/src/strings/en-us.json
+++ b/src/strings/en-us.json
@@ -1585,7 +1585,7 @@
"SyncPlayGroupDefaultTitle": "{0}'s group",
"TabAccess": "Access",
"TabAdvanced": "Advanced",
- "TabBackups": "Backups",
+ "HeaderBackups": "Backups",
"TabCatalog": "Catalog",
"TabDashboard": "Dashboard",
"TabLatest": "Recently Added",