diff --git a/src/main/resolve/backup.ts b/src/main/resolve/backup.ts index 39b672d..892a73d 100644 --- a/src/main/resolve/backup.ts +++ b/src/main/resolve/backup.ts @@ -14,6 +14,9 @@ import { } from '../utils/dirs' import { systemLogger } from '../utils/logger' import { Cron } from 'croner' +import { dialog } from 'electron' +import { existsSync } from 'fs' +import i18next from 'i18next' let backupCronJob: Cron | null = null @@ -196,3 +199,75 @@ export async function reinitScheduler(): Promise { await initWebdavBackupScheduler() await systemLogger.info('WebDAV backup scheduler reinitialized successfully') } + +/** + * 导出本地备份 + */ +export async function exportLocalBackup(): Promise { + const zip = new AdmZip() + if (existsSync(appConfigPath())) { + zip.addLocalFile(appConfigPath()) + } + if (existsSync(controledMihomoConfigPath())) { + zip.addLocalFile(controledMihomoConfigPath()) + } + if (existsSync(profileConfigPath())) { + zip.addLocalFile(profileConfigPath()) + } + if (existsSync(overrideConfigPath())) { + zip.addLocalFile(overrideConfigPath()) + } + if (existsSync(themesDir())) { + zip.addLocalFolder(themesDir(), 'themes') + } + if (existsSync(profilesDir())) { + zip.addLocalFolder(profilesDir(), 'profiles') + } + if (existsSync(overrideDir())) { + zip.addLocalFolder(overrideDir(), 'override') + } + if (existsSync(subStoreDir())) { + zip.addLocalFolder(subStoreDir(), 'substore') + } + + const date = new Date() + const zipFileName = `clash-party-backup-${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip` + const result = await dialog.showSaveDialog({ + title: i18next.t('localBackup.export.title'), + defaultPath: zipFileName, + filters: [ + { name: 'ZIP Files', extensions: ['zip'] }, + { name: 'All Files', extensions: ['*'] } + ] + }) + + if (!result.canceled && result.filePath) { + zip.writeZip(result.filePath) + await systemLogger.info(`Local backup exported to: ${result.filePath}`) + return true + } + return false +} + +/** + * 导入本地备份 + */ +export async function importLocalBackup(): Promise { + const result = await dialog.showOpenDialog({ + title: i18next.t('localBackup.import.title'), + filters: [ + { name: 'ZIP Files', extensions: ['zip'] }, + { name: 'All Files', extensions: ['*'] } + ], + properties: ['openFile'] + }) + + if (!result.canceled && result.filePaths.length > 0) { + const filePath = result.filePaths[0] + const zip = new AdmZip(filePath) + zip.extractAllTo(dataDir(), true) + await systemLogger.info(`Local backup imported from: ${filePath}`) + return true + } + return false +} diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index 543ae22..f674523 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -85,7 +85,7 @@ import { setupFirewall } from '../sys/misc' import { getRuntimeConfig, getRuntimeConfigStr } from '../core/factory' -import { listWebdavBackups, webdavBackup, webdavDelete, webdavRestore } from '../resolve/backup' +import { listWebdavBackups, webdavBackup, webdavDelete, webdavRestore, exportLocalBackup, importLocalBackup } from '../resolve/backup' import { getInterfaces } from '../sys/interface' import { closeTrayIcon, copyEnv, showTrayIcon, updateTrayIcon, updateTrayIconImmediate } from '../resolve/tray' import { registerShortcut } from '../resolve/shortcut' @@ -250,6 +250,8 @@ export function registerIpcMainHandlers(): void { ipcMain.handle('listWebdavBackups', ipcErrorWrapper(listWebdavBackups)) ipcMain.handle('webdavDelete', (_e, filename) => ipcErrorWrapper(webdavDelete)(filename)) ipcMain.handle('reinitWebdavBackupScheduler', ipcErrorWrapper(reinitScheduler)) + ipcMain.handle('exportLocalBackup', () => ipcErrorWrapper(exportLocalBackup)()) + ipcMain.handle('importLocalBackup', () => ipcErrorWrapper(importLocalBackup)()) ipcMain.handle('registerShortcut', (_e, oldShortcut, newShortcut, action) => ipcErrorWrapper(registerShortcut)(oldShortcut, newShortcut, action) ) diff --git a/src/renderer/src/components/settings/local-backup-config.tsx b/src/renderer/src/components/settings/local-backup-config.tsx new file mode 100644 index 0000000..9cb2915 --- /dev/null +++ b/src/renderer/src/components/settings/local-backup-config.tsx @@ -0,0 +1,81 @@ +import React, { useState } from 'react' +import SettingCard from '../base/base-setting-card' +import SettingItem from '../base/base-setting-item' +import { Button, useDisclosure } from '@heroui/react' +import { exportLocalBackup, importLocalBackup } from '@renderer/utils/ipc' +import { useTranslation } from 'react-i18next' +import BaseConfirmModal from '../base/base-confirm-modal' + +const LocalBackupConfig: React.FC = () => { + const { t } = useTranslation() + const { isOpen, onOpen, onClose } = useDisclosure() + const [importing, setImporting] = useState(false) + const [exporting, setExporting] = useState(false) + + const handleExport = async (): Promise => { + setExporting(true) + try { + const success = await exportLocalBackup() + if (success) { + new window.Notification(t('localBackup.notification.exportSuccess.title'), { + body: t('localBackup.notification.exportSuccess.body') + }) + } + } catch (e) { + alert(e) + } finally { + setExporting(false) + } + } + + const handleImport = async (): Promise => { + setImporting(true) + try { + const success = await importLocalBackup() + if (success) { + new window.Notification(t('localBackup.notification.importSuccess.title'), { + body: t('localBackup.notification.importSuccess.body') + }) + } + } catch (e) { + alert(t('common.error.importFailed', { error: e })) + } finally { + setImporting(false) + onClose() + } + } + + return ( + <> + + + + + + + + + + + ) +} + +export default LocalBackupConfig \ No newline at end of file diff --git a/src/renderer/src/locales/en-US.json b/src/renderer/src/locales/en-US.json index c6c19de..7a516b0 100644 --- a/src/renderer/src/locales/en-US.json +++ b/src/renderer/src/locales/en-US.json @@ -239,6 +239,17 @@ "webdav.restore.noBackups": "No backups available", "webdav.notification.backupSuccess.title": "Backup Successful", "webdav.notification.backupSuccess.body": "Backup file has been uploaded to WebDAV", + "localBackup.title": "Backup & Restore", + "localBackup.export.title": "Export Local Backup", + "localBackup.export.button": "Export Backup", + "localBackup.import.title": "Import Local Backup", + "localBackup.import.button": "Import Backup", + "localBackup.import.confirm.title": "Confirm Import", + "localBackup.import.confirm.body": "Importing local backup will overwrite all current configurations. Are you sure you want to continue?", + "localBackup.notification.exportSuccess.title": "Export Successful", + "localBackup.notification.exportSuccess.body": "Local backup has been exported to the specified location", + "localBackup.notification.importSuccess.title": "Import Successful", + "localBackup.notification.importSuccess.body": "Local backup has been successfully imported", "shortcuts.title": "Keyboard Shortcuts", "shortcuts.toggleWindow": "Toggle Window", "shortcuts.toggleFloatingWindow": "Toggle Floating Window", diff --git a/src/renderer/src/locales/fa-IR.json b/src/renderer/src/locales/fa-IR.json index dad27c5..5e91f4b 100644 --- a/src/renderer/src/locales/fa-IR.json +++ b/src/renderer/src/locales/fa-IR.json @@ -217,6 +217,17 @@ "webdav.restore.noBackups": "هیچ پشتیبانی موجود نیست", "webdav.notification.backupSuccess.title": "پشتیبان‌گیری موفق", "webdav.notification.backupSuccess.body": "فایل پشتیبان در WebDAV بارگذاری شد", + "localBackup.title": "پشتیبان‌گیری و بازیابی", + "localBackup.export.title": "صادر کردن پشتیبان محلی", + "localBackup.export.button": "صادر کردن پشتیبان", + "localBackup.import.title": "وارد کردن پشتیبان محلی", + "localBackup.import.button": "وارد کردن پشتیبان", + "localBackup.import.confirm.title": "تایید وارد کردن", + "localBackup.import.confirm.body": "وارد کردن پشتیبان محلی، تمام پیکربندی‌های فعلی را بازنویسی می‌کند. آیا مطمئن هستید که می‌خواهید ادامه دهید؟", + "localBackup.notification.exportSuccess.title": "صادر کردن موفق", + "localBackup.notification.exportSuccess.body": "پشتیبان محلی در مکان مشخص شده صادر شد", + "localBackup.notification.importSuccess.title": "وارد کردن موفق", + "localBackup.notification.importSuccess.body": "پشتیبان محلی با موفقیت وارد شد", "shortcuts.title": "میانبرهای صفحه کلید", "shortcuts.toggleWindow": "تغییر وضعیت پنجره", "shortcuts.toggleFloatingWindow": "تغییر وضعیت پنجره شناور", diff --git a/src/renderer/src/locales/ru-RU.json b/src/renderer/src/locales/ru-RU.json index 7090e62..fff6561 100644 --- a/src/renderer/src/locales/ru-RU.json +++ b/src/renderer/src/locales/ru-RU.json @@ -217,6 +217,17 @@ "webdav.restore.noBackups": "Нет доступных резервных копий", "webdav.notification.backupSuccess.title": "Резервное копирование успешно", "webdav.notification.backupSuccess.body": "Файл резервной копии загружен на WebDAV", + "localBackup.title": "Резервное копирование и восстановление", + "localBackup.export.title": "Экспорт локальной резервной копии", + "localBackup.export.button": "Экспорт резервной копии", + "localBackup.import.title": "Импорт локальной резервной копии", + "localBackup.import.button": "Импорт резервной копии", + "localBackup.import.confirm.title": "Подтвердить импорт", + "localBackup.import.confirm.body": "Импорт локальной резервной копии перезапишет все текущие конфигурации. Вы уверены, что хотите продолжить?", + "localBackup.notification.exportSuccess.title": "Экспорт успешен", + "localBackup.notification.exportSuccess.body": "Локальная резервная копия была экспортирована в указанное место", + "localBackup.notification.importSuccess.title": "Импорт успешен", + "localBackup.notification.importSuccess.body": "Локальная резервная копия была успешно импортирована", "shortcuts.title": "Горячие клавиши", "shortcuts.toggleWindow": "Показать/скрыть окно", "shortcuts.toggleFloatingWindow": "Показать/скрыть плавающее окно", diff --git a/src/renderer/src/locales/zh-CN.json b/src/renderer/src/locales/zh-CN.json index 05aa121..839e3c2 100644 --- a/src/renderer/src/locales/zh-CN.json +++ b/src/renderer/src/locales/zh-CN.json @@ -239,6 +239,17 @@ "webdav.restore.noBackups": "还没有备份", "webdav.notification.backupSuccess.title": "备份成功", "webdav.notification.backupSuccess.body": "备份文件已上传到 WebDAV", + "localBackup.title": "备份与恢复", + "localBackup.export.title": "导出本地备份", + "localBackup.export.button": "导出备份", + "localBackup.import.title": "导入本地备份", + "localBackup.import.button": "导入备份", + "localBackup.import.confirm.title": "确认导入", + "localBackup.import.confirm.body": "导入本地备份将会覆盖当前所有配置,确定要继续吗?", + "localBackup.notification.exportSuccess.title": "导出成功", + "localBackup.notification.exportSuccess.body": "本地备份已导出到指定位置", + "localBackup.notification.importSuccess.title": "导入成功", + "localBackup.notification.importSuccess.body": "本地备份已成功导入", "shortcuts.title": "快捷键设置", "shortcuts.toggleWindow": "打开/关闭窗口", "shortcuts.toggleFloatingWindow": "打开/关闭悬浮窗", diff --git a/src/renderer/src/pages/settings.tsx b/src/renderer/src/pages/settings.tsx index 31e5c8a..fb7be25 100644 --- a/src/renderer/src/pages/settings.tsx +++ b/src/renderer/src/pages/settings.tsx @@ -10,6 +10,7 @@ import ShortcutConfig from '@renderer/components/settings/shortcut-config' import { FaTelegramPlane } from 'react-icons/fa' import SiderConfig from '@renderer/components/settings/sider-config' import SubStoreConfig from '@renderer/components/settings/substore-config' +import LocalBackupConfig from '@renderer/components/settings/local-backup-config' import { useTranslation } from 'react-i18next' const Settings: React.FC = () => { @@ -65,6 +66,7 @@ const Settings: React.FC = () => { + ) diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index 3d8fe3f..5b8dbe2 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -353,6 +353,15 @@ export async function reinitWebdavBackupScheduler(): Promise { ) } +// 本地备份相关 IPC 调用 +export async function exportLocalBackup(): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('exportLocalBackup')) +} + +export async function importLocalBackup(): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('importLocalBackup')) +} + export async function setTitleBarOverlay(overlay: TitleBarOverlayOptions): Promise { try { return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setTitleBarOverlay', overlay))