add tray & creating shortcuts
This commit is contained in:
parent
81f5eadc76
commit
1097a1f441
11 changed files with 452 additions and 22 deletions
124
src/shortcuts.js
Normal file
124
src/shortcuts.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
'use strict'
|
||||
|
||||
// .desktop shortcut generation. A shortcut launches the launcher itself with
|
||||
// `--launch <id>`: the main process either starts the game in the running
|
||||
// instance or boots headless (no window, tray only), starts the game and lets
|
||||
// playtime tracking continue normally.
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { DIRS, resolvePortablePath } = require('./paths')
|
||||
|
||||
const DATA_DIR = process.env.PLWL_BUILD_DIR ? path.join(process.env.PLWL_BUILD_DIR, 'files', 'data') : DIRS.DATA
|
||||
|
||||
// Where user-visible shortcuts live (XDG). Override with PLWL_SHORTCUTS_DIR
|
||||
// so tests (and portable exotic setups) can redirect it.
|
||||
function desktopDirs() {
|
||||
const home = process.env.HOME || '/'
|
||||
const dataHome = process.env.XDG_DATA_HOME || path.join(home, '.local', 'share')
|
||||
return {
|
||||
menu: process.env.PLWL_SHORTCUTS_DIR || path.join(dataHome, 'applications'),
|
||||
desktop: process.env.PLWL_SHORTCUTS_DIR || path.join(home, 'Desktop')
|
||||
}
|
||||
}
|
||||
|
||||
// Launcher executable as the shortcut will see it, as an argv list: the
|
||||
// packaged olcxja.plwl/olcxja.plwl binary, or `electron <project>` in dev.
|
||||
function launcherArgv() {
|
||||
if (process.env.PLWL_LAUNCHER_CMD) return process.env.PLWL_LAUNCHER_CMD.split(' ').filter(Boolean)
|
||||
const m = __dirname.match(/^(.*)[/\\]app\.asar([/\\]|$)/)
|
||||
if (m) {
|
||||
const root = path.resolve(m[1], '..', '..')
|
||||
const bin = path.join(root, 'olcxja.plwl', 'olcxja.plwl')
|
||||
if (fs.existsSync(bin)) return [bin]
|
||||
}
|
||||
// dev fallback: run through electron from the project dir
|
||||
return ['electron', path.resolve(__dirname, '..')]
|
||||
}
|
||||
|
||||
// Desktop Entry Exec field escaping (per the spec): % must be doubled,
|
||||
// quotes and backslashes escaped, and tokens containing spaces wrapped in
|
||||
// double quotes.
|
||||
function execToken(token) {
|
||||
const escaped = token.replace(/%/g, '%%').replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
||||
return /[ $"'\\]/.test(token) ? `"${escaped}"` : escaped
|
||||
}
|
||||
|
||||
function execLine(extraArgs) {
|
||||
return [...launcherArgv(), ...extraArgs.split(' ').filter(Boolean).map(execToken)].map(execToken).join(' ')
|
||||
}
|
||||
|
||||
function sanitizeName(name) {
|
||||
return String(name || 'App').replace(/[\r\n]/g, ' ').trim() || 'App'
|
||||
}
|
||||
|
||||
// Builds the .desktop file content for launching app `id`.
|
||||
function buildDesktopEntry(game) {
|
||||
const name = sanitizeName(game.name)
|
||||
const iconDir = path.join(DATA_DIR, 'icons', game.id)
|
||||
let icon = ''
|
||||
try {
|
||||
const found = fs.readdirSync(iconDir).filter(f => f.startsWith('icon.')).sort().pop()
|
||||
if (found) icon = path.join(iconDir, found)
|
||||
} catch {}
|
||||
if (!icon) {
|
||||
// fall back to the launcher icon so the shortcut is never naked
|
||||
const png = path.resolve(__dirname, '..', 'renderer', 'plwl.png')
|
||||
if (fs.existsSync(png)) icon = png
|
||||
}
|
||||
const lines = [
|
||||
'[Desktop Entry]',
|
||||
'Type=Application',
|
||||
`Name=${name}`,
|
||||
'Comment=Launch via PLWL',
|
||||
`Exec=${execLine(`--launch ${game.id}`)}`,
|
||||
'Terminal=false',
|
||||
'Categories=Game;'
|
||||
]
|
||||
if (icon) lines.push(`Icon=${icon}`)
|
||||
return lines.join('\n') + '\n'
|
||||
}
|
||||
|
||||
// Where a given shortcut would be written (used by tests and by the UI hint).
|
||||
function shortcutPaths(gameId) {
|
||||
const dirs = desktopDirs()
|
||||
return { menu: path.join(dirs.menu, `plwl-${gameId}.desktop`), desktop: path.join(dirs.desktop, `plwl-${gameId}.desktop`) }
|
||||
}
|
||||
|
||||
// Writes the shortcut. Returns the paths it created.
|
||||
function createShortcut(game) {
|
||||
const content = buildDesktopEntry(game)
|
||||
const dirs = desktopDirs()
|
||||
const written = []
|
||||
for (const [key, dir] of [['menu', dirs.menu], ['desktop', dirs.desktop]]) {
|
||||
try {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
const file = path.join(dir, `plwl-${game.id}.desktop`)
|
||||
fs.writeFileSync(file, content)
|
||||
if (key === 'menu') {
|
||||
// update desktop databases best-effort so the entry shows up at once
|
||||
try {
|
||||
require('child_process').execFileSync('update-desktop-database', [dir], { timeout: 10000 })
|
||||
} catch {}
|
||||
}
|
||||
written.push(file)
|
||||
} catch (err) {
|
||||
// a read-only Desktop must not fail the whole operation
|
||||
if (key === 'desktop') continue
|
||||
throw err
|
||||
}
|
||||
}
|
||||
return { ok: true, paths: written }
|
||||
}
|
||||
|
||||
// Removes shortcuts for a game (also called when the game is removed).
|
||||
function removeShortcut(gameId) {
|
||||
const dirs = desktopDirs()
|
||||
for (const file of [path.join(dirs.menu, `plwl-${gameId}.desktop`), path.join(dirs.desktop, `plwl-${gameId}.desktop`)]) {
|
||||
try {
|
||||
fs.rmSync(file, { force: true })
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { buildDesktopEntry, createShortcut, removeShortcut, shortcutPaths, launcherArgv }
|
||||
Loading…
Add table
Add a link
Reference in a new issue