274 lines
11 KiB
JavaScript
274 lines
11 KiB
JavaScript
'use strict'
|
|
|
|
const { ipcMain, dialog, shell, protocol, net } = require('electron')
|
|
const path = require('path')
|
|
const fs = require('fs')
|
|
const { DIRS } = require('../paths')
|
|
const i18n = require('../i18n')
|
|
const library = require('../library')
|
|
const wines = require('../wines')
|
|
const prefixes = require('../prefixes')
|
|
const runner = require('../runner')
|
|
const { extractIconFromExe } = require('../icons')
|
|
const { createGamesService, sendToWindow } = require('./games')
|
|
const shortcuts = require('../shortcuts')
|
|
const openwith = require('../openwith')
|
|
const { registerLanSyncIpc } = require('./lansync')
|
|
|
|
const ICON_EXTS = ['png', 'jpe?g', 'svg', 'ico', 'webp', 'gif', 'bmp']
|
|
|
|
function errorResult(err) {
|
|
return { ok: false, error: String((err && err.message) || err) }
|
|
}
|
|
|
|
// --- icon helpers -----------------------------------------------------------
|
|
|
|
const iconDir = id => path.join(DIRS.DATA, 'icons', id)
|
|
|
|
function clearIconDir(id) {
|
|
try {
|
|
for (const f of fs.readdirSync(iconDir(id))) {
|
|
if (f.startsWith('icon.')) fs.rmSync(path.join(iconDir(id), f), { force: true })
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
function extractGameIcon(id, exePath) {
|
|
const out = path.join(iconDir(id), 'icon.ico')
|
|
const res = extractIconFromExe(exePath, out)
|
|
if (res.ok) library.updateGame(id, { iconSource: out })
|
|
return res
|
|
}
|
|
|
|
// --- registration -----------------------------------------------------------
|
|
|
|
function registerIpc({ getWindow, onGamesChanged }) {
|
|
const games = createGamesService({ getWindow, onGamesChanged })
|
|
|
|
// i18n ---------------------------------------------------------------------
|
|
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: {} }
|
|
})
|
|
|
|
// settings -------------------------------------------------------------------
|
|
ipcMain.handle('settings:get', () => library.loadSettings())
|
|
ipcMain.handle('settings:set', (e, patch) => library.saveSettings(patch))
|
|
|
|
// wines ----------------------------------------------------------------------
|
|
function systemWine() {
|
|
let version = ''
|
|
try {
|
|
const { execFileSync } = require('child_process')
|
|
version = String(execFileSync('wine', ['--version'], { timeout: 15000 })).trim().replace(/^wine-?/i, '')
|
|
} catch {}
|
|
return { id: 'system', type: 'system', label: 'System Wine', version }
|
|
}
|
|
|
|
ipcMain.handle('wines:list', async () => {
|
|
await wines.refreshWines()
|
|
return [systemWine(), ...wines.getWines()]
|
|
})
|
|
ipcMain.handle('wines:refresh', async () => (await wines.refreshWines()).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 errorResult(err)
|
|
}
|
|
})
|
|
|
|
// prefixes ---------------------------------------------------------------------
|
|
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))
|
|
|
|
// games: library -----------------------------------------------------------------
|
|
ipcMain.handle('games:list', () => library.loadGames())
|
|
ipcMain.handle('games:get', (e, id) => library.getGame(id))
|
|
// prefix pin is enforced inside library.updateGame (prefix/importSource
|
|
// fields are ignored for prefix imports), so nothing extra needed here
|
|
ipcMain.handle('games:update', (e, id, patch) => library.updateGame(id, patch))
|
|
ipcMain.handle('games:remove', (e, id) => {
|
|
shortcuts.removeShortcut(id)
|
|
return library.removeGame(id)
|
|
})
|
|
ipcMain.handle('games:createShortcut', (e, id) => {
|
|
const game = library.getGame(id)
|
|
if (!game) return { ok: false, error: 'game-not-found' }
|
|
try {
|
|
return shortcuts.createShortcut(game)
|
|
} catch (err) {
|
|
return errorResult(err)
|
|
}
|
|
})
|
|
ipcMain.handle('games:shortcutPaths', (e, id) => shortcuts.shortcutPaths(id))
|
|
ipcMain.handle('openwith:texts', (e, lang) => openwith.textsFor(lang))
|
|
ipcMain.handle('games:clearPlaytime', (e, id) => library.clearPlaytime(id))
|
|
|
|
// games: icons ---------------------------------------------------------------------
|
|
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:resetIcon', (e, id) => {
|
|
if (!library.getGame(id)) return { ok: false }
|
|
clearIconDir(id)
|
|
library.updateGame(id, { iconSource: 'none' })
|
|
return { ok: true }
|
|
})
|
|
ipcMain.handle('games:setIconFile', async (e, id, filePath) => {
|
|
if (!library.getGame(id) || !filePath) return { ok: false, error: 'bad-request' }
|
|
try {
|
|
clearIconDir(id)
|
|
fs.mkdirSync(iconDir(id), { recursive: true })
|
|
let dst
|
|
if (filePath.toLowerCase().endsWith('.exe')) {
|
|
dst = path.join(iconDir(id), 'icon.ico')
|
|
const res = extractIconFromExe(filePath, dst)
|
|
if (!res.ok) return res
|
|
} else {
|
|
dst = path.join(iconDir(id), `icon${path.extname(filePath).toLowerCase() || '.png'}`)
|
|
fs.copyFileSync(filePath, dst)
|
|
}
|
|
library.updateGame(id, { iconSource: dst })
|
|
return { ok: true, path: dst }
|
|
} catch (err) {
|
|
return errorResult(err)
|
|
}
|
|
})
|
|
ipcMain.handle('games:extractIcon', (e, id) => {
|
|
const game = library.getGame(id)
|
|
if (!game || !game.exePath) return { ok: false, error: 'bad-request' }
|
|
return extractGameIcon(id, game.exePath)
|
|
})
|
|
ipcMain.handle('games:autoIcon', (e, id) => {
|
|
const game = library.getGame(id)
|
|
if (!game || !game.exePath) return { ok: false, error: 'bad-request' }
|
|
clearIconDir(id)
|
|
return extractGameIcon(id, game.exePath)
|
|
})
|
|
|
|
// "Open with PLWL" ------------------------------------------------------------------
|
|
// The renderer asks for the file handed in via `--open` (Exec %f), then
|
|
// launches it with the wine/prefix picked in the popup. Nothing is imported
|
|
// into the library.
|
|
ipcMain.handle('openwith:take', async () => {
|
|
const p = openwith.getPending()
|
|
if (!p) return null
|
|
openwith.clearPending()
|
|
return p
|
|
})
|
|
ipcMain.handle('openwith:run', async (e, payload) => {
|
|
try {
|
|
return await openwith.run({
|
|
openPath: payload && payload.openPath,
|
|
wine: payload && payload.wine,
|
|
prefix: payload && payload.prefix,
|
|
lang: library.loadSettings().language
|
|
})
|
|
} catch (err) {
|
|
return errorResult(err)
|
|
}
|
|
})
|
|
|
|
// games: run --------------------------------------------------------------------------
|
|
ipcMain.handle('games:run', (e, id) => games.start(getWindow(), id))
|
|
ipcMain.handle('games:kill', (e, id) => games.kill(id))
|
|
ipcMain.handle('games:runTool', (e, id, tool, args) => games.runToolFor(id, tool, args))
|
|
ipcMain.handle('games:isRunning', (e, id) => games.isRunning(id))
|
|
ipcMain.handle('games:listRunning', () => games.listRunning())
|
|
ipcMain.handle('games:getLogs', (e, id) => {
|
|
const file = runner.latestLogPath(id)
|
|
return fs.existsSync(file) ? [file] : []
|
|
})
|
|
|
|
// games: import -------------------------------------------------------------------------
|
|
ipcMain.handle('games:pickFolder', async () => {
|
|
const res = await dialog.showOpenDialog(getWindow(), {
|
|
title: i18n.translate(library.loadSettings().language, 'import.pickFolder'),
|
|
properties: ['openDirectory']
|
|
})
|
|
return res.canceled || !res.filePaths[0] ? null : res.filePaths[0]
|
|
})
|
|
ipcMain.handle('games:import', async (e, srcDir, mode) => {
|
|
try {
|
|
return await games.import(getWindow(), srcDir, mode)
|
|
} catch (err) {
|
|
return errorResult(err)
|
|
}
|
|
})
|
|
// import from an existing wine prefix: pick the prefix first, then the app
|
|
// folder inside it (drive_c) - paths are stored prefix-relative so the app
|
|
// shows up for wine as a regular C:\ install
|
|
ipcMain.handle('games:pickPrefixFolder', async (e, prefix) => {
|
|
// open the picker straight in the prefix's C: drive (plain wine: top
|
|
// level, Proton: under pfx/)
|
|
const driveC = prefixes.prefixDriveRoot(prefix)
|
|
const res = await dialog.showOpenDialog(getWindow(), {
|
|
title: i18n.translate(library.loadSettings().language, 'import.pickPrefixFolder'),
|
|
defaultPath: driveC || prefixes.prefixPath(prefix),
|
|
properties: ['openDirectory']
|
|
})
|
|
return res.canceled || !res.filePaths[0] ? null : res.filePaths[0]
|
|
})
|
|
ipcMain.handle('games:importFromPrefix', async (e, prefix, srcDir) => {
|
|
try {
|
|
return await games.importFromPrefix(getWindow(), prefix, srcDir)
|
|
} catch (err) {
|
|
return errorResult(err)
|
|
}
|
|
})
|
|
|
|
// lan syncing ---------------------------------------------------------------------------------
|
|
registerLanSyncIpc({ getWindow })
|
|
|
|
// dialogs & shell ---------------------------------------------------------------------------
|
|
ipcMain.handle('dialog:pickFile', async (e, opts) => {
|
|
const o = { ...(opts || {}) }
|
|
// Electron resolves a RELATIVE defaultPath against the process cwd (the
|
|
// install root in packaged mode), which lands the picker somewhere
|
|
// random like '/'. Renderer paths are FILES-relative by convention, so
|
|
// anchor them there instead.
|
|
if (o.defaultPath && typeof o.defaultPath === 'string' && !path.isAbsolute(o.defaultPath)) {
|
|
o.defaultPath = path.join(DIRS.FILES, o.defaultPath)
|
|
}
|
|
const res = await dialog.showOpenDialog(getWindow(), { properties: ['openFile', 'openDirectory'], ...o })
|
|
return res.canceled || !res.filePaths[0] ? null : res.filePaths[0]
|
|
})
|
|
ipcMain.handle('shell:openPath', (e, p) => shell.openPath(p))
|
|
ipcMain.handle('shell:showItemInFolder', (e, p) => {
|
|
shell.showItemInFolder(p)
|
|
return true
|
|
})
|
|
|
|
// game icons as a protocol so the renderer can display them safely ---------------------------
|
|
protocol.handle('plwl-icon', request => {
|
|
const id = decodeURIComponent(new URL(request.url).hostname)
|
|
let file = null
|
|
try {
|
|
const pick = fs
|
|
.readdirSync(iconDir(id))
|
|
.filter(f => new RegExp(`^icon\\.(${ICON_EXTS.join('|')})$`, 'i').test(f))
|
|
.sort()
|
|
.pop()
|
|
if (pick) file = path.join(iconDir(id), pick)
|
|
} catch {}
|
|
if (!file) return new Response('', { status: 404 })
|
|
return net.fetch('file://' + file)
|
|
})
|
|
|
|
return { games }
|
|
}
|
|
|
|
module.exports = { registerIpc, sendToWindow }
|