- make prefix imported games paths relative to prefix
- show placeholder icon as fallback
- fix crossed working/exe directories
- fix broken working dir in prefix imported games
This commit is contained in:
olcxja 2026-09-30 16:27:31 +02:00
commit 66f42f30cc
8 changed files with 74 additions and 10 deletions

View file

@ -5,7 +5,7 @@ const path = require('path')
const { DIRS, resolvePortablePath } = require('./paths')
const { newGame, newGameId, addGame } = require('./library')
const { extractIconFromExe } = require('./icons')
const { prefixPath } = require('./prefixes')
const { listPrefixes, prefixDriveRoot } = require('./prefixes')
// Directories that typically hold installers/redistributables, not the game.
const SKIPPED_DIRS = new Set(['redist', 'directx', '_commonredist', 'support', 'dotnet', 'vc'])
@ -68,12 +68,11 @@ async function importGame(srcDir, mode, onProgress) {
// prefix, so wine sees the app exactly as if it had been installed
// normally into C:\...
async function importGameFromPrefix(prefix, srcDir, onProgress) {
const prefixes = require('./prefixes')
if (!prefixes.listPrefixes().prefixes.some(p => p.name === prefix)) return { ok: false, error: 'prefix-not-found' }
if (!listPrefixes().prefixes.some(p => p.name === prefix)) return { ok: false, error: 'prefix-not-found' }
// the folder must live inside the prefix's C: drive so the C:\ mapping
// holds - drive_c sits at the prefix top for plain wine, under pfx/ for
// Proton-managed prefixes
const driveRoot = prefixes.prefixDriveRoot(prefix)
const driveRoot = prefixDriveRoot(prefix)
if (!driveRoot) return { ok: false, error: 'no-drive-c' }
const rel = path.relative(driveRoot, srcDir)
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return { ok: false, error: 'outside-drive-c' }

View file

@ -93,6 +93,8 @@ function registerIpc({ getWindow, onGamesChanged }) {
// 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)
@ -204,7 +206,15 @@ function registerIpc({ getWindow, onGamesChanged }) {
// dialogs & shell ---------------------------------------------------------------------------
ipcMain.handle('dialog:pickFile', async (e, opts) => {
const res = await dialog.showOpenDialog(getWindow(), { properties: ['openFile', 'openDirectory'], ...(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))

View file

@ -93,6 +93,14 @@ function updateGame(id, patch) {
const games = loadGames()
const g = games.find(x => x.id === id)
if (!g) return null
// prefix imports are pinned: their paths are stored relative to their
// prefix, so NOTHING may move them to another prefix or flip their
// import source (the patch fields are simply ignored)
if (g.importSource === 'prefix' && patch) {
patch = { ...patch }
delete patch.prefix
delete patch.importSource
}
// playSeconds goes to its own tiny file; the registry just mirrors it for
// anything reading apps.json directly (loadGames prefers the file anyway).
if (patch.playSeconds !== undefined) writePlaySeconds(id, patch.playSeconds)