diff --git a/backend/tauri/src/cmds.rs b/backend/tauri/src/cmds.rs index 2e08df44e0..641741ce51 100644 --- a/backend/tauri/src/cmds.rs +++ b/backend/tauri/src/cmds.rs @@ -362,6 +362,55 @@ pub async fn update_proxy_provider(name: String) -> CmdResult<()> { Ok(()) } +#[cfg(windows)] +#[tauri::command] +pub fn get_custom_app_dir() -> CmdResult> { + use crate::utils::winreg::get_app_dir; + match get_app_dir() { + Ok(Some(path)) => Ok(Some(path.to_string_lossy().to_string())), + Ok(None) => Ok(None), + Err(err) => Err(err.to_string()), + } +} + +#[cfg(not(windows))] +#[tauri::command] +pub fn get_custom_app_dir() -> CmdResult> { + Ok(None) +} + +#[cfg(windows)] +#[tauri::command] +pub async fn set_custom_app_dir(path: String) -> CmdResult { + use crate::utils::{dialog::migrate_dialog, init::do_config_migration, winreg::set_app_dir}; + use rust_i18n::t; + use std::path::PathBuf; + + let path_str = path.clone(); + let path = PathBuf::from(path); + wrap_err!(set_app_dir(&path))?; + + // show a dialog to ask whether to migrate the data + let res = tauri::async_runtime::spawn_blocking(move || { + let msg = t!("dialog.custom_app_dir_migrate", path = path_str).to_string(); + if migrate_dialog(&msg) { + let new_dir = PathBuf::from(path_str); + let old_dir = dirs::old_app_home_dir().unwrap(); + do_config_migration(&old_dir, &new_dir)?; + } + Ok::<_, anyhow::Error>(()) + }) + .await; + wrap_err!(wrap_err!(res)?)?; + Ok(()) +} + +#[cfg(not(windows))] +#[tauri::command] +pub async fn set_custom_app_dir(_path: String) -> CmdResult { + Ok(()) +} + #[cfg(windows)] pub mod uwp { use super::{wrap_err, CmdResult}; diff --git a/backend/tauri/src/main.rs b/backend/tauri/src/main.rs index 050f049735..5a0ddc87d3 100644 --- a/backend/tauri/src/main.rs +++ b/backend/tauri/src/main.rs @@ -178,6 +178,8 @@ fn main() -> std::io::Result<()> { cmds::read_profile_file, cmds::save_profile_file, cmds::save_window_size_state, + cmds::get_custom_app_dir, + cmds::set_custom_app_dir, // service mode cmds::service::check_service, cmds::service::install_service, diff --git a/backend/tauri/src/utils/dialog.rs b/backend/tauri/src/utils/dialog.rs index 4173824e01..4102513cce 100644 --- a/backend/tauri/src/utils/dialog.rs +++ b/backend/tauri/src/utils/dialog.rs @@ -11,14 +11,12 @@ pub fn panic_dialog(msg: &str) { .show(); } -pub fn migrate_dialog() -> bool { - let msg = format!("{}", t!("dialog.migrate")); - +pub fn migrate_dialog(msg: &str) -> bool { MessageDialog::new() .set_level(MessageLevel::Warning) .set_title("Clash Nyanpasu Migration") .set_buttons(MessageButtons::YesNo) - .set_description(msg.as_str()) + .set_description(msg) .show() } diff --git a/backend/tauri/src/utils/dirs.rs b/backend/tauri/src/utils/dirs.rs index e5bfeb1c9e..42a4644972 100644 --- a/backend/tauri/src/utils/dirs.rs +++ b/backend/tauri/src/utils/dirs.rs @@ -88,21 +88,25 @@ pub fn old_app_home_dir() -> Result { pub fn app_home_dir() -> Result { #[cfg(target_os = "windows")] { + use crate::utils::winreg::get_app_dir; use tauri::utils::platform::current_exe; - if !PORTABLE_FLAG.get().unwrap_or(&false) { - Ok(home_dir() + let reg_app_dir = get_app_dir()?; + if let Some(reg_app_dir) = reg_app_dir { + return Ok(reg_app_dir); + } + return Ok(home_dir() .ok_or(anyhow::anyhow!("failed to get app home dir"))? .join(".config") - .join(APP_NAME)) - } else { - let app_exe = current_exe()?; - let app_exe = dunce::canonicalize(app_exe)?; - let app_dir = app_exe - .parent() - .ok_or(anyhow::anyhow!("failed to get the portable app dir"))?; - Ok(PathBuf::from(app_dir).join(".config").join(APP_NAME)) + .join(APP_NAME)); } + + let app_exe = current_exe()?; + let app_exe = dunce::canonicalize(app_exe)?; + let app_dir = app_exe + .parent() + .ok_or(anyhow::anyhow!("failed to get the portable app dir"))?; + Ok(PathBuf::from(app_dir).join(".config").join(APP_NAME)) } #[cfg(not(target_os = "windows"))] diff --git a/backend/tauri/src/utils/init/mod.rs b/backend/tauri/src/utils/init/mod.rs index fdff66dd28..5d96bb47b4 100644 --- a/backend/tauri/src/utils/init/mod.rs +++ b/backend/tauri/src/utils/init/mod.rs @@ -4,6 +4,7 @@ use crate::{ }; use anyhow::Result; use runas::Command as RunasCommand; +use rust_i18n::t; use std::{fs, io::ErrorKind, path::PathBuf}; mod logging; @@ -26,7 +27,8 @@ pub fn init_config() -> Result<()> { })); if let (Some(app_dir), Some(old_app_dir)) = (app_dir, old_app_dir) { - if !app_dir.exists() && old_app_dir.exists() && migrate_dialog() { + let msg = t!("dialog.migrate"); + if !app_dir.exists() && old_app_dir.exists() && migrate_dialog(msg.to_string().as_str()) { if let Err(e) = do_config_migration(&old_app_dir, &app_dir) { super::dialog::error_dialog(format!("failed to do migration: {:?}", e)) } @@ -193,7 +195,7 @@ pub fn init_service() -> Result<()> { Ok(()) } -fn do_config_migration(old_app_dir: &PathBuf, app_dir: &PathBuf) -> anyhow::Result<()> { +pub fn do_config_migration(old_app_dir: &PathBuf, app_dir: &PathBuf) -> anyhow::Result<()> { if let Err(e) = fs::rename(old_app_dir, app_dir) { match e.kind() { #[cfg(windows)] diff --git a/backend/tauri/src/utils/mod.rs b/backend/tauri/src/utils/mod.rs index 5eb5dbf61c..b69c361d99 100644 --- a/backend/tauri/src/utils/mod.rs +++ b/backend/tauri/src/utils/mod.rs @@ -7,3 +7,5 @@ pub mod init; pub mod resolve; pub mod tmpl; // mod winhelp; +#[cfg(windows)] +pub mod winreg; diff --git a/backend/tauri/src/utils/winreg.rs b/backend/tauri/src/utils/winreg.rs new file mode 100644 index 0000000000..a87ad8fd0a --- /dev/null +++ b/backend/tauri/src/utils/winreg.rs @@ -0,0 +1,33 @@ +use std::{ + io::ErrorKind, + path::{Path, PathBuf}, +}; + +use anyhow::Result; +use winreg::{enums::*, RegKey}; + +pub fn get_app_dir() -> Result> { + let hcu = RegKey::predef(HKEY_CURRENT_USER); + let key = match hcu.open_subkey("Software\\Clash Nyanpasu") { + Ok(key) => key, + Err(e) => { + if let ErrorKind::NotFound = e.kind() { + return Ok(None); + } + return Err(e.into()); + } + }; + let path: String = key.get_value("AppDir")?; + if path.is_empty() { + return Ok(None); + } + Ok(Some(PathBuf::from(path))) +} + +pub fn set_app_dir(path: &Path) -> Result<()> { + let hcu = RegKey::predef(HKEY_CURRENT_USER); + let (key, _) = hcu.create_subkey("Software\\Clash Nyanpasu")?; + let path = path.to_str().unwrap(); // safe to unwrap + key.set_value("AppDir", &path)?; + Ok(()) +} diff --git a/locales/en.json b/locales/en.json index a68731ae5c..c1406fdfe8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -32,6 +32,7 @@ }, "dialog": { "panic": "Please report this issue to Github issue tracker.", - "migrate": "Old version config file detected\nMigrate to new version or not?\n WARNING: This will override your current config if exists" + "migrate": "Old version config file detected\nMigrate to new version or not?\n WARNING: This will override your current config if exists", + "custom_app_dir_migrate": "You will set custom app dir to %{path}\n Shall we move the current app dir to the new one?" } } diff --git a/locales/zh.json b/locales/zh.json index c2684aaa0d..1af1446e66 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -32,6 +32,7 @@ }, "dialog": { "panic": "请将此问题汇报到 Github 问题追踪器", - "migrate": "检测到旧版本配置文件\n是否迁移到新版本?\n警告: 此操作会覆盖掉现有配置文件" + "migrate": "检测到旧版本配置文件\n是否迁移到新版本?\n警告: 此操作会覆盖掉现有配置文件", + "custom_app_dir_migrate": "你将要更改应用目录至 %{path}。\n需要将现有数据迁移到新目录吗?" } } diff --git a/src/components/setting/setting-verge.tsx b/src/components/setting/setting-verge.tsx index 9050da757c..004e43ef08 100644 --- a/src/components/setting/setting-verge.tsx +++ b/src/components/setting/setting-verge.tsx @@ -3,12 +3,14 @@ import { useMessage } from "@/hooks/use-notification"; import { useVerge } from "@/hooks/use-verge"; import { collectLogs, + isPortable, openAppDir, openCoreDir, openLogsDir, + setCustomAppDir, } from "@/services/cmds"; import getSystem from "@/utils/get-system"; -import { ArrowForward, IosShare } from "@mui/icons-material"; +import { ArrowForward, IosShare, Settings } from "@mui/icons-material"; import { Chip, CircularProgress, @@ -19,8 +21,9 @@ import { Typography, } from "@mui/material"; import { version } from "@root/package.json"; +import { open } from "@tauri-apps/api/dialog"; import { checkUpdate } from "@tauri-apps/api/updater"; -import { useLockFn } from "ahooks"; +import { useAsyncEffect, useLockFn } from "ahooks"; import { useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import MDYSwitch from "../common/mdy-switch"; @@ -46,6 +49,11 @@ const SettingVerge = ({ onError }: Props) => { const { verge, patchVerge } = useVerge(); const { theme_mode, language, disable_auto_check_update } = verge ?? {}; + const [portable, setPortable] = useState(false); + + useAsyncEffect(async () => { + setPortable(await isPortable()); + }); const [loading, setLoading] = useState({ theme_mode: false, @@ -94,6 +102,30 @@ const SettingVerge = ({ onError }: Props) => { const onSwitchFormat = (_e: any, value: boolean) => value; + const [changingAppDir, setChangingAppDir] = useState(false); + const changeAppDir = useLockFn(async () => { + setChangingAppDir(true); + try { + const selected = await open({ directory: true, multiple: false }); // TODO: use current app dir as defaultPath + if (!selected) return; // user cancelled the selection + if (Array.isArray(selected)) { + useMessage(t("Multiple directories are not supported"), { + title: t("Error"), + type: "error", + }); + return; + } + await setCustomAppDir(selected); + } catch (err: any) { + useMessage(err.message || err.toString(), { + title: t("Error"), + type: "error", + }); + } finally { + setChangingAppDir(false); + } + }); + return ( @@ -197,7 +229,26 @@ const SettingVerge = ({ onError }: Props) => { - + + {changingAppDir ? ( + + ) : ( + + )} + + } + > ("update_proxy_provider", { name }); } + +export async function getCustomAppDir() { + return invoke("get_custom_app_dir"); +} + +export async function setCustomAppDir(dir: string) { + return invoke("set_custom_app_dir", { dir }); +}