a4c654782b
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>
189 lines
5.1 KiB
TypeScript
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()
|
|
}
|