335 lines
10 KiB
JavaScript
335 lines
10 KiB
JavaScript
'use strict'
|
|
|
|
const { app, BrowserWindow, ipcMain, dialog, shell, protocol, net } = require('electron')
|
|
const path = require('path')
|
|
const fs = require('fs')
|
|
const { ensureDirs, DIRS, readJson, writeJson } = require('./paths')
|
|
const wines = require('./wines')
|
|
const prefixes = require('./prefixes')
|
|
const library = require('./library')
|
|
const runner = require('./runner')
|
|
const importer = require('./importer')
|
|
const i18n = require('./i18n')
|
|
const { createMainWindow } = require('./window')
|
|
|
|
app.setName('PLGL')
|
|
|
|
let win = null
|
|
|
|
function t(code, key, ...args) {
|
|
return i18n.translate(code, key, ...args)
|
|
}
|
|
|
|
function wineFor(id) {
|
|
if (!id || id === 'system') {
|
|
return { id: 'system', type: 'system', label: 'System Wine' }
|
|
}
|
|
if (id === 'default') {
|
|
id = library.loadSettings().defaultWine || 'system'
|
|
}
|
|
const w = wines.getWine(id)
|
|
if (!w) {
|
|
const fallback = library.loadSettings().defaultWine || 'system'
|
|
return wineFor(fallback)
|
|
}
|
|
return w
|
|
}
|
|
|
|
function scanGamesFolder(dir, depth, out) {
|
|
if (depth <= 0 || !out) return
|
|
let entries = []
|
|
try {
|
|
entries = require('fs').readdirSync(dir, { withFileTypes: true })
|
|
} catch {
|
|
return
|
|
}
|
|
for (const e of entries) {
|
|
if (!e.isDirectory()) continue
|
|
const full = path.join(dir, e.name)
|
|
if (require('fs').existsSync(path.join(full, '.plgl-imported'))) {
|
|
out.push(full)
|
|
} else {
|
|
scanGamesFolder(full, depth - 1, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
ipcMain.handle('i18n:list', async () => {
|
|
await i18n.init()
|
|
return i18n.listLangs()
|
|
})
|
|
|
|
ipcMain.handle('i18n:get', async (e, code) => {
|
|
const lang = await i18n.loadLang(code)
|
|
if (lang) return lang
|
|
return (await i18n.loadLang(i18n.FALLBACK)) || { data: {}, texts: {} }
|
|
})
|
|
|
|
ipcMain.handle('settings:get', () => library.loadSettings())
|
|
ipcMain.handle('settings:set', (e, patch) => library.saveSettings(patch))
|
|
|
|
ipcMain.handle('wines:list', async () => {
|
|
await wines.refreshWines()
|
|
return [
|
|
{ id: 'system', type: 'system', label: 'System Wine', version: detectSystemWineVersion() },
|
|
...wines.getWines()
|
|
]
|
|
})
|
|
|
|
function detectSystemWineVersion() {
|
|
try {
|
|
const { execFileSync } = require('child_process')
|
|
return String(execFileSync('wine', ['--version'], { timeout: 15000 })).trim().replace(/^wine-?/i, '')
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
ipcMain.handle('wines:refresh', async () => {
|
|
const list = await wines.refreshWines()
|
|
return list.filter(w => !w.disabled)
|
|
})
|
|
|
|
ipcMain.handle('wines:setDisabled', (e, id, disabled) => wines.setWineDisabled(id, disabled))
|
|
ipcMain.handle('wines:setDefault', (e, id) => wines.setDefaultWine(id))
|
|
ipcMain.handle('wines:delete', (e, id) => wines.deleteWine(id))
|
|
ipcMain.handle('wines:import', async (e, srcDir) => {
|
|
try {
|
|
return await wines.importWineFolder(srcDir)
|
|
} catch (err) {
|
|
return { ok: false, error: String(err && err.message || err) }
|
|
}
|
|
})
|
|
|
|
ipcMain.handle('prefixes:list', () => prefixes.listPrefixes())
|
|
ipcMain.handle('prefixes:create', (e, name) => prefixes.createPrefix(name))
|
|
ipcMain.handle('prefixes:delete', (e, name) => prefixes.deletePrefix(name))
|
|
ipcMain.handle('prefixes:setDefault', (e, name) => prefixes.setDefaultPrefix(name))
|
|
|
|
ipcMain.handle('games:list', () => library.loadGames())
|
|
ipcMain.handle('games:remove', (e, id) => library.removeGame(id))
|
|
ipcMain.handle('games:get', (e, id) => library.getGame(id))
|
|
ipcMain.handle('games:update', (e, id, patch) => library.updateGame(id, patch))
|
|
ipcMain.handle('games:clearPlaytime', (e, id) => library.updateGame(id, { playSeconds: 0, lastPlayed: 0 }))
|
|
ipcMain.handle('games:resetIcon', (e, id) => {
|
|
const game = library.getGame(id)
|
|
if (!game) return { ok: false }
|
|
for (const f of ['icon.png', 'icon.ico']) {
|
|
try {
|
|
fs.unlinkSync(path.join(DIRS.DATA, 'icons', id, f))
|
|
} catch {}
|
|
}
|
|
library.updateGame(id, { iconSource: 'none' })
|
|
return { ok: true }
|
|
})
|
|
ipcMain.handle('games:setIconFile', async (e, id, filePath) => {
|
|
const game = library.getGame(id)
|
|
if (!game || !filePath) return { ok: false, error: 'bad-request' }
|
|
try {
|
|
const dir = path.join(DIRS.DATA, 'icons', id)
|
|
fs.mkdirSync(dir, { recursive: true })
|
|
for (const f of fs.readdirSync(dir)) {
|
|
if (f.startsWith('icon.')) fs.unlinkSync(path.join(dir, f))
|
|
}
|
|
const icon = require('./icons')
|
|
let dst
|
|
if (filePath.toLowerCase().endsWith('.exe')) {
|
|
dst = path.join(dir, 'icon.ico')
|
|
const res = icon.extractIconFromExe(filePath, dst)
|
|
if (!res.ok) return res
|
|
} else {
|
|
dst = path.join(dir, `icon${path.extname(filePath).toLowerCase() || '.png'}`)
|
|
fs.copyFileSync(filePath, dst)
|
|
}
|
|
library.updateGame(id, { iconSource: dst })
|
|
return { ok: true, path: dst }
|
|
} catch (err) {
|
|
return { ok: false, error: String(err && err.message || err) }
|
|
}
|
|
})
|
|
ipcMain.handle('games:extractIcon', (e, id) => {
|
|
const game = library.getGame(id)
|
|
if (!game || !game.exePath) return { ok: false, error: 'bad-request' }
|
|
const out = path.join(DIRS.DATA, 'icons', id, 'icon.ico')
|
|
const res = require('./icons').extractIconFromExe(game.exePath, out)
|
|
if (res.ok) library.updateGame(id, { iconSource: out })
|
|
return res
|
|
})
|
|
ipcMain.handle('games:autoIcon', (e, id) => {
|
|
const game = library.getGame(id)
|
|
if (!game || !game.exePath) return { ok: false, error: 'bad-request' }
|
|
const dir = path.join(DIRS.DATA, 'icons', id)
|
|
try {
|
|
for (const f of fs.readdirSync(dir)) {
|
|
if (f.startsWith('icon.')) fs.unlinkSync(path.join(dir, f))
|
|
}
|
|
} catch {}
|
|
const out = path.join(dir, 'icon.ico')
|
|
const res = require('./icons').extractIconFromExe(game.exePath, out)
|
|
if (res.ok) library.updateGame(id, { iconSource: out })
|
|
return res
|
|
})
|
|
ipcMain.handle('games:iconPath', (e, id) => {
|
|
const game = library.getGame(id)
|
|
if (!game || !game.iconSource) return null
|
|
return fs.existsSync(game.iconSource) ? game.iconSource : null
|
|
})
|
|
|
|
ipcMain.handle('games:pickFolder', async e => {
|
|
const res = await dialog.showOpenDialog(win, {
|
|
title: t(library.loadSettings().language, 'import.pickFolder'),
|
|
properties: ['openDirectory']
|
|
})
|
|
if (res.canceled || !res.filePaths[0]) return null
|
|
return res.filePaths[0]
|
|
})
|
|
|
|
ipcMain.handle('games:import', async (e, srcDir, mode) => {
|
|
const onProgress = data => {
|
|
if (win && !win.isDestroyed()) win.webContents.send('import:progress', data)
|
|
}
|
|
const game = await importer.importGame(srcDir, mode, onProgress)
|
|
library.addGame(game)
|
|
require('fs').writeFileSync(path.join(game.workdir, '.plgl-imported'), '')
|
|
return game
|
|
})
|
|
|
|
const running = new Map()
|
|
const killRequests = new Set()
|
|
|
|
ipcMain.handle('games:run', async (e, id) => {
|
|
const game = library.getGame(id)
|
|
if (!game) return { ok: false, error: 'game-not-found' }
|
|
if (running.has(id)) return { ok: false, error: 'already-running' }
|
|
running.set(id, null)
|
|
let wine
|
|
try {
|
|
wine = wineFor(game.wine)
|
|
} catch {
|
|
running.delete(id)
|
|
return { ok: false, error: 'wine-not-found' }
|
|
}
|
|
const systemWine = wine.type === 'system'
|
|
try {
|
|
const res = await runner.runGame({
|
|
game,
|
|
wine,
|
|
systemWine,
|
|
onExit: () => {
|
|
running.delete(id)
|
|
if (win && !win.isDestroyed()) win.webContents.send('games:changed')
|
|
}
|
|
})
|
|
if (res.pid) running.set(id, res.pid)
|
|
if (killRequests.has(id)) {
|
|
killRequests.delete(id)
|
|
if (res.pid) {
|
|
try {
|
|
process.kill(-res.pid, 'SIGKILL')
|
|
} catch {}
|
|
try {
|
|
process.kill(res.pid, 'SIGKILL')
|
|
} catch {}
|
|
}
|
|
}
|
|
if (win && !win.isDestroyed()) win.webContents.send('games:changed')
|
|
return { ok: true }
|
|
} catch (err) {
|
|
running.delete(id)
|
|
return { ok: false, error: String(err && err.message || err) }
|
|
}
|
|
})
|
|
|
|
ipcMain.handle('games:runTool', async (e, id, tool, args) => {
|
|
const game = library.getGame(id)
|
|
if (!game) return { ok: false, error: 'game-not-found' }
|
|
let wine
|
|
try {
|
|
wine = wineFor(game.wine)
|
|
} catch {
|
|
return { ok: false, error: 'wine-not-found' }
|
|
}
|
|
const systemWine = wine.type === 'system'
|
|
try {
|
|
await runner.runTool({ game, wine, tool, args, systemWine })
|
|
return { ok: true }
|
|
} catch (err) {
|
|
return { ok: false, error: String(err && err.message || err) }
|
|
}
|
|
})
|
|
|
|
ipcMain.handle('dialog:pickFile', async (e, opts) => {
|
|
const res = await dialog.showOpenDialog(win, {
|
|
properties: ['openFile', 'openDirectory'],
|
|
...(opts || {})
|
|
})
|
|
if (res.canceled || !res.filePaths[0]) return null
|
|
return res.filePaths[0]
|
|
})
|
|
|
|
ipcMain.handle('shell:openPath', (e, p) => shell.openPath(p))
|
|
ipcMain.handle('shell:showItemInFolder', (e, p) => {
|
|
shell.showItemInFolder(p)
|
|
return true
|
|
})
|
|
|
|
ipcMain.handle('games:getLogs', (e, id) => {
|
|
const dir = path.join(DIRS.DATA, 'logs')
|
|
let files = []
|
|
try {
|
|
files = require('fs').readdirSync(dir).filter(f => f.startsWith(id + '-')).sort()
|
|
} catch {}
|
|
return files.map(f => path.join(dir, f))
|
|
})
|
|
|
|
ipcMain.handle('games:kill', (e, id) => {
|
|
const pid = running.get(id)
|
|
if (pid) {
|
|
try {
|
|
process.kill(-pid, 'SIGKILL')
|
|
} catch {}
|
|
try {
|
|
process.kill(pid, 'SIGKILL')
|
|
} catch {}
|
|
running.delete(id)
|
|
if (win && !win.isDestroyed()) win.webContents.send('games:changed')
|
|
} else if (running.has(id)) {
|
|
killRequests.add(id)
|
|
}
|
|
return { ok: true }
|
|
})
|
|
|
|
ipcMain.handle('games:isRunning', (e, id) => running.has(id))
|
|
ipcMain.handle('games:listRunning', () => [...running.keys()])
|
|
|
|
app.whenReady().then(async () => {
|
|
if (process.platform === 'linux') {
|
|
const { setAppIcon } = require('./appicon')
|
|
try {
|
|
setAppIcon(app)
|
|
} catch {}
|
|
}
|
|
protocol.handle('plgl-icon', request => {
|
|
const id = decodeURIComponent(new URL(request.url).hostname)
|
|
const dir = path.join(DIRS.DATA, 'icons', id)
|
|
let file = null
|
|
try {
|
|
const files = fs.readdirSync(dir).filter(f => f.startsWith('icon.')).sort()
|
|
const pick = files.find(f => /^icon\.(png|jpe?g|svg|ico|webp|gif|bmp)$/i.test(f))
|
|
if (pick) file = path.join(dir, pick)
|
|
} catch {}
|
|
if (!file) return new Response('', { status: 404 })
|
|
return net.fetch('file://' + file)
|
|
})
|
|
ensureDirs()
|
|
await i18n.init()
|
|
await wines.refreshWines()
|
|
win = createMainWindow()
|
|
win.on('closed', () => {
|
|
win = null
|
|
})
|
|
})
|
|
|
|
app.on('window-all-closed', () => {
|
|
app.quit()
|
|
})
|