add tray & creating shortcuts

This commit is contained in:
olcxja 2026-09-26 13:57:12 +02:00
commit 1097a1f441
11 changed files with 452 additions and 22 deletions

View file

@ -7,28 +7,216 @@ const wines = require('./wines')
const { registerIpc } = require('./ipc')
const { createMainWindow } = require('./window')
const { setAppIcon } = require('./appicon')
const path = require('path')
const fs = require('fs')
app.setName('PLWL')
// The launcher keeps running with only a tray icon, so closing the window
// must not quit the app (playtime keeps counting while a game runs).
app.on('window-all-closed', e => {
// no-op on purpose: tray Quit / menu Quit is the way out
})
let win = null
let tray = null
let quitting = false
let headless = false // started via --launch: no window until the user asks
let autoLaunchId = null // game id from --launch <id>
let autoQuitWhenDone = false // headless boot: quit after that game exits
const getWindow = () => win
// --- single instance ---------------------------------------------------------
// A second launcher invocation (e.g. a desktop shortcut with --launch <id>)
// hands its arguments to the running instance and exits immediately.
const gotLock = app.requestSingleInstanceLock()
if (!gotLock) {
app.quit()
} else {
app.on('second-instance', (e, argv) => {
const id = parseLaunchId(argv)
lastHandoffAt = Date.now()
if (id) launchGame(id)
else showMainWindow()
})
}
// set on every second-instance handoff: the auto-quit check skips itself for
// a moment afterwards, so "one game exits" racing "a shortcut just started
// another game" can never kill the launcher mid-handoff
let lastHandoffAt = 0
function parseLaunchId(argv) {
const args = Array.isArray(argv) ? argv : process.argv
const i = args.indexOf('--launch')
if (i !== -1 && args[i + 1]) return args[i + 1]
const eq = args.find(a => a.startsWith('--launch='))
return eq ? eq.slice('--launch='.length) : null
}
autoLaunchId = parseLaunchId(process.argv)
headless = !!autoLaunchId
autoQuitWhenDone = headless // booting just to run one game -> exit when it ends
// --- tray ---------------------------------------------------------------------
// Lives as long as the app does: shows "active in background" even when the
// window is closed, and offers running games + Quit.
function trayIconPath() {
const png = path.resolve(__dirname, '..', 'renderer', 'plwl.png')
return fs.existsSync(png) ? png : undefined
}
function createTray() {
const { Tray, Menu } = require('electron')
try {
tray = new Tray(trayIconPath())
} catch {
return // no icon available; window still works normally
}
tray.setToolTip('PLWL')
rebuildTrayMenu()
}
function rebuildTrayMenu() {
if (!tray) return
const { Menu } = require('electron')
const lang = loadLangSafe()
const gamesSvc = gamesService
const running = gamesSvc ? gamesSvc.listRunning() : []
const items = []
items.push({
label: i18n.translate(lang, 'tray.open'),
click: () => showMainWindow()
})
if (running.length) {
items.push({ type: 'separator' })
items.push({ label: i18n.translate(lang, 'tray.running'), enabled: false })
for (const id of running) {
const game = require('./library').getGame(id)
if (!game) continue
items.push({
label: `• ${game.name}`,
click: () => {
// click toggles: stop the running game
gamesSvc.kill(id)
}
})
}
}
items.push({ type: 'separator' })
items.push({
label: i18n.translate(lang, 'tray.quit'),
click: () => {
quitting = true
app.quit()
}
})
tray.setContextMenu(Menu.buildFromTemplate(items))
const tip = running.length
? `PLWL - ${i18n.translate(lang, 'tray.tooltipRunning', running.length)}`
: 'PLWL'
tray.setToolTip(tip)
}
function loadLangSafe() {
try {
return require('./library').loadSettings().language || 'en-us'
} catch {
return 'en-us'
}
}
function showMainWindow() {
// the user is present now - headless auto-quit must never fire again
headless = false
autoQuitWhenDone = false
if (gamesService) gamesService.setOnGameExit(null)
if (win && !win.isDestroyed()) {
win.show()
win.focus()
return
}
win = createMainWindow()
wireWindow(win)
}
function wireWindow(w) {
// closing the window hides it to the tray (playtime keeps counting in the
// background) unless the user disabled that in settings - then it quits
w.on('close', e => {
if (quitting) return
let closeToTray = true
try {
closeToTray = require('./library').loadSettings().closeToTray !== false
} catch {}
if (closeToTray) {
e.preventDefault()
w.hide()
} else {
quitting = true
app.quit()
}
})
w.on('closed', () => {
win = null
})
}
// --- game auto-launch (shortcuts) -----------------------------------------------
let gamesService = null
async function launchGame(id) {
if (!gamesService) return
const res = await gamesService.start(getWindow(), id)
if (res.ok) {
rebuildTrayMenu()
// headless boot: when the auto-started game finishes and nothing else is
// running, quit the launcher
if (autoQuitWhenDone) {
gamesService.setOnGameExit(() => {
// fires for EVERY game exit; quit only when the last one just ended,
// the user never opened the window and no shortcut handed a new game
// over in the last few seconds (start/exit race guard)
if (Date.now() - lastHandoffAt < 5000) return
if (!gamesService.listRunning().length) {
quitting = true
app.quit()
}
})
}
} else if (headless && autoQuitWhenDone) {
// nothing to run (bad shortcut target) -> do not linger in the tray
quitting = true
app.quit()
}
}
app.whenReady().then(async () => {
if (process.platform === 'linux') {
try {
setAppIcon(app)
} catch {}
}
registerIpc({ getWindow })
ensureDirs()
await i18n.init()
await wines.refreshWines()
win = createMainWindow()
win.on('closed', () => {
win = null
})
const { games } = registerIpc({ getWindow, onGamesChanged: rebuildTrayMenu })
gamesService = games
createTray()
if (headless) {
// shortcut boot: run the game without opening the launcher window
launchGame(autoLaunchId)
} else {
win = createMainWindow()
wireWindow(win)
}
})
app.on('window-all-closed', () => {
app.quit()
app.on('before-quit', () => {
quitting = true
})