diff --git a/Cargo.toml b/Cargo.toml index feb0ccb3..a86ebd99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,6 +106,7 @@ tauri-plugin-opener = "2" tauri-plugin-dialog = "2.6" tauri-plugin-fs = "2" tauri-plugin-log = "2" +tauri-plugin-autostart = "2" tauri-build = { version = "2", features = [] } # Windows-specific dependencies diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2186855f..c80fc769 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -143,6 +143,9 @@ importers: '@tauri-apps/api': specifier: ^2.10.1 version: 2.10.1 + '@tauri-apps/plugin-autostart': + specifier: ^2.0.0 + version: 2.5.1 '@tauri-apps/plugin-dialog': specifier: ^2.6.0 version: 2.6.0 @@ -1394,6 +1397,9 @@ packages: engines: {node: '>= 10'} hasBin: true + '@tauri-apps/plugin-autostart@2.5.1': + resolution: {integrity: sha512-zS/xx7yzveCcotkA+8TqkI2lysmG2wvQXv2HGAVExITmnFfHAdj1arGsbbfs3o6EktRHf6l34pJxc3YGG2mg7w==} + '@tauri-apps/plugin-dialog@2.6.0': resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==} @@ -5987,6 +5993,10 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.10.0 '@tauri-apps/cli-win32-x64-msvc': 2.10.0 + '@tauri-apps/plugin-autostart@2.5.1': + dependencies: + '@tauri-apps/api': 2.10.1 + '@tauri-apps/plugin-dialog@2.6.0': dependencies: '@tauri-apps/api': 2.10.1 diff --git a/src/apps/desktop/Cargo.toml b/src/apps/desktop/Cargo.toml index f521016a..d816dad8 100644 --- a/src/apps/desktop/Cargo.toml +++ b/src/apps/desktop/Cargo.toml @@ -28,6 +28,7 @@ tauri-plugin-opener = { workspace = true } tauri-plugin-dialog = { workspace = true } tauri-plugin-fs = { workspace = true } tauri-plugin-log = { workspace = true } +tauri-plugin-autostart = { workspace = true } # Inherited from workspace tokio = { workspace = true } diff --git a/src/apps/desktop/capabilities/default.json b/src/apps/desktop/capabilities/default.json index 31bacd46..992ed890 100644 --- a/src/apps/desktop/capabilities/default.json +++ b/src/apps/desktop/capabilities/default.json @@ -5,6 +5,7 @@ "windows": ["main"], "permissions": [ "log:default", + "autostart:default", "core:default", "core:path:default", "core:event:default", diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index a0c47bb5..6f7d3754 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -134,6 +134,11 @@ pub async fn run() { .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) + .plugin( + tauri_plugin_autostart::Builder::new() + .app_name("BitFun") + .build(), + ) .manage(app_state) .manage(coordinator_state) .manage(scheduler_state) diff --git a/src/web-ui/package.json b/src/web-ui/package.json index 5ffbb0a9..2a1a48a6 100644 --- a/src/web-ui/package.json +++ b/src/web-ui/package.json @@ -21,6 +21,7 @@ "@tauri-apps/plugin-fs": "^2.0.0", "@tauri-apps/plugin-log": "^2.8.0", "@tauri-apps/plugin-opener": "^2.5.2", + "@tauri-apps/plugin-autostart": "^2.0.0", "@tiptap/core": "^3.20.4", "@tiptap/extension-link": "^3.20.4", "@tiptap/extension-placeholder": "^3.20.4", diff --git a/src/web-ui/src/app/scenes/settings/settingsConfig.ts b/src/web-ui/src/app/scenes/settings/settingsConfig.ts index 3917afac..652e08ed 100644 --- a/src/web-ui/src/app/scenes/settings/settingsConfig.ts +++ b/src/web-ui/src/app/scenes/settings/settingsConfig.ts @@ -50,6 +50,10 @@ export const SETTINGS_CATEGORIES: ConfigCategoryDef[] = [ 'shell', 'pwsh', 'powershell', + 'autostart', + 'login', + 'boot', + 'launch', ], }, { diff --git a/src/web-ui/src/infrastructure/api/service-api/SystemAPI.ts b/src/web-ui/src/infrastructure/api/service-api/SystemAPI.ts index 0c7c68d7..d2f134df 100644 --- a/src/web-ui/src/infrastructure/api/service-api/SystemAPI.ts +++ b/src/web-ui/src/infrastructure/api/service-api/SystemAPI.ts @@ -3,6 +3,7 @@ import { api } from './ApiClient'; import { createTauriCommandError } from '../errors/TauriCommandError'; import { openUrl } from '@tauri-apps/plugin-opener'; +import { disable as autostartDisable, enable as autostartEnable, isEnabled as autostartIsEnabled } from '@tauri-apps/plugin-autostart'; import { createLogger } from '@/shared/utils/logger'; @@ -123,6 +124,36 @@ export class SystemAPI { throw createTauriCommandError('set_macos_edit_menu_mode', error, { mode }); } } + + /** Desktop only: whether the app is registered to launch at OS login. */ + async getLaunchAtLoginEnabled(): Promise { + if (typeof window === 'undefined' || !('__TAURI__' in window)) { + return false; + } + try { + return await autostartIsEnabled(); + } catch (error) { + log.error('Failed to read launch-at-login state', error); + throw createTauriCommandError('autostart_is_enabled', error); + } + } + + /** Desktop only: register or unregister launch at OS login. */ + async setLaunchAtLoginEnabled(enabled: boolean): Promise { + if (typeof window === 'undefined' || !('__TAURI__' in window)) { + return; + } + try { + if (enabled) { + await autostartEnable(); + } else { + await autostartDisable(); + } + } catch (error) { + log.error('Failed to set launch-at-login', { enabled, error }); + throw createTauriCommandError('autostart_set', error, { enabled }); + } + } } diff --git a/src/web-ui/src/infrastructure/config/components/BasicsConfig.tsx b/src/web-ui/src/infrastructure/config/components/BasicsConfig.tsx index 5f0ad5f8..5cf5434e 100644 --- a/src/web-ui/src/infrastructure/config/components/BasicsConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/BasicsConfig.tsx @@ -4,6 +4,7 @@ import { FolderOpen, Upload } from 'lucide-react'; import { Alert, Select, + Switch, Tooltip, ConfigPageLoading, ConfigPageMessage, @@ -189,6 +190,103 @@ function BasicsAppearanceSection() { ); } +function BasicsLaunchAtLoginSection() { + const { t } = useTranslation('settings/basics'); + const isTauri = typeof window !== 'undefined' && '__TAURI__' in window; + const [enabled, setEnabled] = useState(false); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [message, setMessage] = useState<{ type: 'success' | 'error' | 'info'; text: string } | null>(null); + + const showMessage = useCallback((type: 'success' | 'error' | 'info', text: string) => { + setMessage({ type, text }); + setTimeout(() => setMessage(null), 3000); + }, []); + + useEffect(() => { + if (!isTauri) { + setLoading(false); + return; + } + + let cancelled = false; + void (async () => { + try { + setLoading(true); + const v = await systemAPI.getLaunchAtLoginEnabled(); + if (!cancelled) { + setEnabled(v); + } + } catch (error) { + log.error('Failed to load launch-at-login state', error); + if (!cancelled) { + showMessage('error', t('launchAtLogin.messages.loadFailed')); + } + } finally { + if (!cancelled) { + setLoading(false); + } + } + })(); + + return () => { + cancelled = true; + }; + }, [isTauri, showMessage, t]); + + const handleToggle = useCallback( + async (next: boolean) => { + const previous = enabled; + setEnabled(next); + setSaving(true); + try { + await systemAPI.setLaunchAtLoginEnabled(next); + } catch (error) { + setEnabled(previous); + log.error('Failed to set launch-at-login', { next, error }); + showMessage('error', t('launchAtLogin.messages.saveFailed')); + } finally { + setSaving(false); + } + }, + [enabled, showMessage, t] + ); + + if (!isTauri) { + return null; + } + + if (loading) { + return ; + } + + return ( +
+
+ + + + { + void handleToggle(e.target.checked); + }} + disabled={saving} + /> + + +
+
+ ); +} + function BasicsLoggingSection() { const { t } = useTranslation('settings/basics'); const [configLevel, setConfigLevel] = useState('info'); @@ -767,6 +865,7 @@ const BasicsConfig: React.FC = () => { + diff --git a/src/web-ui/src/locales/en-US/settings.json b/src/web-ui/src/locales/en-US/settings.json index a8aeba84..75ab05a9 100644 --- a/src/web-ui/src/locales/en-US/settings.json +++ b/src/web-ui/src/locales/en-US/settings.json @@ -7,7 +7,7 @@ "searchNoResults": "No matching settings", "searchClear": "Clear search", "tabDescriptions": { - "basics": "Language, theme, logging, and terminal shell.", + "basics": "Language, theme, logging, terminal shell, and launch at login.", "models": "AI models, API keys, providers, and proxy.", "sessionConfig": "Session behavior, tools, and timeouts.", "aiContext": "Rules and memory for AI context.", diff --git a/src/web-ui/src/locales/en-US/settings/basics.json b/src/web-ui/src/locales/en-US/settings/basics.json index 2afafd30..0705fdca 100644 --- a/src/web-ui/src/locales/en-US/settings/basics.json +++ b/src/web-ui/src/locales/en-US/settings/basics.json @@ -1,6 +1,6 @@ { "title": "Basics", - "subtitle": "Interface language, theme, logging, and terminal", + "subtitle": "Interface language, theme, logging, terminal, and login startup", "appearance": { "title": "Appearance", "hint": "Interface language and visual theme", @@ -48,6 +48,19 @@ } } }, + "launchAtLogin": { + "sections": { + "title": "Login", + "hint": "Start BitFun when you sign in to this account" + }, + "toggleLabel": "Launch at login", + "toggleDescription": "Automatically start BitFun after you log in. Exact behavior depends on your operating system.", + "messages": { + "loading": "Loading...", + "loadFailed": "Failed to read launch-at-login setting", + "saveFailed": "Failed to update launch-at-login setting" + } + }, "logging": { "sections": { "logging": "Logging", diff --git a/src/web-ui/src/locales/zh-CN/settings.json b/src/web-ui/src/locales/zh-CN/settings.json index 4ee15fab..4f68a5f8 100644 --- a/src/web-ui/src/locales/zh-CN/settings.json +++ b/src/web-ui/src/locales/zh-CN/settings.json @@ -7,7 +7,7 @@ "searchNoResults": "没有匹配的配置", "searchClear": "清除搜索", "tabDescriptions": { - "basics": "语言、主题、日志与终端 Shell。", + "basics": "语言、主题、日志、终端 Shell 与开机启动。", "models": "AI 模型、API 密钥、供应商与代理。", "sessionConfig": "会话行为、工具与超时。", "aiContext": "规则与记忆等 AI 上下文。", diff --git a/src/web-ui/src/locales/zh-CN/settings/basics.json b/src/web-ui/src/locales/zh-CN/settings/basics.json index 4f7b46f8..43556c1f 100644 --- a/src/web-ui/src/locales/zh-CN/settings/basics.json +++ b/src/web-ui/src/locales/zh-CN/settings/basics.json @@ -1,6 +1,6 @@ { "title": "基础", - "subtitle": "界面语言、主题、日志与终端", + "subtitle": "界面语言、主题、日志、终端与登录启动", "appearance": { "title": "外观", "hint": "界面语言与视觉主题", @@ -48,6 +48,19 @@ } } }, + "launchAtLogin": { + "sections": { + "title": "登录时启动", + "hint": "登录本用户后是否自动运行 BitFun" + }, + "toggleLabel": "开机自动启动", + "toggleDescription": "登录系统后自动启动 BitFun;具体表现因操作系统而异。", + "messages": { + "loading": "加载中…", + "loadFailed": "无法读取开机启动设置", + "saveFailed": "无法更新开机启动设置" + } + }, "logging": { "sections": { "logging": "日志",