Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config): support custom app dir in windows #582

Merged
merged 2 commits into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions backend/tauri/src/cmds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<String>> {
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<Option<String>> {
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};
Expand Down
2 changes: 2 additions & 0 deletions backend/tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 2 additions & 4 deletions backend/tauri/src/utils/dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down
24 changes: 14 additions & 10 deletions backend/tauri/src/utils/dirs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,21 +88,25 @@ pub fn old_app_home_dir() -> Result<PathBuf> {
pub fn app_home_dir() -> Result<PathBuf> {
#[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"))]
Expand Down
6 changes: 4 additions & 2 deletions backend/tauri/src/utils/init/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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))
}
Expand Down Expand Up @@ -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)]
Expand Down
2 changes: 2 additions & 0 deletions backend/tauri/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ pub mod init;
pub mod resolve;
pub mod tmpl;
// mod winhelp;
#[cfg(windows)]
pub mod winreg;
33 changes: 33 additions & 0 deletions backend/tauri/src/utils/winreg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use std::{
io::ErrorKind,
path::{Path, PathBuf},
};

use anyhow::Result;
use winreg::{enums::*, RegKey};

pub fn get_app_dir() -> Result<Option<PathBuf>> {
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(())
}
3 changes: 2 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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?"
}
}
3 changes: 2 additions & 1 deletion locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
},
"dialog": {
"panic": "请将此问题汇报到 Github 问题追踪器",
"migrate": "检测到旧版本配置文件\n是否迁移到新版本?\n警告: 此操作会覆盖掉现有配置文件"
"migrate": "检测到旧版本配置文件\n是否迁移到新版本?\n警告: 此操作会覆盖掉现有配置文件",
"custom_app_dir_migrate": "你将要更改应用目录至 %{path}。\n需要将现有数据迁移到新目录吗?"
}
}
57 changes: 54 additions & 3 deletions src/components/setting/setting-verge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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";
Expand All @@ -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,
Expand Down Expand Up @@ -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 (
<SettingList title={t("Nyanpasu Setting")}>
<ThemeViewer ref={themeRef} />
Expand Down Expand Up @@ -197,7 +229,26 @@ const SettingVerge = ({ onError }: Props) => {
</IconButton>
</SettingItem>

<SettingItem label={t("Open App Dir")}>
<SettingItem
label={t("Open App Dir")}
extra={
<IconButton
color="inherit"
size="small"
disabled={changingAppDir}
onClick={changeAppDir}
>
{changingAppDir ? (
<CircularProgress color="inherit" size={20} />
) : (
<Settings
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
)}
</IconButton>
}
>
<IconButton
color="inherit"
size="small"
Expand Down
8 changes: 8 additions & 0 deletions src/services/cmds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,11 @@ export async function selectProxy(group: string, name: string) {
export async function updateProxyProvider(name: string) {
return invoke<void>("update_proxy_provider", { name });
}

export async function getCustomAppDir() {
return invoke<string | null>("get_custom_app_dir");
}

export async function setCustomAppDir(dir: string) {
return invoke<void>("set_custom_app_dir", { dir });
}
Loading