'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() })