Files
geo/apps/desktop-client/src/main/app-settings.ts
T
root a4c654782b feat(desktop-client): split autostart and keep-alive per platform
Refactors openAtLogin and keepRunningInBackground to make the macOS/Windows
divergence explicit instead of leaning on Electron's cross-platform
abstraction:

- openAtLogin on Windows now passes path: process.execPath and args: []
  so the registry HKCU Run key always points at the right binary.
- keepRunningInBackground on Windows shows a one-shot tray balloon ("省心
  推已最小化到托盘…") so first-time users notice the app is still alive;
  persisted via a private hasShownBackgroundBalloon flag so it never fires
  again. macOS preserves the dock icon and stays silent.
- Sets AppUserModelID early on Windows so tray.displayBalloon's Win10+
  Toast routing actually fires.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:21:08 +08:00

189 lines
5.1 KiB
TypeScript

import { readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { app } from 'electron/main'
export interface DesktopAppSettings {
openAtLogin: boolean
keepRunningInBackground: boolean
}
export type DesktopAppSettingKey = keyof DesktopAppSettings
interface DesktopAppInternalFlags {
hasShownBackgroundBalloon: boolean
}
const DEFAULT_DESKTOP_APP_SETTINGS: DesktopAppSettings = {
openAtLogin: false,
keepRunningInBackground: true,
}
const DEFAULT_INTERNAL_FLAGS: DesktopAppInternalFlags = {
hasShownBackgroundBalloon: false,
}
let cachedSettings: DesktopAppSettings = { ...DEFAULT_DESKTOP_APP_SETTINGS }
let cachedFlags: DesktopAppInternalFlags = { ...DEFAULT_INTERNAL_FLAGS }
function settingsPath(): string {
return join(app.getPath('userData'), 'desktop-settings.json')
}
function normalizePersistedFile(input: unknown): {
settings: DesktopAppSettings
flags: DesktopAppInternalFlags
} {
if (!input || typeof input !== 'object') {
return {
settings: { ...DEFAULT_DESKTOP_APP_SETTINGS },
flags: { ...DEFAULT_INTERNAL_FLAGS },
}
}
const candidate = input as Partial<
Record<DesktopAppSettingKey | keyof DesktopAppInternalFlags, unknown>
>
return {
settings: {
openAtLogin:
typeof candidate.openAtLogin === 'boolean'
? candidate.openAtLogin
: DEFAULT_DESKTOP_APP_SETTINGS.openAtLogin,
keepRunningInBackground:
typeof candidate.keepRunningInBackground === 'boolean'
? candidate.keepRunningInBackground
: DEFAULT_DESKTOP_APP_SETTINGS.keepRunningInBackground,
},
flags: {
hasShownBackgroundBalloon:
typeof candidate.hasShownBackgroundBalloon === 'boolean'
? candidate.hasShownBackgroundBalloon
: DEFAULT_INTERNAL_FLAGS.hasShownBackgroundBalloon,
},
}
}
function readPersistedFile(): {
settings: DesktopAppSettings
flags: DesktopAppInternalFlags
} {
try {
return normalizePersistedFile(JSON.parse(readFileSync(settingsPath(), 'utf8')))
} catch {
return {
settings: { ...DEFAULT_DESKTOP_APP_SETTINGS },
flags: { ...DEFAULT_INTERNAL_FLAGS },
}
}
}
function persistFile(): void {
try {
writeFileSync(
settingsPath(),
JSON.stringify({ ...cachedSettings, ...cachedFlags }),
'utf8',
)
} catch (error) {
console.warn('[desktop-settings] persist failed', error)
}
}
function readSystemOpenAtLogin(fallback: boolean): boolean {
try {
return app.getLoginItemSettings().openAtLogin
} catch (error) {
console.warn('[desktop-settings] read login item failed', error)
return fallback
}
}
// Platform-specific login-item application.
//
// macOS — `setLoginItemSettings({ openAtLogin })` registers a LaunchAgent via
// the ServiceManagement framework. `path`/`args` are ignored on darwin; the
// system always launches the bundled .app. We keep `openAsHidden: false`
// because the user wants the main window to appear at login.
//
// Windows — `setLoginItemSettings` writes to
// `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`. In dev the registry
// would point at electron.exe, so we explicitly pass `path: process.execPath`
// and an empty `args: []` so the production NSIS-installed exe is registered
// correctly. No `--hidden` flag because the user wants a normal window at
// startup.
//
// Linux — `setLoginItemSettings` is a no-op in Electron; we silently accept
// the value but the OS will not honor it.
function applyOpenAtLogin(openAtLogin: boolean): boolean {
try {
if (process.platform === 'darwin') {
app.setLoginItemSettings({
openAtLogin,
openAsHidden: false,
})
} else if (process.platform === 'win32') {
app.setLoginItemSettings({
openAtLogin,
path: process.execPath,
args: [],
})
} else {
app.setLoginItemSettings({ openAtLogin })
}
return readSystemOpenAtLogin(openAtLogin)
} catch (error) {
console.warn('[desktop-settings] apply login item failed', error)
return openAtLogin
}
}
export function initDesktopAppSettings(): DesktopAppSettings {
const { settings, flags } = readPersistedFile()
cachedSettings = {
...settings,
openAtLogin: applyOpenAtLogin(settings.openAtLogin),
}
cachedFlags = flags
persistFile()
return getDesktopAppSettings()
}
export function getDesktopAppSettings(): DesktopAppSettings {
cachedSettings = {
...cachedSettings,
openAtLogin: readSystemOpenAtLogin(cachedSettings.openAtLogin),
}
return { ...cachedSettings }
}
export function setDesktopAppSetting(
key: DesktopAppSettingKey,
value: boolean,
): DesktopAppSettings {
const next: DesktopAppSettings = {
...cachedSettings,
[key]: value,
}
if (key === 'openAtLogin') {
next.openAtLogin = applyOpenAtLogin(value)
}
cachedSettings = next
persistFile()
return getDesktopAppSettings()
}
export function hasShownBackgroundBalloon(): boolean {
return cachedFlags.hasShownBackgroundBalloon
}
export function markBackgroundBalloonShown(): void {
if (cachedFlags.hasShownBackgroundBalloon) {
return
}
cachedFlags = { ...cachedFlags, hasShownBackgroundBalloon: true }
persistFile()
}