feat: add system autostart and single-instance support
This commit is contained in:
1 parent
dd9e84a0db
commit
ea51253d58
6 files changed
+249
-4
No files matched your search
@@ -1,5 +1,6 @@
|
|||||||
import { AppConfig } from '../../types/config';
|
import { AppConfig } from '../../types/config';
|
||||||
import { ConfigManager } from './manager';
|
import { ConfigManager } from './manager';
|
||||||
|
import { syncAutoStart } from '../../utils/autoStart';
|
||||||
|
|
||||||
export function loadConfig(): AppConfig {
|
export function loadConfig(): AppConfig {
|
||||||
return ConfigManager.loadConfig();
|
return ConfigManager.loadConfig();
|
||||||
@@ -10,4 +11,5 @@ export function saveConfig(config: AppConfig): void {
|
|||||||
// Logic to update Tray if language changes
|
// Logic to update Tray if language changes
|
||||||
// For now just save
|
// For now just save
|
||||||
ConfigManager.saveConfig(config);
|
ConfigManager.saveConfig(config);
|
||||||
|
syncAutoStart(config);
|
||||||
}
|
}
|
||||||
@@ -100,6 +100,14 @@ i18n
|
|||||||
auto_sync: 'Auto Sync Current Account',
|
auto_sync: 'Auto Sync Current Account',
|
||||||
auto_sync_desc: 'Periodically sync active account information',
|
auto_sync_desc: 'Periodically sync active account information',
|
||||||
},
|
},
|
||||||
|
startup: {
|
||||||
|
title: 'Startup',
|
||||||
|
description: 'Control application launch behavior at system startup.',
|
||||||
|
auto_startup: 'Start with system',
|
||||||
|
auto_startup_desc: 'Launch at sign-in and keep the app in the system tray',
|
||||||
|
macos_hint:
|
||||||
|
'macOS requires a signed app for Login Items to work. If auto-start fails, please sign the app or enable it manually in System Settings.',
|
||||||
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
title: 'Upstream Proxy',
|
title: 'Upstream Proxy',
|
||||||
description: 'Configure a proxy for outgoing requests to Google/Gemini APIs.',
|
description: 'Configure a proxy for outgoing requests to Google/Gemini APIs.',
|
||||||
@@ -391,6 +399,14 @@ i18n
|
|||||||
auto_sync: '自动获取当前账号',
|
auto_sync: '自动获取当前账号',
|
||||||
auto_sync_desc: '定期自动同步当前活跃账号信息',
|
auto_sync_desc: '定期自动同步当前活跃账号信息',
|
||||||
},
|
},
|
||||||
|
startup: {
|
||||||
|
title: '启动',
|
||||||
|
description: '控制系统启动时的应用行为。',
|
||||||
|
auto_startup: '随系统启动',
|
||||||
|
auto_startup_desc: '登录时自动启动并保持在托盘里',
|
||||||
|
macos_hint:
|
||||||
|
'macOS 需要应用已签名才可生效。如自启动失败,请自行签名或在系统设置中手动启用。',
|
||||||
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
title: '上游代理',
|
title: '上游代理',
|
||||||
description: '为发往 Google/Gemini API 的请求配置代理。',
|
description: '为发往 Google/Gemini API 的请求配置代理。',
|
||||||
|
|||||||
+56
-4
@@ -18,6 +18,8 @@ import { bootstrapNestServer, stopNestServer } from './server/main';
|
|||||||
import { initTray, setTrayLanguage, destroyTray } from './ipc/tray/handler';
|
import { initTray, setTrayLanguage, destroyTray } from './ipc/tray/handler';
|
||||||
import { rpcHandler } from './ipc/handler';
|
import { rpcHandler } from './ipc/handler';
|
||||||
import { ConfigManager } from './ipc/config/manager';
|
import { ConfigManager } from './ipc/config/manager';
|
||||||
|
import { AppConfig } from './types/config';
|
||||||
|
import { isAutoStartLaunch, syncAutoStart } from './utils/autoStart';
|
||||||
|
|
||||||
const packetLogPath = path.join(app.getPath('userData'), 'orpc_packets.log');
|
const packetLogPath = path.join(app.getPath('userData'), 'orpc_packets.log');
|
||||||
|
|
||||||
@@ -63,6 +65,25 @@ const inDevelopment = process.env.NODE_ENV === 'development';
|
|||||||
let globalMainWindow: BrowserWindow | null = null;
|
let globalMainWindow: BrowserWindow | null = null;
|
||||||
// let tray: Tray | null = null; // Moved to tray/handler.ts
|
// let tray: Tray | null = null; // Moved to tray/handler.ts
|
||||||
let isQuitting = false;
|
let isQuitting = false;
|
||||||
|
let startupConfig: AppConfig | null = null;
|
||||||
|
let shouldStartHidden = false;
|
||||||
|
|
||||||
|
const gotSingleInstanceLock = app.requestSingleInstanceLock();
|
||||||
|
|
||||||
|
if (!gotSingleInstanceLock) {
|
||||||
|
app.quit();
|
||||||
|
} else {
|
||||||
|
app.on('second-instance', () => {
|
||||||
|
logger.info('Second instance detected, focusing existing window');
|
||||||
|
if (app.isReady()) {
|
||||||
|
createWindow({ startHidden: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
createWindow({ startHidden: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
process.on('exit', (code) => {
|
process.on('exit', (code) => {
|
||||||
logger.info(`Process exit event triggered with code: ${code}`);
|
logger.info(`Process exit event triggered with code: ${code}`);
|
||||||
@@ -75,7 +96,22 @@ process.on('before-exit', (code) => {
|
|||||||
|
|
||||||
// let tray: Tray | null = null; // Moved to tray/handler.ts
|
// let tray: Tray | null = null; // Moved to tray/handler.ts
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow({ startHidden }: { startHidden: boolean }) {
|
||||||
|
if (globalMainWindow && !globalMainWindow.isDestroyed()) {
|
||||||
|
if (startHidden) {
|
||||||
|
globalMainWindow.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (globalMainWindow.isMinimized()) {
|
||||||
|
globalMainWindow.restore();
|
||||||
|
}
|
||||||
|
if (!globalMainWindow.isVisible()) {
|
||||||
|
globalMainWindow.show();
|
||||||
|
}
|
||||||
|
globalMainWindow.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('createWindow: start');
|
logger.info('createWindow: start');
|
||||||
const preload = path.join(__dirname, 'preload.js');
|
const preload = path.join(__dirname, 'preload.js');
|
||||||
logger.info(`createWindow: preload path: ${preload}`);
|
logger.info(`createWindow: preload path: ${preload}`);
|
||||||
@@ -84,6 +120,7 @@ function createWindow() {
|
|||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 800,
|
height: 800,
|
||||||
|
show: !startHidden,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
devTools: inDevelopment,
|
devTools: inDevelopment,
|
||||||
@@ -99,6 +136,10 @@ function createWindow() {
|
|||||||
});
|
});
|
||||||
globalMainWindow = mainWindow;
|
globalMainWindow = mainWindow;
|
||||||
logger.info('createWindow: BrowserWindow instance created');
|
logger.info('createWindow: BrowserWindow instance created');
|
||||||
|
if (startHidden) {
|
||||||
|
mainWindow.hide();
|
||||||
|
logger.info('createWindow: startHidden enabled, window hidden');
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('createWindow: setting main window in ipcContext');
|
logger.info('createWindow: setting main window in ipcContext');
|
||||||
ipcContext.setMainWindow(mainWindow);
|
ipcContext.setMainWindow(mainWindow);
|
||||||
@@ -126,6 +167,7 @@ function createWindow() {
|
|||||||
|
|
||||||
mainWindow.on('closed', () => {
|
mainWindow.on('closed', () => {
|
||||||
logger.info('Window closed event triggered');
|
logger.info('Window closed event triggered');
|
||||||
|
globalMainWindow = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.webContents.on('render-process-gone', (event, details) => {
|
mainWindow.webContents.on('render-process-gone', (event, details) => {
|
||||||
@@ -241,13 +283,23 @@ app
|
|||||||
logger.info('Step: Initialize Antigravity DB (WAL Mode)');
|
logger.info('Step: Initialize Antigravity DB (WAL Mode)');
|
||||||
initDatabase();
|
initDatabase();
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
logger.info('Step: Load Config');
|
||||||
|
const config = ConfigManager.loadConfig();
|
||||||
|
startupConfig = config;
|
||||||
|
syncAutoStart(config);
|
||||||
|
shouldStartHidden = isAutoStartLaunch() && config.auto_startup;
|
||||||
|
if (shouldStartHidden) {
|
||||||
|
logger.info('Startup: Auto-start detected, window will start hidden');
|
||||||
|
}
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info('Step: setupORPC');
|
logger.info('Step: setupORPC');
|
||||||
return setupORPC();
|
return setupORPC();
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
logger.info('Step: createWindow');
|
logger.info('Step: createWindow');
|
||||||
await createWindow();
|
await createWindow({ startHidden: shouldStartHidden });
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.info('Step: installExtensions (SKIPPED)');
|
logger.info('Step: installExtensions (SKIPPED)');
|
||||||
@@ -264,7 +316,7 @@ app
|
|||||||
AuthServer.start();
|
AuthServer.start();
|
||||||
|
|
||||||
// Gateway Server (NestJS) - auto-start if enabled
|
// Gateway Server (NestJS) - auto-start if enabled
|
||||||
const config = ConfigManager.loadConfig();
|
const config = startupConfig || ConfigManager.loadConfig();
|
||||||
if (config.proxy?.auto_start) {
|
if (config.proxy?.auto_start) {
|
||||||
const port = config.proxy?.port || 8045;
|
const port = config.proxy?.port || 8045;
|
||||||
// Default to a valid ProxyConfig object if null, although loadConfig ensures defaults
|
// Default to a valid ProxyConfig object if null, although loadConfig ensures defaults
|
||||||
@@ -306,7 +358,7 @@ app.on('window-all-closed', () => {
|
|||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createWindow();
|
createWindow({ startHidden: false });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
//osX only ends
|
//osX only ends
|
||||||
@@ -47,6 +47,10 @@ function SettingsPage() {
|
|||||||
queryFn: getPlatform,
|
queryFn: getPlatform,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isAutoStartSupported =
|
||||||
|
platform === 'win32' || platform === 'darwin' || platform === 'linux';
|
||||||
|
const isMac = platform === 'darwin';
|
||||||
|
|
||||||
const handleLanguageChange = (value: string) => {
|
const handleLanguageChange = (value: string) => {
|
||||||
setAppLanguage(value, i18n);
|
setAppLanguage(value, i18n);
|
||||||
};
|
};
|
||||||
@@ -179,6 +183,49 @@ function SettingsPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{isAutoStartSupported && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t('settings.startup.title', 'Startup')}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t(
|
||||||
|
'settings.startup.description',
|
||||||
|
'Control application launch behavior at system startup.',
|
||||||
|
)}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label>{t('settings.startup.auto_startup', 'Start with system')}</Label>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
{t(
|
||||||
|
'settings.startup.auto_startup_desc',
|
||||||
|
'Launch at sign-in and keep the app in the system tray',
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={config?.auto_startup || false}
|
||||||
|
onCheckedChange={async (checked) => {
|
||||||
|
if (config) {
|
||||||
|
await saveConfig({ ...config, auto_startup: checked });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{isMac && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
'settings.startup.macos_hint',
|
||||||
|
'macOS requires a signed app for Login Items to work. If auto-start fails, please sign the app or enable it manually in System Settings.',
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t('settings.about.title')}</CardTitle>
|
<CardTitle>{t('settings.about.title')}</CardTitle>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const AppConfigSchema = z.object({
|
|||||||
refresh_interval: z.number(), // minutes
|
refresh_interval: z.number(), // minutes
|
||||||
auto_sync: z.boolean(), // 是否自动同步
|
auto_sync: z.boolean(), // 是否自动同步
|
||||||
sync_interval: z.number(), // minutes
|
sync_interval: z.number(), // minutes
|
||||||
|
auto_startup: z.boolean(), // 是否随系统启动
|
||||||
default_export_path: z.string().nullable().optional(), // 导出路径
|
default_export_path: z.string().nullable().optional(), // 导出路径
|
||||||
proxy: ProxyConfigSchema,
|
proxy: ProxyConfigSchema,
|
||||||
});
|
});
|
||||||
@@ -37,6 +38,7 @@ export const DEFAULT_APP_CONFIG: AppConfig = {
|
|||||||
refresh_interval: 15,
|
refresh_interval: 15,
|
||||||
auto_sync: false,
|
auto_sync: false,
|
||||||
sync_interval: 5,
|
sync_interval: 5,
|
||||||
|
auto_startup: false,
|
||||||
default_export_path: null,
|
default_export_path: null,
|
||||||
proxy: {
|
proxy: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { app } from 'electron';
|
||||||
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
import { logger } from './logger';
|
||||||
|
import { AppConfig } from '../types/config';
|
||||||
|
|
||||||
|
export const AUTO_START_ARG = '--autostart';
|
||||||
|
const LINUX_AUTOSTART_FILENAME = 'antigravity-manager.desktop';
|
||||||
|
|
||||||
|
function getLinuxAutoStartPath() {
|
||||||
|
const dir = path.join(os.homedir(), '.config', 'autostart');
|
||||||
|
return path.join(dir, LINUX_AUTOSTART_FILENAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAutoStartLaunch(argv = process.argv) {
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
return argv.includes(AUTO_START_ARG);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32' || process.platform === 'darwin') {
|
||||||
|
try {
|
||||||
|
const settings = app.getLoginItemSettings();
|
||||||
|
return settings.wasOpenedAtLogin;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('AutoStart: Failed to read login item settings', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncWindowsOrMacAutoStart(enabled: boolean) {
|
||||||
|
try {
|
||||||
|
app.setLoginItemSettings({
|
||||||
|
openAtLogin: enabled,
|
||||||
|
openAsHidden: enabled,
|
||||||
|
path: process.execPath,
|
||||||
|
args: enabled ? [AUTO_START_ARG] : [],
|
||||||
|
});
|
||||||
|
const settings = app.getLoginItemSettings();
|
||||||
|
if (settings.openAtLogin !== enabled) {
|
||||||
|
logger.warn(
|
||||||
|
`AutoStart: Login item mismatch (expected ${enabled}, actual ${settings.openAtLogin})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
logger.info(`AutoStart: Login item set to ${enabled}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('AutoStart: Failed to update login item settings', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupLinuxAutoStartEntries(currentPath: string) {
|
||||||
|
const autoStartDir = path.dirname(currentPath);
|
||||||
|
if (!fs.existsSync(autoStartDir)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const legacyNames = [
|
||||||
|
`${app.getName()}.desktop`,
|
||||||
|
`${app.getName().replace(/\s+/g, '')}.desktop`,
|
||||||
|
'Antigravity Manager.desktop',
|
||||||
|
'AntigravityManager.desktop',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const name of legacyNames) {
|
||||||
|
const candidate = path.join(autoStartDir, name);
|
||||||
|
if (candidate === currentPath) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (fs.existsSync(candidate)) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(candidate);
|
||||||
|
logger.info(`AutoStart: Removed legacy autostart entry ${candidate}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('AutoStart: Failed to remove legacy autostart entry', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncLinuxAutoStart(enabled: boolean) {
|
||||||
|
const autoStartPath = getLinuxAutoStartPath();
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
if (fs.existsSync(autoStartPath)) {
|
||||||
|
fs.unlinkSync(autoStartPath);
|
||||||
|
logger.info('AutoStart: Removed Linux autostart entry');
|
||||||
|
}
|
||||||
|
cleanupLinuxAutoStartEntries(autoStartPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoStartDir = path.dirname(autoStartPath);
|
||||||
|
if (!fs.existsSync(autoStartDir)) {
|
||||||
|
fs.mkdirSync(autoStartDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const execPath = process.execPath.replace(/"/g, '\\"');
|
||||||
|
const content = [
|
||||||
|
'[Desktop Entry]',
|
||||||
|
'Type=Application',
|
||||||
|
`Name=${app.getName()}`,
|
||||||
|
`Exec="${execPath}" ${AUTO_START_ARG}`,
|
||||||
|
'X-GNOME-Autostart-enabled=true',
|
||||||
|
'Hidden=false',
|
||||||
|
'NoDisplay=false',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
cleanupLinuxAutoStartEntries(autoStartPath);
|
||||||
|
fs.writeFileSync(autoStartPath, content, 'utf-8');
|
||||||
|
logger.info('AutoStart: Created Linux autostart entry');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncAutoStart(config: AppConfig) {
|
||||||
|
const enabled = Boolean(config.auto_startup);
|
||||||
|
|
||||||
|
if (process.platform === 'win32' || process.platform === 'darwin') {
|
||||||
|
syncWindowsOrMacAutoStart(enabled);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
syncLinuxAutoStart(enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user