clash-verge-rev/src/components/setting/mods/auto-backup-settings.tsx
Sline 838e401796
feat(auto-backup): implement centralized auto-backup manager and UI (#5374)
* feat(auto-backup): implement centralized auto-backup manager and UI

- Introduced AutoBackupManager to handle verge settings, run a background scheduler, debounce change-driven backups, and trim auto-labeled archives (keeps 20); wired into startup and config refresh hooks
  (src-tauri/src/module/auto_backup.rs:28-209, src-tauri/src/utils/resolve/mod.rs:64-136, src-tauri/src/feat/config.rs:102-238)

- Extended verge schema and backup helpers so scheduled/change-based settings persist, create_local_backup can rename archives, and profile/global-extend mutations now trigger backups
  (src-tauri/src/config/verge.rs:162-536, src/types/types.d.ts:857-859, src-tauri/src/feat/backup.rs:125-189, src-tauri/src/cmd/profile.rs:66-476, src-tauri/src/cmd/save_profile.rs:21-82)

- Added Auto Backup settings panel in backup dialog with dual toggles + interval selector; localized new strings across all locales
  (src/components/setting/mods/auto-backup-settings.tsx:1-138, src/components/setting/mods/backup-viewer.tsx:28-309, src/locales/en/settings.json:312-326 and mirrored entries)

- Regenerated typed i18n resources for strong typing in React
  (src/types/generated/i18n-keys.ts, src/types/generated/i18n-resources.ts)

* refactor(setting/backup): restructure backup dialog for consistent layout

* refactor(ui): unify settings dialog style

* fix(backup): only trigger auto-backup on valid saves & restore restarts app safely

* fix(backup): scrub console.log leak and rewire WebDAV dialog to actually probe server

* refactor: rename SubscriptionChange to ProfileChange

* chore: update i18n

* chore: WebDAV i18n improvements

* refactor(backup): error handling

* refactor(auto-backup): wrap scheduler startup with maybe_start_runner

* refactor: remove the redundant throw in handleExport

* feat(backup-history-viewer): improve WebDAV handling and UI fallback

* feat(auto-backup): trigger backups on all profile edits & improve interval input UX

* refactor: use InputAdornment

* docs: Changelog.md
2025-11-10 13:49:14 +08:00

206 lines
5.7 KiB
TypeScript

import {
InputAdornment,
ListItem,
ListItemText,
Stack,
TextField,
} from "@mui/material";
import { useLockFn } from "ahooks";
import { Fragment, useMemo, useState, type ChangeEvent } from "react";
import { useTranslation } from "react-i18next";
import { Switch } from "@/components/base";
import { useVerge } from "@/hooks/use-verge";
import { showNotice } from "@/services/noticeService";
const MIN_INTERVAL_HOURS = 1;
const MAX_INTERVAL_HOURS = 168;
interface AutoBackupState {
scheduleEnabled: boolean;
intervalHours: number;
changeEnabled: boolean;
}
export function AutoBackupSettings() {
const { t } = useTranslation();
const { verge, patchVerge } = useVerge();
const derivedValues = useMemo<AutoBackupState>(() => {
return {
scheduleEnabled: verge?.enable_auto_backup_schedule ?? false,
intervalHours: verge?.auto_backup_interval_hours ?? 24,
changeEnabled: verge?.auto_backup_on_change ?? true,
};
}, [
verge?.enable_auto_backup_schedule,
verge?.auto_backup_interval_hours,
verge?.auto_backup_on_change,
]);
const [pendingValues, setPendingValues] = useState<AutoBackupState | null>(
null,
);
const values = useMemo(() => {
if (!pendingValues) {
return derivedValues;
}
if (
pendingValues.scheduleEnabled === derivedValues.scheduleEnabled &&
pendingValues.intervalHours === derivedValues.intervalHours &&
pendingValues.changeEnabled === derivedValues.changeEnabled
) {
return derivedValues;
}
return pendingValues;
}, [pendingValues, derivedValues]);
const [intervalInputDraft, setIntervalInputDraft] = useState<string | null>(
null,
);
const applyPatch = useLockFn(
async (
partial: Partial<AutoBackupState>,
payload: Partial<IVergeConfig>,
) => {
const nextValues = { ...values, ...partial };
setPendingValues(nextValues);
try {
await patchVerge(payload);
} catch (error) {
showNotice.error(error);
setPendingValues(null);
}
},
);
const disabled = !verge;
const handleScheduleToggle = (
_: ChangeEvent<HTMLInputElement>,
checked: boolean,
) => {
applyPatch(
{ scheduleEnabled: checked },
{
enable_auto_backup_schedule: checked,
auto_backup_interval_hours: values.intervalHours,
},
);
};
const handleChangeToggle = (
_: ChangeEvent<HTMLInputElement>,
checked: boolean,
) => {
applyPatch({ changeEnabled: checked }, { auto_backup_on_change: checked });
};
const handleIntervalInputChange = (event: ChangeEvent<HTMLInputElement>) => {
setIntervalInputDraft(event.target.value);
};
const commitIntervalInput = () => {
const rawValue = intervalInputDraft ?? values.intervalHours.toString();
const trimmed = rawValue.trim();
if (trimmed === "") {
setIntervalInputDraft(null);
return;
}
const parsed = Number(trimmed);
if (!Number.isFinite(parsed)) {
setIntervalInputDraft(null);
return;
}
const clamped = Math.min(
MAX_INTERVAL_HOURS,
Math.max(MIN_INTERVAL_HOURS, Math.round(parsed)),
);
if (clamped === values.intervalHours) {
setIntervalInputDraft(null);
return;
}
applyPatch(
{ intervalHours: clamped },
{ auto_backup_interval_hours: clamped },
);
setIntervalInputDraft(null);
};
const scheduleDisabled = disabled || !values.scheduleEnabled;
return (
<Fragment>
<ListItem divider disableGutters>
<Stack direction="row" alignItems="center" spacing={1} width="100%">
<ListItemText
primary={t("settings.modals.backup.auto.scheduleLabel")}
secondary={t("settings.modals.backup.auto.scheduleHelper")}
/>
<Switch
edge="end"
checked={values.scheduleEnabled}
onChange={handleScheduleToggle}
disabled={disabled}
/>
</Stack>
</ListItem>
<ListItem divider disableGutters>
<Stack direction="row" alignItems="center" spacing={2} width="100%">
<ListItemText
primary={t("settings.modals.backup.auto.intervalLabel")}
/>
<TextField
label={t("settings.modals.backup.auto.intervalLabel")}
size="small"
type="number"
value={intervalInputDraft ?? values.intervalHours.toString()}
disabled={scheduleDisabled}
onChange={handleIntervalInputChange}
onBlur={commitIntervalInput}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
commitIntervalInput();
}
}}
sx={{ minWidth: 160 }}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">
{t("shared.units.hours")}
</InputAdornment>
),
},
htmlInput: {
min: MIN_INTERVAL_HOURS,
max: MAX_INTERVAL_HOURS,
inputMode: "numeric",
},
}}
/>
</Stack>
</ListItem>
<ListItem divider disableGutters>
<Stack direction="row" alignItems="center" spacing={1} width="100%">
<ListItemText
primary={t("settings.modals.backup.auto.changeLabel")}
secondary={t("settings.modals.backup.auto.changeHelper")}
/>
<Switch
edge="end"
checked={values.changeEnabled}
onChange={handleChangeToggle}
disabled={disabled}
/>
</Stack>
</ListItem>
</Fragment>
);
}