feat: enhance backup functionality with attachment options

- Added support for including attachments in backup exports.
- Updated backup-related interfaces and functions to handle attachment options.
- Introduced a new UI component for selecting attachment inclusion during backup operations.
- Modified existing components to integrate the new attachment functionality.
- Improved user feedback and error handling during backup processes.
This commit is contained in:
shuaiplus
2026-03-20 04:55:23 +08:00
parent 3d38424d77
commit cbf1e86881
19 changed files with 883 additions and 352 deletions
@@ -9,6 +9,7 @@ import { COMMON_TIME_ZONES, WEEKDAY_OPTIONS, getDestinationTypeLabel } from '@/l
import type { RecommendedProvider } from '@/lib/backup-recommendations';
import { RemoteBackupBrowser } from './RemoteBackupBrowser';
import { t } from '@/lib/i18n';
import { BackupIncludeAttachmentsField } from './BackupIncludeAttachmentsField';
interface BackupDestinationDetailProps {
selectedRecommendedProvider: RecommendedProvider | null;
@@ -287,6 +288,15 @@ export function BackupDestinationDetail(props: BackupDestinationDetailProps) {
</label>
</div>
<BackupIncludeAttachmentsField
checked={props.selectedDestination.includeAttachments}
disabled={props.loadingSettings || props.disableWhileBusy}
onChange={(checked) => props.onUpdateDestination((destination) => ({
...destination,
includeAttachments: checked,
}))}
/>
{props.selectedDestination.schedule.frequency === 'weekly' ? (
<div className="field-grid backup-detail-schedule-extra-grid">
<label className="field">
@@ -0,0 +1,57 @@
import { useEffect, useRef, useState } from 'preact/hooks';
import { t } from '@/lib/i18n';
interface BackupIncludeAttachmentsFieldProps {
checked: boolean;
disabled?: boolean;
showHelp?: boolean;
onChange: (checked: boolean) => void;
}
export function BackupIncludeAttachmentsField(props: BackupIncludeAttachmentsFieldProps) {
const [open, setOpen] = useState(false);
const wrapRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!open) return;
function handlePointerDown(event: PointerEvent) {
if (!wrapRef.current?.contains(event.target as Node)) {
setOpen(false);
}
}
document.addEventListener('pointerdown', handlePointerDown);
return () => document.removeEventListener('pointerdown', handlePointerDown);
}, [open]);
return (
<div className="backup-option-field">
<label className="backup-option-label">
<input
type="checkbox"
checked={props.checked}
disabled={props.disabled}
onInput={(event) => props.onChange((event.currentTarget as HTMLInputElement).checked)}
/>
<span>{t('txt_backup_include_attachments')}</span>
</label>
{props.showHelp !== false ? (
<div ref={wrapRef} className={`backup-help-wrap ${open ? 'open' : ''}`}>
<button
type="button"
className="backup-help-trigger"
aria-label={t('txt_backup_include_attachments_help_button')}
aria-expanded={open ? 'true' : 'false'}
onClick={() => setOpen((current) => !current)}
>
?
</button>
<div className="backup-help-bubble" role="tooltip">
{t('txt_backup_include_attachments_help')}
</div>
</div>
) : null}
</div>
);
}
@@ -2,16 +2,19 @@ import { Download, FileUp } from 'lucide-preact';
import type { RecommendedProvider } from '@/lib/backup-recommendations';
import { hasLinkedStorages } from '@/lib/backup-recommendations';
import { t } from '@/lib/i18n';
import { BackupIncludeAttachmentsField } from './BackupIncludeAttachmentsField';
interface BackupOperationsSidebarProps {
disableWhileBusy: boolean;
exporting: boolean;
importing: boolean;
exportIncludeAttachments: boolean;
selectedProviderId: string | null;
recommendedWebDavProviders: RecommendedProvider[];
recommendedS3Providers: RecommendedProvider[];
onExport: () => void;
onImport: () => void;
onExportIncludeAttachmentsChange: (checked: boolean) => void;
onSelectProvider: (providerId: string) => void;
}
@@ -26,6 +29,12 @@ export function BackupOperationsSidebar(props: BackupOperationsSidebarProps) {
<Download size={14} className="btn-icon" />
{props.exporting ? t('txt_backup_exporting') : t('txt_backup_export')}
</button>
<BackupIncludeAttachmentsField
checked={props.exportIncludeAttachments}
disabled={props.disableWhileBusy}
showHelp={false}
onChange={props.onExportIncludeAttachmentsChange}
/>
<button type="button" className="btn btn-secondary" disabled={props.disableWhileBusy} onClick={props.onImport}>
<FileUp size={14} className="btn-icon" />
{props.importing ? t('txt_backup_restoring') : t('txt_backup_import')}
@@ -102,22 +102,22 @@ export function RemoteBackupBrowser(props: RemoteBackupBrowserProps) {
<FolderOpen size={14} className="btn-icon" />
{t('txt_backup_remote_open')}
</button>
) : (
) : isZipCandidate(item) ? (
<>
<button type="button" className="btn btn-secondary small" disabled={props.disableWhileBusy || props.downloadingRemotePath === item.path || !isZipCandidate(item)} onClick={() => props.onDownload(item.path)}>
<button type="button" className="btn btn-secondary small" disabled={props.disableWhileBusy || props.downloadingRemotePath === item.path} onClick={() => props.onDownload(item.path)}>
<Download size={14} className="btn-icon" />
{getDownloadLabel(item.path)}
</button>
<button type="button" className="btn btn-primary small" disabled={props.disableWhileBusy || props.restoringRemotePath === item.path || !isZipCandidate(item)} onClick={() => props.onRestore(item.path)}>
<button type="button" className="btn btn-primary small" disabled={props.disableWhileBusy || props.restoringRemotePath === item.path} onClick={() => props.onRestore(item.path)}>
<RotateCcw size={14} className="btn-icon" />
{props.restoringRemotePath === item.path ? t('txt_backup_restoring') : t('txt_backup_remote_restore')}
</button>
<button type="button" className="btn btn-danger small" disabled={props.disableWhileBusy || props.deletingRemotePath === item.path || !isZipCandidate(item)} onClick={() => props.onPromptDelete(item.path)}>
<button type="button" className="btn btn-danger small" disabled={props.disableWhileBusy || props.deletingRemotePath === item.path} onClick={() => props.onPromptDelete(item.path)}>
<Trash2 size={14} className="btn-icon" />
{props.deletingRemotePath === item.path ? t('txt_backup_remote_deleting') : t('txt_delete')}
</button>
</>
)}
) : null}
</div>
</div>
))}