feat(config): migrate app_config_dir to Tauri Store for independent management (#109)
This commit is contained in:
@@ -37,6 +37,7 @@
|
||||
"@tauri-apps/api": "^2.8.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.0",
|
||||
"@tauri-apps/plugin-process": "^2.0.0",
|
||||
"@tauri-apps/plugin-store": "^2.0.0",
|
||||
"@tauri-apps/plugin-updater": "^2.0.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"i18next": "^25.5.2",
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -38,6 +38,9 @@ importers:
|
||||
'@tauri-apps/plugin-process':
|
||||
specifier: ^2.0.0
|
||||
version: 2.3.0
|
||||
'@tauri-apps/plugin-store':
|
||||
specifier: ^2.0.0
|
||||
version: 2.4.0
|
||||
'@tauri-apps/plugin-updater':
|
||||
specifier: ^2.0.0
|
||||
version: 2.9.0
|
||||
@@ -689,6 +692,9 @@ packages:
|
||||
'@tauri-apps/plugin-process@2.3.0':
|
||||
resolution: {integrity: sha512-0DNj6u+9csODiV4seSxxRbnLpeGYdojlcctCuLOCgpH9X3+ckVZIEj6H7tRQ7zqWr7kSTEWnrxtAdBb0FbtrmQ==}
|
||||
|
||||
'@tauri-apps/plugin-store@2.4.0':
|
||||
resolution: {integrity: sha512-PjBnlnH6jyI71MGhrPaxUUCsOzc7WO1mbc4gRhME0m2oxLgCqbksw6JyeKQimuzv4ysdpNO3YbmaY2haf82a3A==}
|
||||
|
||||
'@tauri-apps/plugin-updater@2.9.0':
|
||||
resolution: {integrity: sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==}
|
||||
|
||||
@@ -1561,6 +1567,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.8.0
|
||||
|
||||
'@tauri-apps/plugin-store@2.4.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.8.0
|
||||
|
||||
'@tauri-apps/plugin-updater@2.9.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.8.0
|
||||
|
||||
17
src-tauri/Cargo.lock
generated
17
src-tauri/Cargo.lock
generated
@@ -583,6 +583,7 @@ dependencies = [
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-store",
|
||||
"tauri-plugin-updater",
|
||||
"tokio",
|
||||
"toml 0.8.2",
|
||||
@@ -4542,6 +4543,22 @@ dependencies = [
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-store"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d85dd80d60a76ee2c2fdce09e9ef30877b239c2a6bb76e6d7d03708aa5f13a19"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.9.0"
|
||||
|
||||
@@ -28,6 +28,7 @@ tauri-plugin-opener = "2"
|
||||
tauri-plugin-process = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-store = "2"
|
||||
dirs = "5.0"
|
||||
toml = "0.8"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
|
||||
|
||||
137
src-tauri/src/app_store.rs
Normal file
137
src-tauri/src/app_store.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use serde_json::Value;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{OnceLock, RwLock};
|
||||
use tauri_plugin_store::StoreExt;
|
||||
|
||||
/// Store 中的键名
|
||||
const STORE_KEY_APP_CONFIG_DIR: &str = "app_config_dir_override";
|
||||
|
||||
/// 全局缓存的 AppHandle (在应用启动时设置)
|
||||
static APP_HANDLE: OnceLock<RwLock<Option<tauri::AppHandle>>> = OnceLock::new();
|
||||
|
||||
/// 设置全局 AppHandle
|
||||
pub fn set_app_handle(handle: tauri::AppHandle) {
|
||||
let store = APP_HANDLE.get_or_init(|| RwLock::new(None));
|
||||
if let Ok(mut guard) = store.write() {
|
||||
*guard = Some(handle);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取全局 AppHandle
|
||||
fn get_app_handle() -> Option<tauri::AppHandle> {
|
||||
let store = APP_HANDLE.get()?;
|
||||
let guard = store.read().ok()?;
|
||||
guard.as_ref().cloned()
|
||||
}
|
||||
|
||||
/// 从 Tauri Store 读取 app_config_dir 覆盖配置 (无需 AppHandle 版本)
|
||||
pub fn get_app_config_dir_override() -> Option<PathBuf> {
|
||||
let app = get_app_handle()?;
|
||||
get_app_config_dir_from_store(&app)
|
||||
}
|
||||
|
||||
/// 从 Tauri Store 读取 app_config_dir 覆盖配置(公开函数)
|
||||
pub fn get_app_config_dir_from_store(app: &tauri::AppHandle) -> Option<PathBuf> {
|
||||
let store = app.store_builder("app_paths.json").build();
|
||||
|
||||
if let Err(e) = &store {
|
||||
log::warn!("无法创建 Store: {}", e);
|
||||
return None;
|
||||
}
|
||||
|
||||
let store = store.unwrap();
|
||||
|
||||
match store.get(STORE_KEY_APP_CONFIG_DIR) {
|
||||
Some(Value::String(path_str)) => {
|
||||
let path_str = path_str.trim();
|
||||
if path_str.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = resolve_path(path_str);
|
||||
|
||||
// 验证路径是否存在
|
||||
if !path.exists() {
|
||||
log::warn!(
|
||||
"Store 中配置的 app_config_dir 不存在: {:?}\n\
|
||||
将使用默认路径。",
|
||||
path
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
log::info!("使用 Store 中的 app_config_dir: {:?}", path);
|
||||
Some(path)
|
||||
}
|
||||
Some(_) => {
|
||||
log::warn!("Store 中的 {} 类型不正确,应为字符串", STORE_KEY_APP_CONFIG_DIR);
|
||||
None
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 写入 app_config_dir 到 Tauri Store
|
||||
pub fn set_app_config_dir_to_store(
|
||||
app: &tauri::AppHandle,
|
||||
path: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
let store = app
|
||||
.store_builder("app_paths.json")
|
||||
.build()
|
||||
.map_err(|e| format!("创建 Store 失败: {}", e))?;
|
||||
|
||||
match path {
|
||||
Some(p) => {
|
||||
let trimmed = p.trim();
|
||||
if !trimmed.is_empty() {
|
||||
store.set(STORE_KEY_APP_CONFIG_DIR, Value::String(trimmed.to_string()));
|
||||
log::info!("已将 app_config_dir 写入 Store: {}", trimmed);
|
||||
} else {
|
||||
// 空字符串 = 删除配置
|
||||
store.delete(STORE_KEY_APP_CONFIG_DIR);
|
||||
log::info!("已从 Store 中删除 app_config_dir 配置");
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// None = 删除配置
|
||||
store.delete(STORE_KEY_APP_CONFIG_DIR);
|
||||
log::info!("已从 Store 中删除 app_config_dir 配置");
|
||||
}
|
||||
}
|
||||
|
||||
store.save().map_err(|e| format!("保存 Store 失败: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 解析路径,支持 ~ 开头的相对路径
|
||||
fn resolve_path(raw: &str) -> PathBuf {
|
||||
if raw == "~" {
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
return home;
|
||||
}
|
||||
} else if let Some(stripped) = raw.strip_prefix("~/") {
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
return home.join(stripped);
|
||||
}
|
||||
} else if let Some(stripped) = raw.strip_prefix("~\\") {
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
return home.join(stripped);
|
||||
}
|
||||
}
|
||||
|
||||
PathBuf::from(raw)
|
||||
}
|
||||
|
||||
/// 从旧的 settings.json 迁移 app_config_dir 到 Store
|
||||
pub fn migrate_app_config_dir_from_settings(app: &tauri::AppHandle) -> Result<(), String> {
|
||||
// app_config_dir 已从 settings.json 移除,此函数保留但不再执行迁移
|
||||
// 如果用户在旧版本设置过 app_config_dir,需要在 Store 中手动配置
|
||||
log::info!("app_config_dir 迁移功能已移除,请在设置中重新配置");
|
||||
|
||||
// 确保 Store 初始化正常
|
||||
let _ = get_app_config_dir_from_store(app);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1222,6 +1222,13 @@ pub async fn save_settings(settings: crate::settings::AppSettings) -> Result<boo
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// 重启应用程序(当 app_config_dir 变更后使用)
|
||||
#[tauri::command]
|
||||
pub async fn restart_app(app: tauri::AppHandle) -> Result<bool, String> {
|
||||
// 使用 tauri-plugin-process 重启应用
|
||||
app.restart();
|
||||
}
|
||||
|
||||
/// 检查更新
|
||||
#[tauri::command]
|
||||
pub async fn check_for_updates(handle: tauri::AppHandle) -> Result<bool, String> {
|
||||
@@ -1469,3 +1476,20 @@ pub async fn update_endpoint_last_used(
|
||||
state.save()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取 app_config_dir 覆盖配置 (从 Store)
|
||||
#[tauri::command]
|
||||
pub async fn get_app_config_dir_override(app: tauri::AppHandle) -> Result<Option<String>, String> {
|
||||
Ok(crate::app_store::get_app_config_dir_from_store(&app)
|
||||
.map(|p| p.to_string_lossy().to_string()))
|
||||
}
|
||||
|
||||
/// 设置 app_config_dir 覆盖配置 (到 Store)
|
||||
#[tauri::command]
|
||||
pub async fn set_app_config_dir_override(
|
||||
app: tauri::AppHandle,
|
||||
path: Option<String>,
|
||||
) -> Result<bool, String> {
|
||||
crate::app_store::set_app_config_dir_to_store(&app, path.as_deref())?;
|
||||
Ok(true)
|
||||
}
|
||||
@@ -33,6 +33,10 @@ pub fn get_claude_settings_path() -> PathBuf {
|
||||
|
||||
/// 获取应用配置目录路径 (~/.cc-switch)
|
||||
pub fn get_app_config_dir() -> PathBuf {
|
||||
if let Some(custom) = crate::app_store::get_app_config_dir_override() {
|
||||
return custom;
|
||||
}
|
||||
|
||||
dirs::home_dir()
|
||||
.expect("无法获取用户主目录")
|
||||
.join(".cc-switch")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod app_config;
|
||||
mod app_store;
|
||||
mod claude_mcp;
|
||||
mod claude_plugin;
|
||||
mod codex_config;
|
||||
@@ -305,7 +306,10 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.setup(|app| {
|
||||
// 设置全局 AppHandle 以供 Store 使用
|
||||
app_store::set_app_handle(app.handle().clone());
|
||||
// 注册 Updater 插件(桌面端)
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
@@ -359,6 +363,11 @@ pub fn run() {
|
||||
// 初始化应用状态(仅创建一次,并在本函数末尾注入 manage)
|
||||
let app_state = AppState::new();
|
||||
|
||||
// 迁移旧的 app_config_dir 配置到 Store
|
||||
if let Err(e) = app_store::migrate_app_config_dir_from_settings(&app.handle()) {
|
||||
log::warn!("迁移 app_config_dir 失败: {}", e);
|
||||
}
|
||||
|
||||
// 首次启动迁移:扫描副本文件,合并到 config.json,并归档副本;旧 config.json 先归档
|
||||
{
|
||||
let mut config_guard = app_state.config.lock().unwrap();
|
||||
@@ -418,6 +427,7 @@ pub fn run() {
|
||||
commands::read_live_provider_settings,
|
||||
commands::get_settings,
|
||||
commands::save_settings,
|
||||
commands::restart_app,
|
||||
commands::check_for_updates,
|
||||
commands::is_portable_mode,
|
||||
commands::get_claude_plugin_status,
|
||||
@@ -447,6 +457,9 @@ pub fn run() {
|
||||
commands::add_custom_endpoint,
|
||||
commands::remove_custom_endpoint,
|
||||
commands::update_endpoint_last_used,
|
||||
// app_config_dir override via Store
|
||||
commands::get_app_config_dir_override,
|
||||
commands::set_app_config_dir_override,
|
||||
// theirs: config import/export and dialogs
|
||||
import_export::export_config_to_file,
|
||||
import_export::import_config_from_file,
|
||||
|
||||
@@ -64,7 +64,12 @@ impl Default for AppSettings {
|
||||
|
||||
impl AppSettings {
|
||||
fn settings_path() -> PathBuf {
|
||||
crate::config::get_app_config_dir().join("settings.json")
|
||||
// settings.json 必须使用固定路径,不能被 app_config_dir 覆盖
|
||||
// 否则会造成循环依赖:读取 settings 需要知道路径,但路径在 settings 中
|
||||
dirs::home_dir()
|
||||
.expect("无法获取用户主目录")
|
||||
.join(".cc-switch")
|
||||
.join("settings.json")
|
||||
}
|
||||
|
||||
fn normalize_paths(&mut self) {
|
||||
|
||||
@@ -61,6 +61,10 @@ export default function SettingsModal({
|
||||
codexConfigDir: undefined,
|
||||
language: persistedLanguage,
|
||||
});
|
||||
// appConfigDir 现在从 Store 独立管理
|
||||
const [appConfigDir, setAppConfigDir] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [initialLanguage, setInitialLanguage] = useState<"zh" | "en">(
|
||||
persistedLanguage,
|
||||
);
|
||||
@@ -69,9 +73,14 @@ export default function SettingsModal({
|
||||
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const [showUpToDate, setShowUpToDate] = useState(false);
|
||||
const [resolvedAppConfigDir, setResolvedAppConfigDir] = useState<string>("");
|
||||
const [resolvedClaudeDir, setResolvedClaudeDir] = useState<string>("");
|
||||
const [resolvedCodexDir, setResolvedCodexDir] = useState<string>("");
|
||||
const [isPortable, setIsPortable] = useState(false);
|
||||
const [initialAppConfigDir, setInitialAppConfigDir] = useState<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
const [showRestartDialog, setShowRestartDialog] = useState(false);
|
||||
const { hasUpdate, updateInfo, updateHandle, checkUpdate, resetDismiss } =
|
||||
useUpdate();
|
||||
|
||||
@@ -86,6 +95,7 @@ export default function SettingsModal({
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
loadAppConfigDirFromStore(); // 从 Store 加载 appConfigDir
|
||||
loadConfigPath();
|
||||
loadVersion();
|
||||
loadResolvedDirs();
|
||||
@@ -103,6 +113,24 @@ export default function SettingsModal({
|
||||
}
|
||||
};
|
||||
|
||||
// 从 Tauri Store 加载 appConfigDir
|
||||
const loadAppConfigDirFromStore = async () => {
|
||||
try {
|
||||
const storeValue = await (window as any).api.getAppConfigDirOverride();
|
||||
if (storeValue) {
|
||||
setAppConfigDir(storeValue);
|
||||
setInitialAppConfigDir(storeValue);
|
||||
setResolvedAppConfigDir(storeValue);
|
||||
} else {
|
||||
// 使用默认值
|
||||
const defaultDir = await computeDefaultAppConfigDir();
|
||||
setResolvedAppConfigDir(defaultDir);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("从 Store 加载 appConfigDir 失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
const loadedSettings = await window.api.getSettings();
|
||||
@@ -195,7 +223,17 @@ export default function SettingsModal({
|
||||
: undefined,
|
||||
language: selectedLanguage,
|
||||
};
|
||||
|
||||
// 保存 settings.json (不包含 appConfigDir)
|
||||
await window.api.saveSettings(payload);
|
||||
|
||||
// 单独保存 appConfigDir 到 Store
|
||||
const normalizedAppConfigDir =
|
||||
appConfigDir && appConfigDir.trim() !== ""
|
||||
? appConfigDir.trim()
|
||||
: null;
|
||||
await (window as any).api.setAppConfigDirOverride(normalizedAppConfigDir);
|
||||
|
||||
// 立即生效:根据开关无条件写入/移除 ~/.claude/config.json
|
||||
try {
|
||||
if (payload.enableClaudePluginIntegration) {
|
||||
@@ -206,7 +244,14 @@ export default function SettingsModal({
|
||||
} catch (e) {
|
||||
console.warn("[Settings] Apply Claude plugin config on save failed", e);
|
||||
}
|
||||
|
||||
// 检测 appConfigDir 是否真正发生变化
|
||||
const appConfigDirChanged =
|
||||
(normalizedAppConfigDir || undefined) !==
|
||||
(initialAppConfigDir || undefined);
|
||||
|
||||
setSettings(payload);
|
||||
setInitialAppConfigDir(normalizedAppConfigDir ?? undefined);
|
||||
try {
|
||||
window.localStorage.setItem("language", selectedLanguage);
|
||||
} catch (error) {
|
||||
@@ -216,12 +261,47 @@ export default function SettingsModal({
|
||||
if (i18n.language !== selectedLanguage) {
|
||||
void i18n.changeLanguage(selectedLanguage);
|
||||
}
|
||||
onClose();
|
||||
|
||||
// 如果修改了 appConfigDir,需要提示用户重启应用程序
|
||||
if (appConfigDirChanged) {
|
||||
setShowRestartDialog(true);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(t("console.saveSettingsFailed"), error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestartNow = async () => {
|
||||
// 开发模式下不真正重启,只提示
|
||||
if (import.meta.env.DEV) {
|
||||
onNotify?.(
|
||||
t("settings.devModeRestartHint"),
|
||||
"success",
|
||||
5000,
|
||||
);
|
||||
setShowRestartDialog(false);
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
// 生产模式下真正重启应用
|
||||
try {
|
||||
await window.api.restartApp();
|
||||
} catch (e) {
|
||||
console.warn("[Settings] Restart app failed", e);
|
||||
// 如果重启失败,仍然关闭设置窗口
|
||||
setShowRestartDialog(false);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestartLater = () => {
|
||||
setShowRestartDialog(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleLanguageChange = (lang: "zh" | "en") => {
|
||||
setSettings((prev) => ({ ...prev, language: lang }));
|
||||
if (i18n.language !== lang) {
|
||||
@@ -298,6 +378,28 @@ export default function SettingsModal({
|
||||
}
|
||||
};
|
||||
|
||||
const handleBrowseAppConfigDir = async () => {
|
||||
try {
|
||||
const currentResolved = appConfigDir ?? resolvedAppConfigDir;
|
||||
const selected = await window.api.selectConfigDirectory(currentResolved);
|
||||
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitized = selected.trim();
|
||||
|
||||
if (sanitized === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
setAppConfigDir(sanitized);
|
||||
setResolvedAppConfigDir(sanitized);
|
||||
} catch (error) {
|
||||
console.error(t("console.selectConfigDirFailed"), error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBrowseConfigDir = async (app: AppType) => {
|
||||
try {
|
||||
const currentResolved =
|
||||
@@ -340,6 +442,24 @@ export default function SettingsModal({
|
||||
}
|
||||
};
|
||||
|
||||
const computeDefaultAppConfigDir = async () => {
|
||||
try {
|
||||
const home = await homeDir();
|
||||
return await join(home, ".cc-switch");
|
||||
} catch (error) {
|
||||
console.error(t("console.getDefaultConfigDirFailed"), error);
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetAppConfigDir = async () => {
|
||||
setAppConfigDir(undefined);
|
||||
const defaultDir = await computeDefaultAppConfigDir();
|
||||
if (defaultDir) {
|
||||
setResolvedAppConfigDir(defaultDir);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetConfigDir = async (app: AppType) => {
|
||||
setSettings((prev) => ({
|
||||
...prev,
|
||||
@@ -605,6 +725,40 @@ export default function SettingsModal({
|
||||
{t("settings.configDirectoryDescription")}
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
||||
{t("settings.appConfigDir")}
|
||||
</label>
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500 mb-1">
|
||||
{t("settings.appConfigDirDescription")}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={appConfigDir ?? resolvedAppConfigDir ?? ""}
|
||||
onChange={(e) => setAppConfigDir(e.target.value)}
|
||||
placeholder={t("settings.browsePlaceholderApp")}
|
||||
className="flex-1 px-3 py-2 text-xs font-mono bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/40"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBrowseAppConfigDir}
|
||||
className="px-2 py-2 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
title={t("settings.browseDirectory")}
|
||||
>
|
||||
<FolderSearch size={16} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResetAppConfigDir}
|
||||
className="px-2 py-2 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
title={t("settings.resetDefault")}
|
||||
>
|
||||
<Undo2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
||||
{t("settings.claudeConfigDir")}
|
||||
@@ -854,6 +1008,39 @@ export default function SettingsModal({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Restart Confirmation Dialog */}
|
||||
{showRestartDialog && (
|
||||
<div className="fixed inset-0 z-[60] flex items-center justify-center">
|
||||
<div
|
||||
className={`absolute inset-0 bg-black/50 dark:bg-black/70${
|
||||
isLinux() ? "" : " backdrop-blur-sm"
|
||||
}`}
|
||||
/>
|
||||
<div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-2xl w-[400px] p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">
|
||||
{t("settings.restartRequired")}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
{t("settings.restartRequiredMessage")}
|
||||
</p>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={handleRestartLater}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||
>
|
||||
{t("settings.restartLater")}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRestartNow}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 rounded-lg transition-colors"
|
||||
>
|
||||
{t("settings.restartNow")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -109,6 +109,9 @@
|
||||
"openFolder": "Open Folder",
|
||||
"configDirectoryOverride": "Configuration Directory Override (Advanced)",
|
||||
"configDirectoryDescription": "When using Claude Code or Codex in environments like WSL, you can manually specify the configuration directory in WSL to keep provider data consistent with the main environment.",
|
||||
"appConfigDir": "CC-Switch Configuration Directory",
|
||||
"appConfigDirDescription": "Customize the storage location for CC-Switch configuration files (config.json, etc.)",
|
||||
"browsePlaceholderApp": "e.g., C:\\Users\\Administrator\\.cc-switch",
|
||||
"claudeConfigDir": "Claude Code Configuration Directory",
|
||||
"codexConfigDir": "Codex Configuration Directory",
|
||||
"browsePlaceholderClaude": "e.g., /home/<your-username>/.claude",
|
||||
@@ -123,7 +126,12 @@
|
||||
"releaseNotes": "Release Notes",
|
||||
"viewReleaseNotes": "View release notes for this version",
|
||||
"viewCurrentReleaseNotes": "View current version release notes",
|
||||
"exportFailedError": "Export config failed:"
|
||||
"exportFailedError": "Export config failed:",
|
||||
"restartRequired": "Restart Required",
|
||||
"restartRequiredMessage": "Modifying the CC-Switch configuration directory requires restarting the application to take effect. Restart now?",
|
||||
"restartNow": "Restart Now",
|
||||
"restartLater": "Restart Later",
|
||||
"devModeRestartHint": "Dev Mode: Configuration saved. Please manually restart the application for changes to take effect"
|
||||
},
|
||||
"apps": {
|
||||
"claude": "Claude Code",
|
||||
|
||||
@@ -109,6 +109,9 @@
|
||||
"openFolder": "打开文件夹",
|
||||
"configDirectoryOverride": "配置目录覆盖(高级)",
|
||||
"configDirectoryDescription": "在 WSL 等环境使用 Claude Code 或 Codex 的时候,可手动指定 WSL 里的配置目录,供应商数据与主环境保持一致。",
|
||||
"appConfigDir": "CC-Switch 配置目录",
|
||||
"appConfigDirDescription": "自定义 CC-Switch 的配置存储位置(config.json 等文件)",
|
||||
"browsePlaceholderApp": "例如:C:\\Users\\Administrator\\.cc-switch",
|
||||
"claudeConfigDir": "Claude Code 配置目录",
|
||||
"codexConfigDir": "Codex 配置目录",
|
||||
"browsePlaceholderClaude": "例如:/home/<你的用户名>/.claude",
|
||||
@@ -123,7 +126,12 @@
|
||||
"releaseNotes": "更新日志",
|
||||
"viewReleaseNotes": "查看该版本更新日志",
|
||||
"viewCurrentReleaseNotes": "查看当前版本更新日志",
|
||||
"exportFailedError": "导出配置失败:"
|
||||
"exportFailedError": "导出配置失败:",
|
||||
"restartRequired": "需要重启应用",
|
||||
"restartRequiredMessage": "修改 CC-Switch 配置目录后需要重启应用才能生效,是否立即重启?",
|
||||
"restartNow": "立即重启",
|
||||
"restartLater": "稍后重启",
|
||||
"devModeRestartHint": "开发模式:配置已保存,请手动重启应用以使新配置生效"
|
||||
},
|
||||
"apps": {
|
||||
"claude": "Claude Code",
|
||||
|
||||
@@ -209,6 +209,16 @@ export const tauriAPI = {
|
||||
}
|
||||
},
|
||||
|
||||
// 重启应用程序
|
||||
restartApp: async (): Promise<boolean> => {
|
||||
try {
|
||||
return await invoke("restart_app");
|
||||
} catch (error) {
|
||||
console.error("重启应用失败:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// 检查更新
|
||||
checkForUpdates: async (): Promise<void> => {
|
||||
try {
|
||||
@@ -653,6 +663,26 @@ export const tauriAPI = {
|
||||
});
|
||||
return unlisten;
|
||||
},
|
||||
|
||||
// 获取 app_config_dir 覆盖配置(从 Store)
|
||||
getAppConfigDirOverride: async (): Promise<string | null> => {
|
||||
try {
|
||||
return await invoke<string | null>("get_app_config_dir_override");
|
||||
} catch (error) {
|
||||
console.error("获取 app_config_dir 覆盖配置失败:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
// 设置 app_config_dir 覆盖配置(到 Store)
|
||||
setAppConfigDirOverride: async (path: string | null): Promise<boolean> => {
|
||||
try {
|
||||
return await invoke<boolean>("set_app_config_dir_override", { path });
|
||||
} catch (error) {
|
||||
console.error("设置 app_config_dir 覆盖配置失败:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 创建全局 API 对象,兼容现有代码
|
||||
|
||||
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@@ -58,6 +58,7 @@ declare global {
|
||||
) => Promise<UnlistenFn>;
|
||||
getSettings: () => Promise<Settings>;
|
||||
saveSettings: (settings: Settings) => Promise<boolean>;
|
||||
restartApp: () => Promise<boolean>;
|
||||
checkForUpdates: () => Promise<void>;
|
||||
isPortable: () => Promise<boolean>;
|
||||
getAppConfigPath: () => Promise<string>;
|
||||
|
||||
Reference in New Issue
Block a user