feat: implement launch at startup functionality in zn-ai

- Added a new setting for "launch at startup" in the GeneralSettingsPanel.
- Integrated the setting with the existing settings store and IPC mechanisms.
- Implemented platform-specific logic for enabling/disabling startup behavior in the main process.
- Created a new service for managing launch at startup settings, including Linux desktop entry creation.
- Added unit tests for the new functionality and ensured existing tests are updated accordingly.
- Updated i18n messages for the new setting in English, Chinese, and Japanese.
This commit is contained in:
duanshuwen
2026-04-20 23:29:10 +08:00
parent 35319e6a1d
commit 301f7d33ed
15 changed files with 924 additions and 3 deletions

View File

@@ -433,6 +433,8 @@ export const messages: I18nMessages = {
description: 'Customize the look and feel of the application.',
themeSection: 'Theme Settings',
languageSection: 'Language',
launchAtStartupTitle: 'Launch at startup',
launchAtStartupDescription: 'Start the app automatically after you sign in',
gatewayTitle: 'Gateway',
gatewayDescription: 'View the current Gateway state and basic runtime controls.',
gatewayStatusLabel: 'Status',
@@ -911,6 +913,8 @@ export const messages: I18nMessages = {
description: '自定义应用的外观与使用体验。',
themeSection: '主题设置',
languageSection: '语言',
launchAtStartupTitle: '开机自动启动',
launchAtStartupDescription: '登录系统后自动启动应用',
gatewayTitle: '网关',
gatewayDescription: '查看当前网关状态与基础运行控制。',
gatewayStatusLabel: '状态',
@@ -1389,6 +1393,8 @@ export const messages: I18nMessages = {
description: 'アプリの見た目と操作感をカスタマイズします。',
themeSection: 'テーマ設定',
languageSection: '言語',
launchAtStartupTitle: 'システム起動時に自動起動',
launchAtStartupDescription: 'システムにログインした後、アプリを自動的に起動します',
gatewayTitle: 'ゲートウェイ',
gatewayDescription: '現在のゲートウェイ状態と基本的なランタイム操作を確認します。',
gatewayStatusLabel: '状態',

View File

@@ -10,8 +10,10 @@ import ToggleSwitch from './ToggleSwitch';
type GeneralSettingsPanelProps = {
themeMode: ThemeMode;
language: LanguageCode;
launchAtStartup: boolean;
onThemeChange: (theme: ThemeMode) => void | Promise<void>;
onLanguageChange: (language: LanguageCode) => void | Promise<void>;
onLaunchAtStartupChange: (enabled: boolean) => void | Promise<void>;
gatewayState: GatewaySettingState;
updateState: SettingUpdateState;
};
@@ -83,8 +85,10 @@ function getGatewayStatusMeta(
export default function GeneralSettingsPanel({
themeMode,
language,
launchAtStartup,
onThemeChange,
onLanguageChange,
onLaunchAtStartupChange,
gatewayState,
updateState,
}: GeneralSettingsPanelProps) {
@@ -160,6 +164,24 @@ export default function GeneralSettingsPanel({
</div>
</div>
<div className="mt-5 box-border flex w-full items-center justify-between gap-4 border-b border-dashed border-[#E5E8EE] py-5 dark:border-gray-700">
<div>
<div className="mb-1 text-[16px] text-[#171717] dark:text-gray-100">
{t('settings.general.launchAtStartupTitle')}
</div>
<div className="text-[14px] text-[#99A0AE] dark:text-gray-500">
{t('settings.general.launchAtStartupDescription')}
</div>
</div>
<ToggleSwitch
checked={launchAtStartup}
onChange={(nextValue) => {
void onLaunchAtStartupChange(nextValue);
}}
/>
</div>
<div className="mt-10">
<div className="mb-6 text-[24px] font-medium text-[#171717] dark:text-gray-100">
{t('settings.general.gatewayTitle')}

View File

@@ -1,7 +1,7 @@
import { Suspense, lazy, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useI18n } from '../../i18n';
import { updateLanguage, updateThemeMode, useSettingsStore } from '../../stores';
import { updateLanguage, updateLaunchAtStartup, updateThemeMode, useSettingsStore } from '../../stores';
import type { LanguageCode, ThemeMode } from '../../types/runtime';
import AccountSettingsPanel from './components/AccountSettingsPanel';
import GeneralSettingsPanel from './components/GeneralSettingsPanel';
@@ -18,6 +18,7 @@ export default function SettingPage() {
const { t } = useI18n();
const themeMode = useSettingsStore((state) => state.themeMode);
const language = useSettingsStore((state) => state.language);
const launchAtStartup = useSettingsStore((state) => state.launchAtStartup);
const updateState = useSettingUpdateState();
const gatewayState = useGatewaySettingState();
const rawView = new URLSearchParams(location.search).get('view');
@@ -45,6 +46,10 @@ export default function SettingPage() {
await updateLanguage(nextLanguage);
};
const handleLaunchAtStartupChange = async (nextValue: boolean) => {
await updateLaunchAtStartup(nextValue);
};
const handleViewChange = (nextView: SettingView) => {
if (nextView === currentView) return;
@@ -93,8 +98,10 @@ export default function SettingPage() {
<GeneralSettingsPanel
themeMode={themeMode}
language={language}
launchAtStartup={launchAtStartup}
onThemeChange={handleThemeChange}
onLanguageChange={handleLanguageChange}
onLaunchAtStartupChange={handleLaunchAtStartupChange}
gatewayState={gatewayState}
updateState={updateState}
/>

View File

@@ -12,6 +12,8 @@ export {
updateLanguage as setLanguage,
updateFontSize as setFontSize,
updateMinimizeToTray as setMinimizeToTray,
updateLaunchAtStartup,
updateLaunchAtStartup as setLaunchAtStartup,
updatePrimaryColor as setPrimaryColor,
updateGatewayAutoStart,
updateGatewayAutoStart as setGatewayAutoStart,

View File

@@ -27,6 +27,7 @@ export interface SettingsState {
primaryColor: string;
fontSize: number;
minimizeToTray: boolean;
launchAtStartup: boolean;
providerId: string | null;
defaultModel: string | null;
gatewayAutoStart: boolean;
@@ -72,6 +73,7 @@ function createInitialState(): SettingsState {
primaryColor: '#1677ff',
fontSize: 14,
minimizeToTray: false,
launchAtStartup: false,
providerId: null,
defaultModel: null,
gatewayAutoStart: true,
@@ -205,11 +207,12 @@ async function hydrate(): Promise<SettingsState> {
const systemTheme = detectSystemTheme();
const systemLanguage = detectSystemLanguage();
const [themeMode, language, fontSize, minimizeToTray, primaryColor, providerId, defaultModel, gatewayAutoStart] = await Promise.all([
const [themeMode, language, fontSize, minimizeToTray, launchAtStartup, primaryColor, providerId, defaultModel, gatewayAutoStart] = await Promise.all([
readThemeMode(),
readConfigValue<LanguageCode>(CONFIG_KEYS.LANGUAGE, systemLanguage),
readConfigValue<number>(CONFIG_KEYS.FONT_SIZE, 14),
readConfigValue<boolean>(CONFIG_KEYS.MINIMIZE_TO_TRAY, false),
readConfigValue<boolean>(CONFIG_KEYS.LAUNCH_AT_STARTUP, false),
readConfigValue<string>(CONFIG_KEYS.PRIMARY_COLOR, '#1677ff'),
readConfigValue<string | null>(CONFIG_KEYS.PROVIDER, null),
readConfigValue<string | null>(CONFIG_KEYS.DEFAULT_MODEL, null),
@@ -231,6 +234,7 @@ async function hydrate(): Promise<SettingsState> {
primaryColor: primaryColor ?? '#1677ff',
fontSize: fontSize ?? 14,
minimizeToTray: Boolean(minimizeToTray),
launchAtStartup: Boolean(launchAtStartup),
providerId: providerId ?? null,
defaultModel: defaultModel ?? null,
gatewayAutoStart: Boolean(gatewayAutoStart),
@@ -306,6 +310,21 @@ async function setMinimizeToTray(minimizeToTray: boolean, persist = true): Promi
return state;
}
async function setLaunchAtStartup(launchAtStartup: boolean, persist = true): Promise<SettingsState> {
const next = Boolean(launchAtStartup);
if (state.launchAtStartup === next && state.initialized) return state;
patchState({
launchAtStartup: next,
});
if (persist) {
await writeConfigValue(CONFIG_KEYS.LAUNCH_AT_STARTUP, next);
}
return state;
}
async function setPrimaryColor(primaryColor: string, persist = true): Promise<SettingsState> {
const next = primaryColor || '#1677ff';
if (state.primaryColor === next && state.initialized) return state;
@@ -353,6 +372,7 @@ export const settingsStore = {
setLanguage,
setFontSize,
setMinimizeToTray,
setLaunchAtStartup,
setPrimaryColor,
setGatewayAutoStart,
hostApiFetch,
@@ -391,6 +411,10 @@ export async function updateMinimizeToTray(minimizeToTray: boolean, persist = tr
return setMinimizeToTray(minimizeToTray, persist);
}
export async function updateLaunchAtStartup(launchAtStartup: boolean, persist = true): Promise<SettingsState> {
return setLaunchAtStartup(launchAtStartup, persist);
}
export async function updateGatewayAutoStart(gatewayAutoStart: boolean, persist = true): Promise<SettingsState> {
return setGatewayAutoStart(gatewayAutoStart, persist);
}

View File

@@ -28,6 +28,7 @@ export const CONFIG_KEYS = {
LANGUAGE: 'language',
FONT_SIZE: 'fontSize',
MINIMIZE_TO_TRAY: 'minimizeToTray',
LAUNCH_AT_STARTUP: 'launchAtStartup',
PROVIDER: 'provider',
DEFAULT_MODEL: 'defaultModel',
GATEWAY_AUTO_START: 'gatewayAutoStart',
@@ -46,6 +47,7 @@ export interface ConfigValueMap {
language: LanguageCode;
fontSize: number;
minimizeToTray: boolean;
launchAtStartup: boolean;
provider: string | null;
defaultModel: string | null;
gatewayAutoStart: boolean;