diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8864a43..ca0cfc6 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -228,6 +228,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auto-launch" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471" +dependencies = [ + "dirs 4.0.0", + "thiserror 1.0.69", + "winreg 0.10.1", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -605,6 +616,7 @@ dependencies = [ "tar", "tauri", "tauri-build", + "tauri-plugin-autostart", "tauri-plugin-opener", "tokio", "tokio-util", @@ -803,13 +815,33 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", ] [[package]] @@ -820,7 +852,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.2", "windows-sys 0.61.2", ] @@ -947,7 +979,7 @@ dependencies = [ "rustc_version", "toml 1.1.3+spec-1.1.0", "vswhom", - "winreg", + "winreg 0.55.0", ] [[package]] @@ -3033,6 +3065,17 @@ dependencies = [ "bitflags 2.13.0", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -3884,7 +3927,7 @@ dependencies = [ "anyhow", "bytes", "cookie", - "dirs", + "dirs 6.0.0", "dunce", "embed_plist", "getrandom 0.3.4", @@ -3934,7 +3977,7 @@ checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" dependencies = [ "anyhow", "cargo_toml", - "dirs", + "dirs 6.0.0", "glob", "heck 0.5.0", "json-patch", @@ -4004,6 +4047,20 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-autostart" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459383cebc193cdd03d1ba4acc40f2c408a7abce419d64bdcd2d745bc2886f70" +dependencies = [ + "auto-launch", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + [[package]] name = "tauri-plugin-opener" version = "2.5.4" @@ -4501,7 +4558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" dependencies = [ "crossbeam-channel", - "dirs", + "dirs 6.0.0", "libappindicator", "muda", "objc2", @@ -5323,6 +5380,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "winreg" version = "0.55.0" @@ -5355,7 +5421,7 @@ dependencies = [ "block2", "cookie", "crossbeam-channel", - "dirs", + "dirs 6.0.0", "dom_query", "dpi", "dunce", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7dfdb95..8f2ad93 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -27,6 +27,7 @@ serde_yaml = "0.9" sha2 = "0.10" tar = "0.4" tauri = { version = "~2.11", features = ["tray-icon"] } +tauri-plugin-autostart = "2" tauri-plugin-opener = "2" tokio = { version = "1", features = ["macros", "sync", "time"] } tokio-util = "0.7" diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 4496cdc..5e50161 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -41,6 +41,7 @@ use tauri::{ tray::{MouseButton, TrayIconBuilder, TrayIconEvent}, }; use tauri::{Emitter, LogicalSize, Manager}; +use tauri_plugin_autostart::ManagerExt as AutostartManagerExt; use tauri_plugin_opener::OpenerExt; use tokio_util::sync::CancellationToken; use zip::ZipArchive; @@ -644,6 +645,20 @@ struct GuiSettings { close_behavior: WindowsCloseBehavior, } +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct SoftwareSettings { + close_behavior: WindowsCloseBehavior, + autostart_enabled: bool, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SoftwareSettingsInput { + close_behavior: WindowsCloseBehavior, + autostart_enabled: bool, +} + #[derive(Clone, Serialize)] #[serde(rename_all = "camelCase")] struct AgentConfigStatus { @@ -1668,13 +1683,78 @@ fn resolve_windows_close_request( Ok(()) } +fn app_autostart_enabled(app: &tauri::AppHandle) -> Result { + app.autolaunch() + .is_enabled() + .map_err(|error| format!("读取系统开机自启状态失败: {error}")) +} + +fn set_app_autostart_enabled(app: &tauri::AppHandle, enabled: bool) -> Result<(), String> { + let manager = app.autolaunch(); + if enabled { + manager + .enable() + .map_err(|error| format!("启用开机自启失败: {error}")) + } else { + manager + .disable() + .map_err(|error| format!("关闭开机自启失败: {error}")) + } +} + +fn software_settings( + app: &tauri::AppHandle, + config: &GuiConfigFile, +) -> Result { + Ok(SoftwareSettings { + close_behavior: config.close_behavior, + autostart_enabled: app_autostart_enabled(app)?, + }) +} + #[tauri::command] -fn save_app_close_behavior( +fn get_software_settings( + app: tauri::AppHandle, gui_config_state: tauri::State<'_, GuiConfigState>, - behavior: WindowsCloseBehavior, -) -> Result { - let config = gui_config_state.set_close_behavior(behavior)?; - Ok(GuiSettings::from(&config)) +) -> Result { + let config = gui_config_state.snapshot()?; + software_settings(&app, &config) +} + +#[tauri::command] +fn save_software_settings( + app: tauri::AppHandle, + gui_config_state: tauri::State<'_, GuiConfigState>, + settings: SoftwareSettingsInput, +) -> Result { + let previous_config = gui_config_state.snapshot()?; + let previous_autostart_enabled = app_autostart_enabled(&app)?; + let autostart_changed = previous_autostart_enabled != settings.autostart_enabled; + + if autostart_changed { + set_app_autostart_enabled(&app, settings.autostart_enabled)?; + } + + let config = if previous_config.close_behavior == settings.close_behavior { + previous_config + } else { + match gui_config_state.set_close_behavior(settings.close_behavior) { + Ok(config) => config, + Err(error) => { + let rollback_error = autostart_changed + .then(|| set_app_autostart_enabled(&app, previous_autostart_enabled).err()) + .flatten(); + return Err(match rollback_error { + Some(rollback_error) => { + format!("{error}; 回滚开机自启设置也失败: {rollback_error}") + } + None => error, + }); + } + } + }; + + software_settings(&app, &config) } fn inspect_agent_config_statuses( @@ -18122,6 +18202,10 @@ fn main() { let initial_window_size = configured_window_size(&gui_config); let app = tauri::Builder::default() + .plugin(tauri_plugin_autostart::init( + tauri_plugin_autostart::MacosLauncher::LaunchAgent, + None, + )) .plugin( tauri_plugin_opener::Builder::new() .open_js_links_on_click(false) @@ -18281,7 +18365,8 @@ fn main() { save_api_access_remark, set_app_locale, resolve_windows_close_request, - save_app_close_behavior, + get_software_settings, + save_software_settings, get_agent_config_statuses, refresh_agent_config_statuses, get_agent_models, diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index b3c9915..4f7a715 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -345,10 +345,12 @@ export const jaOverrides = { 'config.network.sessionAffinityHint': '同じセッションでは以前選択した認証情報を優先します', 'config.network.sessionTtl': 'セッションアフィニティ TTL', 'config.network.sessionTtlHint': '例:1h。空欄の場合はコアのデフォルト値を使用します', - 'config.notice.softwareCloseBehaviorUpdated': 'アプリの終了動作を更新しました', + 'config.notice.softwareUpdated': 'ソフトウェア設定を保存しました', 'config.software.title': 'ソフトウェア設定', - 'config.software.description': 'ウィンドウの閉じるボタンをクリックしたときのデフォルト動作を設定します。「毎回確認」を選ぶと確認ダイアログに戻ります。', + 'config.software.autostart': 'システム起動時に開始', + 'config.software.autostartDescription': 'サインイン後に EasyCLIProxyAPI を自動的に起動します', 'config.software.closeBehavior': '終了動作', + 'config.software.closeBehaviorDescription': 'ウィンドウを閉じたときにアプリを終了するか、システムトレイで実行を続けるかを選択します', 'config.software.behavior.ask': '毎回確認', 'config.software.behavior.minimize': 'トレイに最小化', 'config.software.behavior.exit': 'アプリを終了', diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts index f6fcdb4..c128def 100644 --- a/src/i18n/resources.ts +++ b/src/i18n/resources.ts @@ -292,10 +292,12 @@ export const zhCN = { 'config.notice.sessionAffinityEnabled': '会话粘性路由已启用', 'config.notice.sessionAffinityDisabled': '会话粘性路由已停用', 'config.notice.sessionTtlUpdated': '会话粘性 TTL 已保存', - 'config.notice.softwareCloseBehaviorUpdated': '软件关闭行为已更新', + 'config.notice.softwareUpdated': '软件设置已保存', 'config.software.title': '软件设置', - 'config.software.description': '设置点击窗口关闭按钮时的默认行为。选择“每次询问”即可恢复关闭确认弹窗。', + 'config.software.autostart': '开机自启', + 'config.software.autostartDescription': '登录系统后自动启动 EasyCLIProxyAPI', 'config.software.closeBehavior': '关闭行为', + 'config.software.closeBehaviorDescription': '决定关闭窗口时是退出程序还是继续在系统托盘运行', 'config.software.behavior.ask': '每次询问', 'config.software.behavior.minimize': '最小化到托盘', 'config.software.behavior.exit': '退出程序', @@ -1221,10 +1223,12 @@ export const en: Record = { 'config.notice.sessionAffinityEnabled': 'Session affinity enabled', 'config.notice.sessionAffinityDisabled': 'Session affinity disabled', 'config.notice.sessionTtlUpdated': 'Session affinity TTL saved', - 'config.notice.softwareCloseBehaviorUpdated': 'App close behavior updated', + 'config.notice.softwareUpdated': 'Software settings saved', 'config.software.title': 'Software Settings', - 'config.software.description': 'Choose what happens by default when you click the window close button. Select “Ask every time” to restore the confirmation dialog.', + 'config.software.autostart': 'Launch at startup', + 'config.software.autostartDescription': 'Automatically launch EasyCLIProxyAPI when you sign in', 'config.software.closeBehavior': 'Close behavior', + 'config.software.closeBehaviorDescription': 'Choose whether closing the window exits the app or keeps it running in the system tray', 'config.software.behavior.ask': 'Ask every time', 'config.software.behavior.minimize': 'Minimize to Tray', 'config.software.behavior.exit': 'Exit App', diff --git a/src/i18n/traditional.ts b/src/i18n/traditional.ts index 13a9cc1..0af2fac 100644 --- a/src/i18n/traditional.ts +++ b/src/i18n/traditional.ts @@ -1,4 +1,8 @@ const phraseReplacements: ReadonlyArray = [ + ['登录系统后自动启动', '登入系統後自動啟動'], + ['系统托盘', '系統匣'], + ['开机自启', '開機自啟'], + ['行为', '行為'], ['兼容格式', '相容格式'], ['历史', '歷史'], ['高级设置', '進階設定'], diff --git a/src/pages/ConfigPanel.tsx b/src/pages/ConfigPanel.tsx index 425c77f..b72abe6 100644 --- a/src/pages/ConfigPanel.tsx +++ b/src/pages/ConfigPanel.tsx @@ -59,8 +59,9 @@ type DraftRefreshMode = 'replace' | 'preserve'; type NetworkDraftDirty = Record; -type GuiSettings = { +type SoftwareSettings = { closeBehavior: CloseBehavior; + autostartEnabled: boolean; }; const cleanNetworkDraft = (): NetworkDraftDirty => ({ @@ -80,9 +81,10 @@ export function ConfigPanelPage() { const { t } = useI18n(); const { status: coreStatus, publishStatus, refreshStatus } = useCoreRuntime(); const [settings, setSettings] = useState(null); - const [softwareSettings, setSoftwareSettings] = useState(null); + const [softwareSettings, setSoftwareSettings] = useState(null); const [softwareSettingsLoading, setSoftwareSettingsLoading] = useState(true); const [softwareCloseBehaviorDraft, setSoftwareCloseBehaviorDraft] = useState('ask'); + const [softwareAutostartDraft, setSoftwareAutostartDraft] = useState(false); const [softwareSavedStatusVisible, setSoftwareSavedStatusVisible] = useState(false); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(''); @@ -196,9 +198,10 @@ export function ConfigPanelPage() { setSoftwareSettingsLoading(true); setSoftwareSavedStatusVisible(false); try { - const result = await invoke('get_gui_settings'); + const result = await invoke('get_software_settings'); setSoftwareSettings(result); setSoftwareCloseBehaviorDraft(result.closeBehavior); + setSoftwareAutostartDraft(result.autostartEnabled); } catch (error) { setSoftwareSettings(null); showNotice(t('config.error.saveFailed', { error: String(error) }), 'error'); @@ -461,23 +464,32 @@ export function ConfigPanelPage() { } }; - const saveSoftwareCloseBehavior = async () => { + const saveSoftwareSettings = async () => { if (!softwareSettings || busyAction !== null) return; - if (softwareCloseBehaviorDraft === softwareSettings.closeBehavior) return; + if ( + softwareCloseBehaviorDraft === softwareSettings.closeBehavior + && softwareAutostartDraft === softwareSettings.autostartEnabled + ) return; setBusyAction('software'); try { - const result = await invoke('save_app_close_behavior', { - behavior: softwareCloseBehaviorDraft, + const result = await invoke('save_software_settings', { + settings: { + closeBehavior: softwareCloseBehaviorDraft, + autostartEnabled: softwareAutostartDraft, + }, }); setSoftwareSettings(result); setSoftwareCloseBehaviorDraft(result.closeBehavior); + setSoftwareAutostartDraft(result.autostartEnabled); setSoftwareSavedStatusVisible(true); - showNotice(t('config.notice.softwareCloseBehaviorUpdated'), 'success'); + showNotice(t('config.notice.softwareUpdated'), 'success'); } catch (error) { setSoftwareCloseBehaviorDraft(softwareSettings.closeBehavior); + setSoftwareAutostartDraft(softwareSettings.autostartEnabled); setSoftwareSavedStatusVisible(false); showNotice(t('config.error.saveFailed', { error: String(error) }), 'error'); + void loadSoftwareSettings(); } finally { setBusyAction(null); } @@ -504,18 +516,21 @@ export function ConfigPanelPage() { : ''; const softwareCloseBehaviorDirty = softwareSettings !== null && softwareCloseBehaviorDraft !== softwareSettings.closeBehavior; + const softwareAutostartDirty = softwareSettings !== null + && softwareAutostartDraft !== softwareSettings.autostartEnabled; + const softwareSettingsDirty = softwareCloseBehaviorDirty || softwareAutostartDirty; const softwareStatusLabel = softwareSettingsLoading ? t('common.loading') : softwareSettings === null ? t('common.unavailable') - : softwareCloseBehaviorDirty + : softwareSettingsDirty ? t('config.network.unsaved') : softwareSavedStatusVisible ? t('config.network.saved') : ''; const softwareStatusIsSaved = !softwareSettingsLoading && softwareSettings !== null - && !softwareCloseBehaviorDirty + && !softwareSettingsDirty && softwareSavedStatusVisible; const networkStatusIsSaved = !loading && settings !== null @@ -1033,8 +1048,8 @@ export function ConfigPanelPage() {