diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a9e7ec93..dc5126fb 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -55,7 +55,7 @@ tauri-plugin-deep-link = "2.0.1" tauri-plugin-devtools = "2.0.0-rc" url = "2.5.2" zip = "2.2.0" -reqwest_dav = "=0.1.14" +reqwest_dav = "0.1.14" [target.'cfg(windows)'.dependencies] runas = "=1.2.0" deelevate = "0.2.0" diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index d532aa63..5a71440f 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -185,7 +185,7 @@ pub async fn change_clash_core(clash_core: Option) -> CmdResult { /// restart the sidecar #[tauri::command] -pub async fn restart_sidecar() -> CmdResult { +pub async fn restart_core() -> CmdResult { wrap_err!(CoreManager::global().restart_core().await) } @@ -396,15 +396,28 @@ pub async fn save_webdav_config(url: String, username: String, password: String) #[tauri::command] pub async fn create_webdav_backup() -> CmdResult<()> { - feat::create_backup_and_upload_webdav() - .await - .map_err(|err| err.to_string())?; - Ok(()) + wrap_err!(feat::create_backup_and_upload_webdav().await) } #[tauri::command] pub async fn list_webdav_backup() -> CmdResult> { - feat::list_wevdav_backup().await.map_err(|e| e.to_string()) + wrap_err!(feat::list_wevdav_backup().await) +} + +#[tauri::command] +pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> { + wrap_err!(feat::delete_webdav_backup(filename).await) +} + +#[tauri::command] +pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> { + wrap_err!(feat::restore_webdav_backup(filename).await) +} + +#[tauri::command] +pub async fn restart_app() -> CmdResult<()> { + feat::restart_app(); + Ok(()) } pub mod service { diff --git a/src-tauri/src/core/backup.rs b/src-tauri/src/core/backup.rs index c1604359..f5e5d766 100644 --- a/src-tauri/src/core/backup.rs +++ b/src-tauri/src/core/backup.rs @@ -40,14 +40,27 @@ impl WebDavClient { let url = verge.webdav_url.unwrap_or_default(); let username = verge.webdav_username.unwrap_or_default(); let password = verge.webdav_password.unwrap_or_default(); - + let url = url.trim_end_matches('/'); let client = reqwest_dav::ClientBuilder::new() + .set_agent( + reqwest::Client::builder() + .danger_accept_invalid_certs(true) + .build() + .unwrap(), + ) .set_host(url.to_owned()) .set_auth(reqwest_dav::Auth::Basic( username.to_owned(), password.to_owned(), )) .build()?; + if let Err(_) = client + .list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(0)) + .await + { + client.mkcol(dirs::BACKUP_DIR).await?; + } + *self.client.lock() = Some(client.clone()); } Ok(self.client.lock().clone().unwrap()) @@ -61,10 +74,6 @@ impl WebDavClient { pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> { let client = self.get_client().await?; - if client.get(dirs::BACKUP_DIR).await.is_err() { - client.mkcol(dirs::BACKUP_DIR).await?; - } - let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name); client .put(webdav_path.as_ref(), fs::read(file_path)?) @@ -72,7 +81,16 @@ impl WebDavClient { Ok(()) } - pub async fn list_files(&self) -> Result, Error> { + pub async fn download(&self, filename: String, storage_path: PathBuf) -> Result<(), Error> { + let client = self.get_client().await?; + let path = format!("{}/{}", dirs::BACKUP_DIR, filename); + let response = client.get(&path.as_str()).await?; + let content = response.bytes().await?; + fs::write(&storage_path, &content)?; + Ok(()) + } + + pub async fn list(&self) -> Result, Error> { let client = self.get_client().await?; let files = client .list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(1)) @@ -85,6 +103,13 @@ impl WebDavClient { } Ok(final_files) } + + pub async fn delete(&self, file_name: String) -> Result<(), Error> { + let client = self.get_client().await?; + let path = format!("{}/{}", dirs::BACKUP_DIR, file_name); + client.delete(&path).await?; + Ok(()) + } } pub fn create_backup() -> Result<(String, PathBuf), Error> { @@ -109,8 +134,17 @@ pub fn create_backup() -> Result<(String, PathBuf), Error> { } zip.start_file(dirs::CLASH_CONFIG, options)?; zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?; + + let mut verge_config: serde_json::Value = + serde_yaml::from_str(&fs::read_to_string(dirs::verge_path()?)?)?; + if let Some(obj) = verge_config.as_object_mut() { + obj.remove("webdav_username"); + obj.remove("webdav_password"); + obj.remove("webdav_url"); + } zip.start_file(dirs::VERGE_CONFIG, options)?; - zip.write_all(fs::read(dirs::verge_path()?)?.as_slice())?; + zip.write_all(serde_yaml::to_string(&verge_config)?.as_bytes())?; + zip.start_file(dirs::PROFILE_YAML, options)?; zip.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?; zip.finish()?; diff --git a/src-tauri/src/core/core.rs b/src-tauri/src/core/core.rs index ea02df95..947fa56f 100644 --- a/src-tauri/src/core/core.rs +++ b/src-tauri/src/core/core.rs @@ -87,6 +87,7 @@ impl CoreManager { service::stop_core_by_service().await?; } *running = false; + Ok(()) } diff --git a/src-tauri/src/core/tray.rs b/src-tauri/src/core/tray.rs index 834cdb2a..7e69c0f9 100644 --- a/src-tauri/src/core/tray.rs +++ b/src-tauri/src/core/tray.rs @@ -1,14 +1,14 @@ use crate::{ cmds, config::Config, - core::CoreManager, - feat, log_err, t, + feat, t, utils::{ dirs, resolve::{self, VERSION}, }, }; use anyhow::Result; +use tauri::AppHandle; use tauri::{ menu::CheckMenuItem, tray::{MouseButton, MouseButtonState, TrayIconEvent, TrayIconId}, @@ -17,7 +17,6 @@ use tauri::{ menu::{MenuEvent, MenuItem, PredefinedMenuItem, Submenu}, Wry, }; -use tauri::{AppHandle, Manager}; use super::handle; pub struct Tray {} @@ -408,7 +407,7 @@ fn create_tray_menu( Ok(menu) } -fn on_menu_event(app_handle: &AppHandle, event: MenuEvent) { +fn on_menu_event(_: &AppHandle, event: MenuEvent) { match event.id.as_ref() { mode @ ("rule_mode" | "global_mode" | "direct_mode") => { let mode = &mode[0..mode.len() - 5]; @@ -423,15 +422,7 @@ fn on_menu_event(app_handle: &AppHandle, event: MenuEvent) { "open_core_dir" => crate::log_err!(cmds::open_core_dir()), "open_logs_dir" => crate::log_err!(cmds::open_logs_dir()), "restart_clash" => feat::restart_clash_core(), - "restart_app" => { - tauri::async_runtime::block_on(async move { - log_err!(CoreManager::global().stop_core().await); - }); - resolve::resolve_reset(); - //睡1秒再重启 - std::thread::sleep(std::time::Duration::from_secs(1)); - tauri::process::restart(&app_handle.env()); - } + "restart_app" => feat::restart_app(), "quit" => { println!("quit"); feat::quit(Some(0)); diff --git a/src-tauri/src/feat.rs b/src-tauri/src/feat.rs index c28108f6..9374e2aa 100644 --- a/src-tauri/src/feat.rs +++ b/src-tauri/src/feat.rs @@ -7,10 +7,13 @@ use crate::config::*; use crate::core::*; use crate::log_err; +use crate::utils::dirs::app_home_dir; use crate::utils::resolve; use anyhow::{bail, Result}; use reqwest_dav::list_cmd::ListFile; use serde_yaml::{Mapping, Value}; +use std::fs; +use tauri::Manager; use tauri_plugin_clipboard_manager::ClipboardExt; // 打开面板 @@ -40,6 +43,18 @@ pub fn restart_clash_core() { }); } +pub fn restart_app() { + tauri::async_runtime::spawn_blocking(|| { + tauri::async_runtime::block_on(async { + log_err!(CoreManager::global().stop_core().await); + }); + resolve::resolve_reset(); + let app_handle = handle::Handle::global().app_handle().unwrap(); + std::thread::sleep(std::time::Duration::from_secs(1)); + tauri::process::restart(&app_handle.env()); + }); +} + // 切换模式 rule/global/direct/script mode pub fn change_clash_mode(mode: String) { let mut mapping = Mapping::new(); @@ -425,11 +440,37 @@ pub async fn create_backup_and_upload_webdav() -> Result<()> { } pub async fn list_wevdav_backup() -> Result> { + backup::WebDavClient::global().list().await.map_err(|err| { + log::error!(target: "app", "Failed to list WebDAV backup files: {:#?}", err); + err + }) +} + +pub async fn delete_webdav_backup(filename: String) -> Result<()> { backup::WebDavClient::global() - .list_files() + .delete(filename) .await .map_err(|err| { - log::error!(target: "app", "Failed to list WebDAV backup files: {:#?}", err); + log::error!(target: "app", "Failed to delete WebDAV backup file: {:#?}", err); err }) } + +pub async fn restore_webdav_backup(filename: String) -> Result<()> { + let backup_storage_path = app_home_dir().unwrap().join(&filename); + backup::WebDavClient::global() + .download(filename, backup_storage_path.clone()) + .await + .map_err(|err| { + log::error!(target: "app", "Failed to download WebDAV backup file: {:#?}", err); + err + })?; + + // extract zip file + let mut zip = zip::ZipArchive::new(fs::File::open(backup_storage_path.clone())?)?; + zip.extract(app_home_dir()?)?; + + // 最后删除临时文件 + fs::remove_file(backup_storage_path)?; + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1d3ccec1..bf572749 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -84,7 +84,8 @@ pub fn run() { cmds::get_portable_flag, cmds::get_network_interfaces, // cmds::kill_sidecar, - cmds::restart_sidecar, + cmds::restart_core, + cmds::restart_app, // clash cmds::get_clash_info, cmds::get_clash_logs, @@ -128,6 +129,8 @@ pub fn run() { cmds::create_webdav_backup, cmds::save_webdav_config, cmds::list_webdav_backup, + cmds::delete_webdav_backup, + cmds::restore_webdav_backup, ]); #[cfg(debug_assertions)] diff --git a/src/components/setting/mods/backup-config-viewer.tsx b/src/components/setting/mods/backup-config-viewer.tsx new file mode 100644 index 00000000..fe4b99c2 --- /dev/null +++ b/src/components/setting/mods/backup-config-viewer.tsx @@ -0,0 +1,220 @@ +import { useState, useRef, memo, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useForm } from "react-hook-form"; +import { useVerge } from "@/hooks/use-verge"; +import { Notice } from "@/components/base"; +import { isValidUrl } from "@/utils/helper"; +import { useLockFn } from "ahooks"; +import { + TextField, + Button, + Grid, + Stack, + IconButton, + InputAdornment, +} from "@mui/material"; +import Visibility from "@mui/icons-material/Visibility"; +import VisibilityOff from "@mui/icons-material/VisibilityOff"; +import { saveWebdavConfig, createWebdavBackup } from "@/services/cmds"; + +export interface BackupConfigViewerProps { + onBackupSuccess: () => Promise; + onSaveSuccess: () => Promise; + onInit: () => Promise; + setLoading: (loading: boolean) => void; +} + +export const BackupConfigViewer = memo( + ({ + onBackupSuccess, + onSaveSuccess, + onInit, + setLoading, + }: BackupConfigViewerProps) => { + const { t } = useTranslation(); + const { verge } = useVerge(); + const { webdav_url, webdav_username, webdav_password } = verge || {}; + const [showPassword, setShowPassword] = useState(false); + const usernameRef = useRef(null); + const passwordRef = useRef(null); + const urlRef = useRef(null); + + const { register, handleSubmit, watch } = useForm({ + defaultValues: { + url: webdav_url, + username: webdav_username, + password: webdav_password, + }, + }); + const url = watch("url"); + const username = watch("username"); + const password = watch("password"); + + const webdavChanged = + webdav_url !== url || + webdav_username !== username || + webdav_password !== password; + + const handleClickShowPassword = () => { + setShowPassword((prev) => !prev); + }; + + useEffect(() => { + if (webdav_url && webdav_username && webdav_password) { + onInit(); + } + }, []); + + const checkForm = () => { + const username = usernameRef.current?.value; + const password = passwordRef.current?.value; + const url = urlRef.current?.value; + + if (!url) { + urlRef.current?.focus(); + Notice.error(t("WebDAV URL Required")); + throw new Error(t("WebDAV URL Required")); + } else if (!isValidUrl(url)) { + urlRef.current?.focus(); + Notice.error(t("Invalid WebDAV URL")); + throw new Error(t("Invalid WebDAV URL")); + } + if (!username) { + usernameRef.current?.focus(); + Notice.error(t("WebDAV URL Required")); + throw new Error(t("Username Required")); + } + if (!password) { + passwordRef.current?.focus(); + Notice.error(t("WebDAV URL Required")); + throw new Error(t("Password Required")); + } + }; + + const save = useLockFn(async (data: IWebDavConfig) => { + checkForm(); + try { + setLoading(true); + await saveWebdavConfig(data.url, data.username, data.password).then( + () => { + Notice.success(t("WebDAV Config Saved")); + onSaveSuccess(); + } + ); + } catch (error) { + Notice.error(t("WebDAV Config Save Failed", { error }), 3000); + } finally { + setLoading(false); + } + }); + + const handleBackup = useLockFn(async () => { + checkForm(); + try { + setLoading(true); + await createWebdavBackup().then(async () => { + await onBackupSuccess(); + Notice.success(t("Backup Created")); + }); + } catch (error) { + Notice.error(t("Backup Failed", { error })); + } finally { + setLoading(false); + } + }); + + return ( +
e.preventDefault()}> + + + + + + + + + + + + + {showPassword ? : } + + + ), + }} + /> + + + + + + {webdavChanged || + webdav_url === null || + webdav_username == null || + webdav_password == null ? ( + + ) : ( + + )} + + + +
+ ); + } +); diff --git a/src/components/setting/mods/backup-table-viewer.tsx b/src/components/setting/mods/backup-table-viewer.tsx new file mode 100644 index 00000000..ef1b7b7f --- /dev/null +++ b/src/components/setting/mods/backup-table-viewer.tsx @@ -0,0 +1,266 @@ +import { SVGProps, memo } from "react"; +import { + Box, + Paper, + IconButton, + Divider, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TablePagination, +} from "@mui/material"; +import { Notice } from "@/components/base"; +import { Typography } from "@mui/material"; +import { useLockFn } from "ahooks"; +import { useTranslation } from "react-i18next"; +import { Dayjs } from "dayjs"; +import { + deleteWebdavBackup, + restoreWebDavBackup, + restartApp, +} from "@/services/cmds"; +import DeleteIcon from "@mui/icons-material/Delete"; +import RestoreIcon from "@mui/icons-material/Restore"; + +export type BackupFile = IWebDavFile & { + platform: string; + backup_time: Dayjs; + allow_apply: boolean; +}; + +export const DEFAULT_ROWS_PER_PAGE = 5; + +export interface BackupTableViewerProps { + datasource: BackupFile[]; + page: number; + onPageChange: ( + event: React.MouseEvent | null, + page: number + ) => void; + total: number; + onRefresh: () => Promise; +} + +export const BackupTableViewer = memo( + ({ + datasource, + page, + onPageChange, + total, + onRefresh, + }: BackupTableViewerProps) => { + const { t } = useTranslation(); + + const handleDelete = useLockFn(async (filename: string) => { + await deleteWebdavBackup(filename); + await onRefresh(); + }); + + const handleRestore = useLockFn(async (filename: string) => { + await restoreWebDavBackup(filename).then(() => { + Notice.success(t("Restore Success, App will restart in 1s")); + }); + await restartApp(); + }); + + return ( + + + + + {t("Filename")} + {t("Backup Time")} + {t("Actions")} + + + + {datasource.length > 0 ? ( + datasource?.map((file, index) => ( + + + {file.platform === "windows" ? ( + + ) : file.platform === "linux" ? ( + + ) : ( + + )} + {file.filename} + + + {file.backup_time.fromNow()} + + + + { + e.preventDefault(); + const confirmed = await window.confirm( + t("Confirm to delete this backup file?") + ); + if (confirmed) { + await handleDelete(file.filename); + } + }} + > + + + + { + e.preventDefault(); + const confirmed = await window.confirm( + t("Confirm to restore this backup file?") + ); + if (confirmed) { + await handleRestore(file.filename); + } + }} + > + + + + + + )) + ) : ( + + + + + {t("No Backups")} + + + + + )} + +
+ +
+ ); + } +); + +function LinuxIcon(props: SVGProps) { + return ( + + + + + + + + + + + + + + + + ); +} + +function WindowsIcon(props: SVGProps) { + return ( + + + + ); +} + +function MacIcon(props: SVGProps) { + return ( + + + + ); +} diff --git a/src/components/setting/mods/backup-viewer.tsx b/src/components/setting/mods/backup-viewer.tsx index 492c4c52..a21eb58c 100644 --- a/src/components/setting/mods/backup-viewer.tsx +++ b/src/components/setting/mods/backup-viewer.tsx @@ -2,87 +2,25 @@ import { forwardRef, useImperativeHandle, useState, - useRef, - SVGProps, useCallback, - useMemo, - memo, + useEffect, } from "react"; import { useTranslation } from "react-i18next"; -import { useLockFn } from "ahooks"; -import { Typography } from "@mui/material"; -import { useForm, UseFormRegister } from "react-hook-form"; -import { useVerge } from "@/hooks/use-verge"; -import { BaseDialog, DialogRef, Notice } from "@/components/base"; -import { isValidUrl } from "@/utils/helper"; +import { BaseDialog, DialogRef } from "@/components/base"; import getSystem from "@/utils/get-system"; import { BaseLoadingOverlay } from "@/components/base"; -import dayjs, { Dayjs } from "dayjs"; +import dayjs from "dayjs"; import customParseFormat from "dayjs/plugin/customParseFormat"; +import { + BackupTableViewer, + BackupFile, + DEFAULT_ROWS_PER_PAGE, +} from "./backup-table-viewer"; +import { BackupConfigViewer } from "./backup-config-viewer"; +import { Box, Paper, Divider } from "@mui/material"; +import { listWebDavBackup } from "@/services/cmds"; dayjs.extend(customParseFormat); -import { - TextField, - Button, - Grid, - Box, - Paper, - Stack, - IconButton, - InputAdornment, - Divider, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TablePagination, -} from "@mui/material"; -import Visibility from "@mui/icons-material/Visibility"; -import VisibilityOff from "@mui/icons-material/VisibilityOff"; -import DeleteIcon from "@mui/icons-material/Delete"; -import RestoreIcon from "@mui/icons-material/Restore"; - -import { - createWebdavBackup, - listWebDavBackup, - saveWebdavConfig, -} from "@/services/cmds"; - -type BackupFile = IWebDavFile & { - platform: string; - backup_time: Dayjs; - allow_apply: boolean; -}; - -interface BackupTableProps { - datasource: BackupFile[]; - page: number; - rowsPerPage: number; - onPageChange: (event: any, newPage: number) => void; - onRowsPerPageChange: (event: React.ChangeEvent) => void; - totalCount: number; -} - -interface WebDAVConfigFormProps { - onSubmit: (e: React.FormEvent) => void; - initialValues: Partial; - urlRef: React.RefObject; - usernameRef: React.RefObject; - passwordRef: React.RefObject; - showPassword: boolean; - onShowPasswordClick: () => void; - webdavChanged: boolean; - webdavUrl: string | undefined | null; - webdavUsername: string | undefined | null; - webdavPassword: string | undefined | null; - handleBackup: () => Promise; - register: UseFormRegister; -} - -// 将魔法数字和配置提取为常量 -const DEFAULT_ROWS_PER_PAGE = 5; const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss"; const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/; @@ -90,43 +28,17 @@ export const BackupViewer = forwardRef((props, ref) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); - const { verge, mutateVerge } = useVerge(); - const { webdav_url, webdav_username, webdav_password } = verge || {}; - const [showPassword, setShowPassword] = useState(false); - const usernameRef = useRef(null); - const passwordRef = useRef(null); - const [backupState, setBackupState] = useState({ - files: [] as BackupFile[], - page: 0, - rowsPerPage: DEFAULT_ROWS_PER_PAGE, - isLoading: false, - }); + const [isLoading, setIsLoading] = useState(false); + const [backupFiles, setBackupFiles] = useState([]); + const [dataSource, setDataSource] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(0); const OS = getSystem(); - const urlRef = useRef(null); - - const { register, handleSubmit, watch } = useForm({ - defaultValues: { - url: webdav_url, - username: webdav_username, - password: webdav_password, - }, - }); - - const url = watch("url"); - const username = watch("username"); - const password = watch("password"); - const webdavChanged = - webdav_url !== url || - webdav_username !== username || - webdav_password !== password; useImperativeHandle(ref, () => ({ open: () => { setOpen(true); - if (webdav_url && webdav_username && webdav_password) { - fetchAndSetBackupFiles(); - } }, close: () => setOpen(false), })); @@ -134,111 +46,37 @@ export const BackupViewer = forwardRef((props, ref) => { // Handle page change const handleChangePage = useCallback( (_: React.MouseEvent | null, page: number) => { - setBackupState((prev) => ({ ...prev, page })); - }, - [] - ); - - // Handle rows per page change - const handleChangeRowsPerPage = useCallback( - (event: React.ChangeEvent) => { - setBackupState((prev) => ({ - ...prev, - rowsPerPage: parseInt(event.target.value, 10), - page: 0, - })); + setPage(page); }, [] ); const fetchAndSetBackupFiles = async () => { try { - setBackupState((prev) => ({ ...prev, isLoading: true })); + setIsLoading(true); const files = await getAllBackupFiles(); - setBackupState((prev) => ({ ...prev, files })); + setBackupFiles(files); + setTotal(files.length); } catch (error) { - console.error("Failed to fetch backup files:", error); - Notice.error(t("Failed to fetch backup files")); + console.error(error); + // Notice.error(t("Failed to fetch backup files")); } finally { - setBackupState((prev) => ({ ...prev, isLoading: false })); + setIsLoading(false); } }; - const checkForm = () => { - const username = usernameRef.current?.value; - const password = passwordRef.current?.value; - const url = urlRef.current?.value; - - if (!url) { - Notice.error(t("WebDAV URL Required")); - urlRef.current?.focus(); - return; - } else if (!isValidUrl(url)) { - Notice.error(t("Invalid WebDAV URL")); - urlRef.current?.focus(); - return; - } - if (!username) { - Notice.error(t("Username Required")); - usernameRef.current?.focus(); - return; - } - if (!password) { - Notice.error(t("Password Required")); - passwordRef.current?.focus(); - return; - } - }; - - const submit = async (data: IWebDavConfig) => { - checkForm(); - setBackupState((prev) => ({ ...prev, isLoading: true })); - await saveWebdavConfig(data.url, data.username, data.password) - .then(() => { - mutateVerge( - { - webdav_url: data.url, - webdav_username: data.username, - webdav_password: data.password, - }, - false - ); - Notice.success(t("WebDAV Config Saved")); - }) - .catch((e) => { - Notice.error(t("WebDAV Config Save Failed", { error: e }), 3000); - }) - .finally(() => { - setBackupState((prev) => ({ ...prev, isLoading: false })); - fetchAndSetBackupFiles(); - }); - }; - - const handleClickShowPassword = useCallback(() => { - setShowPassword((prev) => !prev); - }, []); - - const handleBackup = useLockFn(async () => { - try { - checkForm(); - setBackupState((prev) => ({ ...prev, isLoading: true })); - await createWebdavBackup(); - Notice.success(t("Backup Created")); - await fetchAndSetBackupFiles(); - } catch (error) { - console.error("Backup failed:", error); - Notice.error(t("Backup Failed", { error })); - } finally { - setBackupState((prev) => ({ ...prev, isLoading: false })); - } - }); - const getAllBackupFiles = async () => { const files = await listWebDavBackup(); return files .map((file) => { const platform = file.filename.split("-")[0]; const fileBackupTimeStr = file.filename.match(FILENAME_PATTERN)!; + console.log(file, fileBackupTimeStr); + + if (fileBackupTimeStr === null) { + return null; + } + const backupTime = dayjs(fileBackupTimeStr[0], DATE_FORMAT); const allowApply = OS === platform; return { @@ -248,365 +86,55 @@ export const BackupViewer = forwardRef((props, ref) => { allow_apply: allowApply, } as BackupFile; }) + .filter((item) => item !== null) .sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1)); }; - const datasource = useMemo(() => { - return backupState.files.slice( - backupState.page * backupState.rowsPerPage, - backupState.page * backupState.rowsPerPage + backupState.rowsPerPage + useEffect(() => { + setDataSource( + backupFiles.slice( + page * DEFAULT_ROWS_PER_PAGE, + page * DEFAULT_ROWS_PER_PAGE + DEFAULT_ROWS_PER_PAGE + ) ); - }, [backupState.files, backupState.page, backupState.rowsPerPage]); - - const onFormSubmit = (e: React.FormEvent) => { - e.preventDefault(); - handleSubmit(submit)(e); - }; + }, [page, backupFiles]); return ( setOpen(false)} onCancel={() => setOpen(false)} + disableOk > - + - { + fetchAndSetBackupFiles(); + }} + onSaveSuccess={async () => { + fetchAndSetBackupFiles(); + }} + onInit={async () => { + fetchAndSetBackupFiles(); }} - urlRef={urlRef} - usernameRef={usernameRef} - passwordRef={passwordRef} - showPassword={showPassword} - onShowPasswordClick={handleClickShowPassword} - webdavChanged={webdavChanged} - webdavUrl={webdav_url} - webdavUsername={webdav_username} - webdavPassword={webdav_password} - handleBackup={handleBackup} - register={register} /> - handleChangePage(null, page)} - onRowsPerPageChange={handleChangeRowsPerPage} - totalCount={backupState.files.length} + ); }); - -const BackupTable = memo( - ({ - datasource, - page, - rowsPerPage, - onPageChange, - onRowsPerPageChange, - totalCount, - }: BackupTableProps) => { - const { t } = useTranslation(); - return ( - - - - - {t("Filename")} - {t("Backup Time")} - {t("Actions")} - - - - {datasource.length > 0 ? ( - datasource?.map((file, index) => ( - - - {file.platform === "windows" ? ( - - ) : file.platform === "linux" ? ( - - ) : ( - - )} - {file.filename} - - - {file.backup_time.fromNow()} - - - - - - - - - - - - - - )) - ) : ( - - - - - {t("No Backups")} - - - - - )} - -
- -
- ); - } -); - -const WebDAVConfigForm = memo( - ({ - onSubmit, - initialValues, - urlRef, - usernameRef, - passwordRef, - showPassword, - onShowPasswordClick, - webdavChanged, - webdavUrl, - webdavUsername, - webdavPassword, - handleBackup, - register, - }: WebDAVConfigFormProps) => { - const { t } = useTranslation(); - return ( -
- - - - - - - - - - - - - {showPassword ? : } - - - ), - }} - /> - - - - - - {webdavChanged || - webdavUrl === null || - webdavUsername == null || - webdavPassword == null ? ( - - ) : ( - - )} - - - -
- ); - } -); - -export function LinuxIcon(props: SVGProps) { - return ( - - - - - - - - - - - - - - - - ); -} - -export function WindowsIcon(props: SVGProps) { - return ( - - - - ); -} - -export function MacIcon(props: SVGProps) { - return ( - - - - ); -} diff --git a/src/components/setting/mods/clash-core-viewer.tsx b/src/components/setting/mods/clash-core-viewer.tsx index fc943528..c2301e59 100644 --- a/src/components/setting/mods/clash-core-viewer.tsx +++ b/src/components/setting/mods/clash-core-viewer.tsx @@ -17,7 +17,7 @@ import { ListItemButton, ListItemText, } from "@mui/material"; -import { changeClashCore, restartSidecar } from "@/services/cmds"; +import { changeClashCore, restartCore } from "@/services/cmds"; import { closeAllConnections, upgradeCore } from "@/services/api"; const VALID_CORE = [ @@ -59,7 +59,7 @@ export const ClashCoreViewer = forwardRef((props, ref) => { const onRestart = useLockFn(async () => { try { - await restartSidecar(); + await restartCore(); Notice.success(t(`Clash Core Restarted`), 1000); } catch (err: any) { Notice.error(err?.message || err.toString()); diff --git a/src/locales/en.json b/src/locales/en.json index 8858e135..2eca4953 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -386,10 +386,14 @@ "Password Required": "Password cannot be empty", "Failed to Fetch Backups": "Failed to fetch backup files", "WebDAV Config Saved": "WebDAV configuration saved successfully", - "WebDAV Config Save Failed": "Failed to save WebDAV configuration", + "WebDAV Config Save Failed": "Failed to save WebDAV configuration: {{error}}", "Backup Created": "Backup created successfully", - "Backup Failed": "Failed to create backup", + "Backup Failed": "Backup failed: {{error}}", "Delete Backup": "Delete Backup", "Restore Backup": "Restore Backup", - "Backup Time": "Backup Time" + "Backup Time": "Backup Time", + "Confirm to delete this backup file?": "Confirm to delete this backup file?", + "Confirm to restore this backup file?": "Confirm to restore this backup file?", + "Restore Success, App will restart in 1s": "Restore Success, App will restart in 1s", + "Failed to fetch backup files": "Failed to fetch backup files" } diff --git a/src/locales/fa.json b/src/locales/fa.json index 6769e674..c86e0c5f 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -386,10 +386,14 @@ "Password Required": "رمز عبور نمی‌تواند خالی باشد", "Failed to Fetch Backups": "دریافت فایل‌های پشتیبان ناموفق بود", "WebDAV Config Saved": "پیکربندی WebDAV با موفقیت ذخیره شد", - "WebDAV Config Save Failed": "ذخیره پیکربندی WebDAV ناموفق بود", + "WebDAV Config Save Failed": "خطا در ذخیره تنظیمات WebDAV: {{error}}", "Backup Created": "پشتیبان‌گیری با موفقیت ایجاد شد", - "Backup Failed": "ایجاد پشتیبان ناموفق بود", + "Backup Failed": "خطا در پشتیبان‌گیری: {{error}}", "Delete Backup": "حذف پشتیبان", "Restore Backup": "بازیابی پشتیبان", - "Backup Time": "زمان پشتیبان‌گیری" + "Backup Time": "زمان پشتیبان‌گیری", + "Confirm to delete this backup file?": "آیا از حذف این فایل پشتیبان اطمینان دارید؟", + "Confirm to restore this backup file?": "آیا از بازیابی این فایل پشتیبان اطمینان دارید؟", + "Restore Success, App will restart in 1s": "بازیابی با موفقیت انجام شد، برنامه در 1 ثانیه راه‌اندازی مجدد می‌شود", + "Failed to fetch backup files": "دریافت فایل‌های پشتیبان ناموفق بود" } diff --git a/src/locales/ru.json b/src/locales/ru.json index 297117dd..8d3b34f7 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -386,10 +386,14 @@ "Password Required": "Пароль не может быть пустым", "Failed to Fetch Backups": "Не удалось получить файлы резервных копий", "WebDAV Config Saved": "Конфигурация WebDAV успешно сохранена", - "WebDAV Config Save Failed": "Не удалось сохранить конфигурацию WebDAV", + "WebDAV Config Save Failed": "Не удалось сохранить конфигурацию WebDAV: {{error}}", "Backup Created": "Резервная копия успешно создана", - "Backup Failed": "Не удалось создать резервную копию", + "Backup Failed": "Ошибка резервного копирования: {{error}}", "Delete Backup": "Удалить резервную копию", "Restore Backup": "Восстановить резервную копию", - "Backup Time": "Время резервного копирования" + "Backup Time": "Время резервного копирования", + "Confirm to delete this backup file?": "Вы уверены, что хотите удалить этот файл резервной копии?", + "Confirm to restore this backup file?": "Вы уверены, что хотите восстановить этот файл резервной копии?", + "Restore Success, App will restart in 1s": "Восстановление успешно выполнено, приложение перезапустится через 1 секунду", + "Failed to fetch backup files": "Не удалось получить файлы резервных копий" } diff --git a/src/locales/zh.json b/src/locales/zh.json index 6a4e2188..b32f9507 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -380,16 +380,20 @@ "Actions": "操作", "Restore": "恢复", "No Backups": "暂无备份", - "WebDAV URL Required": "WebDAV 地址不能为空", - "Invalid WebDAV URL": "无效的 WebDAV 地址格式", + "WebDAV URL Required": "WebDAV 服务器地址不能为空", + "Invalid WebDAV URL": "无效的 WebDAV 服务器地址格式", "Username Required": "用户名不能为空", "Password Required": "密码不能为空", "Failed to Fetch Backups": "获取备份文件失败", "WebDAV Config Saved": "WebDAV 配置保存成功", - "WebDAV Config Save Failed": "WebDAV 配置保存失败", + "WebDAV Config Save Failed": "保存 WebDAV 配置失败: {{error}}", "Backup Created": "备份创建成功", - "Backup Failed": "备份创建失败", + "Backup Failed": "备份失败: {{error}}", "Delete Backup": "删除备份", "Restore Backup": "恢复备份", - "Backup Time": "备份时间" + "Backup Time": "备份时间", + "Confirm to delete this backup file?": "确认删除此备份文件吗?", + "Confirm to restore this backup file?": "确认恢复此 份文件吗?", + "Restore Success, App will restart in 1s": "恢复成功,应用将在1秒后重启", + "Failed to fetch backup files": "获取备份文件失败" } diff --git a/src/services/cmds.ts b/src/services/cmds.ts index 57d45efa..9a416c28 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -141,8 +141,12 @@ export async function changeClashCore(clashCore: string) { return invoke("change_clash_core", { clashCore }); } -export async function restartSidecar() { - return invoke("restart_sidecar"); +export async function restartCore() { + return invoke("restart_core"); +} + +export async function restartApp() { + return invoke("restart_app"); } export async function getAppDir() { @@ -240,6 +244,15 @@ export async function getNetworkInterfacesInfo() { export async function createWebdavBackup() { return invoke("create_webdav_backup"); } + +export async function deleteWebdavBackup(filename: string) { + return invoke("delete_webdav_backup", { filename }); +} + +export async function restoreWebDavBackup(filename: string) { + return invoke("restore_webdav_backup", { filename }); +} + export async function saveWebdavConfig( url: string, username: string,