code cleanup
This commit is contained in:
parent
78ef651ca7
commit
e8cd664965
8 changed files with 362 additions and 406 deletions
|
|
@ -826,14 +826,12 @@ function wireEvents() {
|
||||||
if (p) qs('#gc-workdir').value = p
|
if (p) qs('#gc-workdir').value = p
|
||||||
})
|
})
|
||||||
qs('#gc-tool-run').addEventListener('click', runCustomTool)
|
qs('#gc-tool-run').addEventListener('click', runCustomTool)
|
||||||
|
async function applyIconResult(res) {
|
||||||
|
toast(res.ok ? t('config.icon.set') : t('config.icon.noIcon'), res.ok ? 'success' : 'error')
|
||||||
|
if (res.ok) refreshConfigIcon()
|
||||||
|
}
|
||||||
qs('#gc-icon-auto').addEventListener('click', async () => {
|
qs('#gc-icon-auto').addEventListener('click', async () => {
|
||||||
const res = await window.plwl.gamesAutoIcon(state.currentGame.id)
|
applyIconResult(await window.plwl.gamesAutoIcon(state.currentGame.id))
|
||||||
if (res.ok) {
|
|
||||||
toast(t('config.icon.set'), 'success')
|
|
||||||
refreshConfigIcon()
|
|
||||||
} else {
|
|
||||||
toast(t('config.icon.noIcon'), 'error')
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
qs('#gc-icon-browse').addEventListener('click', async () => {
|
qs('#gc-icon-browse').addEventListener('click', async () => {
|
||||||
const p = await window.plwl.dialogPickFile({
|
const p = await window.plwl.dialogPickFile({
|
||||||
|
|
@ -842,17 +840,7 @@ function wireEvents() {
|
||||||
filters: [{ name: t('config.icon.pick'), extensions: ['png', 'jpg', 'jpeg', 'svg', 'ico', 'webp', 'gif', 'bmp', 'exe'] }]
|
filters: [{ name: t('config.icon.pick'), extensions: ['png', 'jpg', 'jpeg', 'svg', 'ico', 'webp', 'gif', 'bmp', 'exe'] }]
|
||||||
})
|
})
|
||||||
if (!p) return
|
if (!p) return
|
||||||
let res
|
applyIconResult(await window.plwl.gamesSetIconFile(state.currentGame.id, p))
|
||||||
if (p.toLowerCase().endsWith('.exe')) {
|
|
||||||
res = await window.plwl.gamesSetIconFile(state.currentGame.id, p)
|
|
||||||
if (res.ok) toast(t('config.icon.set'), 'success')
|
|
||||||
else toast(t('config.icon.noIcon'), 'error')
|
|
||||||
} else {
|
|
||||||
res = await window.plwl.gamesSetIconFile(state.currentGame.id, p)
|
|
||||||
if (res.ok) toast(t('config.icon.set'), 'success')
|
|
||||||
else toast(t('config.icon.noIcon'), 'error')
|
|
||||||
}
|
|
||||||
refreshConfigIcon()
|
|
||||||
})
|
})
|
||||||
qs('#gc-icon-reset').addEventListener('click', async () => {
|
qs('#gc-icon-reset').addEventListener('click', async () => {
|
||||||
await window.plwl.gamesResetIcon(state.currentGame.id)
|
await window.plwl.gamesResetIcon(state.currentGame.id)
|
||||||
|
|
|
||||||
15
src/fsutil.js
Normal file
15
src/fsutil.js
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
// Returns a non-existing path in `base` named `name` (or `name (2)`, `name (3)`...).
|
||||||
|
function dedupeDir(base, name) {
|
||||||
|
const candidate = path.join(base, name)
|
||||||
|
if (!fs.existsSync(candidate)) return candidate
|
||||||
|
let n = 2
|
||||||
|
while (fs.existsSync(path.join(base, `${name} (${n})`))) n++
|
||||||
|
return path.join(base, `${name} (${n})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { dedupeDir }
|
||||||
|
|
@ -2,24 +2,12 @@
|
||||||
|
|
||||||
const fsp = require('fs').promises
|
const fsp = require('fs').promises
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fs = require('fs')
|
|
||||||
const { DIRS } = require('./paths')
|
const { DIRS } = require('./paths')
|
||||||
const { newGame } = require('./library')
|
const { newGame } = require('./library')
|
||||||
|
const { dedupeDir } = require('./fsutil')
|
||||||
|
|
||||||
function walk(dir, base) {
|
// Directories that typically hold installers/redistributables, not the game.
|
||||||
return fsp.readdir(dir, { withFileTypes: true }).then(async entries => {
|
const SKIPPED_DIRS = new Set(['redist', 'directx', '_commonredist', 'support', 'dotnet', 'vc'])
|
||||||
let total = 0
|
|
||||||
for (const e of entries) {
|
|
||||||
const full = path.join(dir, e.name)
|
|
||||||
if (e.isDirectory()) total += await walk(full, base)
|
|
||||||
else if (e.isFile()) {
|
|
||||||
const st = await fsp.stat(full)
|
|
||||||
total += st.size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return total
|
|
||||||
}).catch(() => 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyRecursive(src, dst, onProgress) {
|
function copyRecursive(src, dst, onProgress) {
|
||||||
return fsp.readdir(src, { withFileTypes: true }).then(async entries => {
|
return fsp.readdir(src, { withFileTypes: true }).then(async entries => {
|
||||||
|
|
@ -38,41 +26,28 @@ function copyRecursive(src, dst, onProgress) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveFolder(src, dst, onProgress) {
|
function moveFolder(src, dst, onProgress) {
|
||||||
if (path.parse(src).root !== path.parse(dst).root) {
|
const fallback = () => copyRecursive(src, dst, onProgress).then(() => fsp.rm(src, { recursive: true, force: true }))
|
||||||
return copyRecursive(src, dst, onProgress).then(() => fsp.rm(src, { recursive: true, force: true }))
|
if (path.parse(src).root !== path.parse(dst).root) return fallback()
|
||||||
}
|
return fsp.rename(src, dst).catch(fallback)
|
||||||
return fsp.rename(src, dst).catch(() => copyRecursive(src, dst, onProgress).then(() => fsp.rm(src, { recursive: true, force: true })))
|
|
||||||
}
|
|
||||||
|
|
||||||
function dedupeName(base, name) {
|
|
||||||
let candidate = path.join(base, name)
|
|
||||||
let n = 2
|
|
||||||
while (fs.existsSync(candidate)) {
|
|
||||||
candidate = path.join(base, `${name} (${n})`)
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
return candidate
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importGame(srcDir, mode, onProgress) {
|
async function importGame(srcDir, mode, onProgress) {
|
||||||
const name = path.basename(srcDir)
|
const name = path.basename(srcDir)
|
||||||
let workdir = srcDir
|
let workdir = srcDir
|
||||||
if (mode === 'copy') {
|
if (mode === 'copy' || mode === 'move') {
|
||||||
const dst = dedupeName(DIRS.APPS, name)
|
const dst = dedupeDir(DIRS.APPS, name)
|
||||||
await copyRecursive(srcDir, dst, onProgress)
|
if (mode === 'copy') await copyRecursive(srcDir, dst, onProgress)
|
||||||
workdir = dst
|
else await moveFolder(srcDir, dst, onProgress)
|
||||||
} else if (mode === 'move') {
|
|
||||||
const dst = dedupeName(DIRS.APPS, name)
|
|
||||||
await moveFolder(srcDir, dst, onProgress)
|
|
||||||
workdir = dst
|
workdir = dst
|
||||||
}
|
}
|
||||||
const exePath = await findDefaultExe(workdir)
|
const exePath = await findDefaultExe(workdir)
|
||||||
const game = newGame({ name, exePath, workdir, mode })
|
return newGame({ name, exePath, workdir, mode })
|
||||||
return game
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Largest .exe in the tree, skipping redistributable folders.
|
||||||
async function findDefaultExe(dir) {
|
async function findDefaultExe(dir) {
|
||||||
let best = ''
|
let best = ''
|
||||||
|
let bestSize = -1
|
||||||
const walk = async d => {
|
const walk = async d => {
|
||||||
let entries = []
|
let entries = []
|
||||||
try {
|
try {
|
||||||
|
|
@ -83,12 +58,13 @@ async function findDefaultExe(dir) {
|
||||||
for (const e of entries) {
|
for (const e of entries) {
|
||||||
const full = path.join(d, e.name)
|
const full = path.join(d, e.name)
|
||||||
if (e.isDirectory()) {
|
if (e.isDirectory()) {
|
||||||
const lower = e.name.toLowerCase()
|
if (!SKIPPED_DIRS.has(e.name.toLowerCase())) await walk(full)
|
||||||
if (lower === 'redist' || lower === 'directx' || lower === '_commonredist' || lower === 'support' || lower === 'dotnet' || lower === 'vc') continue
|
|
||||||
await walk(full)
|
|
||||||
} else if (e.name.toLowerCase().endsWith('.exe')) {
|
} else if (e.name.toLowerCase().endsWith('.exe')) {
|
||||||
const st = await fsp.stat(full)
|
const size = await fsp.stat(full).then(st => st.size).catch(() => -1)
|
||||||
if (!best || st.size > (await fsp.stat(best)).size) best = full
|
if (size > bestSize) {
|
||||||
|
best = full
|
||||||
|
bestSize = size
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -96,4 +72,4 @@ async function findDefaultExe(dir) {
|
||||||
return best
|
return best
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { importGame, findDefaultExe, copyRecursive, moveFolder, walk }
|
module.exports = { importGame, findDefaultExe, copyRecursive, moveFolder }
|
||||||
|
|
|
||||||
104
src/ipc/games.js
Normal file
104
src/ipc/games.js
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const { runGame, runTool } = require('../runner')
|
||||||
|
const { importGame } = require('../importer')
|
||||||
|
const library = require('../library')
|
||||||
|
|
||||||
|
function killProcessGroup(pid) {
|
||||||
|
try {
|
||||||
|
process.kill(-pid, 'SIGKILL')
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
process.kill(pid, 'SIGKILL')
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendToWindow(win, channel, data) {
|
||||||
|
if (win && !win.isDestroyed()) win.webContents.send(channel, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Game-process state: id -> pid (or null while the process is starting).
|
||||||
|
function createGamesService({ getWindow }) {
|
||||||
|
const running = new Map()
|
||||||
|
const killRequests = new Set()
|
||||||
|
|
||||||
|
const isRunning = id => running.has(id)
|
||||||
|
const listRunning = () => [...running.keys()]
|
||||||
|
|
||||||
|
function resolveWine(game) {
|
||||||
|
const system = { id: 'system', type: 'system', label: 'System Wine' }
|
||||||
|
const wanted = game.wine === 'default' ? (library.loadSettings().defaultWine || 'system') : (game.wine || 'system')
|
||||||
|
if (!wanted || wanted === 'system') return system
|
||||||
|
return require('../wines').getWine(wanted) || system
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExit(id) {
|
||||||
|
running.delete(id)
|
||||||
|
sendToWindow(getWindow(), 'games:changed')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start(win, 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 = resolveWine(game)
|
||||||
|
} catch {
|
||||||
|
running.delete(id)
|
||||||
|
return { ok: false, error: 'wine-not-found' }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await runGame({ game, wine, systemWine: wine.type === 'system', onExit: () => handleExit(id) })
|
||||||
|
if (res.pid) running.set(id, res.pid)
|
||||||
|
// stop was requested while the process was still starting
|
||||||
|
if (killRequests.has(id)) {
|
||||||
|
killRequests.delete(id)
|
||||||
|
if (res.pid) killProcessGroup(res.pid)
|
||||||
|
}
|
||||||
|
sendToWindow(win, 'games:changed')
|
||||||
|
return { ok: true }
|
||||||
|
} catch (err) {
|
||||||
|
running.delete(id)
|
||||||
|
return { ok: false, error: String((err && err.message) || err) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function kill(id) {
|
||||||
|
const pid = running.get(id)
|
||||||
|
if (pid) {
|
||||||
|
killProcessGroup(pid)
|
||||||
|
running.delete(id)
|
||||||
|
sendToWindow(getWindow(), 'games:changed')
|
||||||
|
} else if (running.has(id)) {
|
||||||
|
killRequests.add(id)
|
||||||
|
}
|
||||||
|
return { ok: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runToolFor(id, tool, args) {
|
||||||
|
const game = library.getGame(id)
|
||||||
|
if (!game) return { ok: false, error: 'game-not-found' }
|
||||||
|
let wine
|
||||||
|
try {
|
||||||
|
wine = resolveWine(game)
|
||||||
|
} catch {
|
||||||
|
return { ok: false, error: 'wine-not-found' }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await runTool({ game, wine, tool, args, systemWine: wine.type === 'system' })
|
||||||
|
return { ok: true }
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, error: String((err && err.message) || err) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importTo(win, srcDir, mode) {
|
||||||
|
return importGame(srcDir, mode, data => sendToWindow(win, 'import:progress', data))
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isRunning, listRunning, start, kill, runToolFor, import: importTo }
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { createGamesService, killProcessGroup, sendToWindow }
|
||||||
200
src/ipc/index.js
Normal file
200
src/ipc/index.js
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
'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 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 }) {
|
||||||
|
const games = createGamesService({ getWindow })
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
ipcMain.handle('games:update', (e, id, patch) => library.updateGame(id, patch))
|
||||||
|
ipcMain.handle('games:remove', (e, id) => library.removeGame(id))
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// dialogs & shell ---------------------------------------------------------------------------
|
||||||
|
ipcMain.handle('dialog:pickFile', async (e, opts) => {
|
||||||
|
const res = await dialog.showOpenDialog(getWindow(), { properties: ['openFile', 'openDirectory'], ...(opts || {}) })
|
||||||
|
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 }
|
||||||
315
src/main.js
315
src/main.js
|
|
@ -1,326 +1,25 @@
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const { app, BrowserWindow, ipcMain, dialog, shell, protocol, net } = require('electron')
|
const { app } = require('electron')
|
||||||
const path = require('path')
|
const { ensureDirs } = require('./paths')
|
||||||
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 i18n = require('./i18n')
|
||||||
|
const wines = require('./wines')
|
||||||
|
const { registerIpc } = require('./ipc')
|
||||||
const { createMainWindow } = require('./window')
|
const { createMainWindow } = require('./window')
|
||||||
|
const { setAppIcon } = require('./appicon')
|
||||||
|
|
||||||
app.setName('PLWL')
|
app.setName('PLWL')
|
||||||
|
|
||||||
let win = null
|
let win = null
|
||||||
|
const getWindow = () => win
|
||||||
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, '.plwl-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.clearPlaytime(id))
|
|
||||||
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, '.plwl-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 file = require('./runner').latestLogPath(id)
|
|
||||||
try {
|
|
||||||
fs.accessSync(file)
|
|
||||||
return [file]
|
|
||||||
} catch {}
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
|
|
||||||
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 () => {
|
app.whenReady().then(async () => {
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
const { setAppIcon } = require('./appicon')
|
|
||||||
try {
|
try {
|
||||||
setAppIcon(app)
|
setAppIcon(app)
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
protocol.handle('plwl-icon', request => {
|
registerIpc({ getWindow })
|
||||||
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()
|
ensureDirs()
|
||||||
await i18n.init()
|
await i18n.init()
|
||||||
await wines.refreshWines()
|
await wines.refreshWines()
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ function linkPrefix(compatData, prefixDir) {
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEnv({ game, wine, workdir, systemWine }) {
|
function buildEnv({ game, wine }) {
|
||||||
const env = { ...process.env }
|
const env = { ...process.env }
|
||||||
const isProton = wine && wine.type === 'proton'
|
const isProton = wine && wine.type === 'proton'
|
||||||
let prefixDir = game.prefix ? prefixPath(game.prefix) : null
|
let prefixDir = game.prefix ? prefixPath(game.prefix) : null
|
||||||
|
|
@ -46,14 +46,13 @@ function buildEnv({ game, wine, workdir, systemWine }) {
|
||||||
env.SteamGameId = '0'
|
env.SteamGameId = '0'
|
||||||
env.WINEPREFIX = path.join(env.STEAM_COMPAT_DATA_PATH, 'pfx')
|
env.WINEPREFIX = path.join(env.STEAM_COMPAT_DATA_PATH, 'pfx')
|
||||||
env.PROTON_LOG_DIR = path.join(DIRS.DATA, 'logs')
|
env.PROTON_LOG_DIR = path.join(DIRS.DATA, 'logs')
|
||||||
fs.mkdirSync(path.join(env.PROTON_LOG_DIR), { recursive: true })
|
fs.mkdirSync(env.PROTON_LOG_DIR, { recursive: true })
|
||||||
linkPrefix(env.STEAM_COMPAT_DATA_PATH, prefixDir)
|
linkPrefix(env.STEAM_COMPAT_DATA_PATH, prefixDir)
|
||||||
if (!fs.existsSync(env.STEAM_COMPAT_DATA_PATH)) {
|
if (!fs.existsSync(env.STEAM_COMPAT_DATA_PATH)) {
|
||||||
fs.mkdirSync(env.STEAM_COMPAT_DATA_PATH, { recursive: true })
|
fs.mkdirSync(env.STEAM_COMPAT_DATA_PATH, { recursive: true })
|
||||||
}
|
}
|
||||||
} else if (!systemWine) {
|
|
||||||
env.WINEPREFIX = prefixDir
|
|
||||||
} else {
|
} else {
|
||||||
|
// plain wine (system or imported build)
|
||||||
env.WINEPREFIX = prefixDir
|
env.WINEPREFIX = prefixDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,15 +97,7 @@ function latestLogPath(gameId) {
|
||||||
function openLatestLog(gameId) {
|
function openLatestLog(gameId) {
|
||||||
const logFile = latestLogPath(gameId)
|
const logFile = latestLogPath(gameId)
|
||||||
fs.mkdirSync(path.dirname(logFile), { recursive: true })
|
fs.mkdirSync(path.dirname(logFile), { recursive: true })
|
||||||
// Keep only the latest run's log: truncate the file from the previous session
|
// 'w' truncates, so every session starts with a fresh log
|
||||||
// and clean up logs left by older versions (<id>-<timestamp>.log).
|
|
||||||
try {
|
|
||||||
for (const f of fs.readdirSync(path.dirname(logFile))) {
|
|
||||||
if (f.startsWith(`${gameId}-`) && f.endsWith('.log') && f !== `${gameId}-latest.log`) {
|
|
||||||
fs.rmSync(path.join(path.dirname(logFile), f), { force: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
return fs.createWriteStream(logFile, { flags: 'w' })
|
return fs.createWriteStream(logFile, { flags: 'w' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
29
src/wines.js
29
src/wines.js
|
|
@ -4,6 +4,7 @@ const fs = require('fs')
|
||||||
const fsp = fs.promises
|
const fsp = fs.promises
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const { DIRS, readJson, writeJson } = require('./paths')
|
const { DIRS, readJson, writeJson } = require('./paths')
|
||||||
|
const { dedupeDir } = require('./fsutil')
|
||||||
|
|
||||||
const WINES_DIR = DIRS.WINES
|
const WINES_DIR = DIRS.WINES
|
||||||
const REGISTRY = path.join(DIRS.DATA, 'wines.json')
|
const REGISTRY = path.join(DIRS.DATA, 'wines.json')
|
||||||
|
|
@ -119,13 +120,8 @@ async function refreshWines() {
|
||||||
}
|
}
|
||||||
const disabled = new Set(prev.filter(w => w.disabled).map(w => w.id))
|
const disabled = new Set(prev.filter(w => w.disabled).map(w => w.id))
|
||||||
for (const w of found) if (disabled.has(w.id)) w.disabled = true
|
for (const w of found) if (disabled.has(w.id)) w.disabled = true
|
||||||
const merged = [...found]
|
writeJson(REGISTRY, found)
|
||||||
for (const p of prev) {
|
return found
|
||||||
if (p.system) merged.push(p)
|
|
||||||
else if (!found.some(f => f.id === p.id) && exists(p.path)) merged.push(p)
|
|
||||||
}
|
|
||||||
writeJson(REGISTRY, merged)
|
|
||||||
return merged
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWines() {
|
function getWines() {
|
||||||
|
|
@ -169,16 +165,6 @@ function deleteWine(id) {
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
function dedupeDir(base, name) {
|
|
||||||
let candidate = path.join(base, name)
|
|
||||||
let n = 2
|
|
||||||
while (fs.existsSync(candidate)) {
|
|
||||||
candidate = path.join(base, `${name} (${n})`)
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
return candidate
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- symlink healing -------------------------------------------------------
|
// --- symlink healing -------------------------------------------------------
|
||||||
// Wine/Proton builds contain symlinks (e.g. inside the bundled default prefix)
|
// Wine/Proton builds contain symlinks (e.g. inside the bundled default prefix)
|
||||||
// that point to absolute paths from wherever the build lived before (zip
|
// that point to absolute paths from wherever the build lived before (zip
|
||||||
|
|
@ -261,16 +247,13 @@ async function importWineFolder(srcDir) {
|
||||||
const type = detectType(srcDir)
|
const type = detectType(srcDir)
|
||||||
let copySrc = srcDir
|
let copySrc = srcDir
|
||||||
if (!type) {
|
if (!type) {
|
||||||
|
// the user picked a folder one level above the actual build
|
||||||
let subs = []
|
let subs = []
|
||||||
try {
|
try {
|
||||||
subs = (await fsp.readdir(srcDir, { withFileTypes: true })).filter(e => e.isDirectory())
|
subs = (await fsp.readdir(srcDir, { withFileTypes: true })).filter(e => e.isDirectory())
|
||||||
} catch {}
|
} catch {}
|
||||||
for (const s of subs) {
|
const build = subs.find(s => detectType(path.join(srcDir, s.name)))
|
||||||
if (detectType(path.join(srcDir, s.name))) {
|
if (build) copySrc = path.join(srcDir, build.name)
|
||||||
copySrc = path.join(srcDir, s.name)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const dst = dedupeDir(WINES_DIR, path.basename(copySrc))
|
const dst = dedupeDir(WINES_DIR, path.basename(copySrc))
|
||||||
await fsp.cp(copySrc, dst, { recursive: true, verbatimSymlinks: true })
|
await fsp.cp(copySrc, dst, { recursive: true, verbatimSymlinks: true })
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue