cleanup comments, update translations, bump to 0.4.1

This commit is contained in:
olcxja 2026-10-05 15:42:44 +02:00
commit 7d8ab87eee
28 changed files with 412 additions and 1114 deletions

View file

@ -13,9 +13,8 @@ const { copyTree, treeSize } = require('./fsutil')
// Directories that typically hold installers/redistributables, not the game.
const SKIPPED_DIRS = new Set(['redist', 'directx', '_commonredist', 'support', 'dotnet', 'vc'])
// The copy itself lives in fsutil (copyTree): symlinks recreated, permissions
// carried over, byte-level progress. moveFolder only falls back to it across
// filesystems; a same-device rename reports nothing (nothing is copied).
// The copy itself lives in fsutil.copyTree; moveFolder falls back to it
// across filesystems only.
function moveFolder(src, dst, onProgress) {
const fallback = () => copyTree(src, dst, onProgress).then(() => fsp.rm(src, { recursive: true, force: true }))
if (path.parse(src).root !== path.parse(dst).root) return fallback()
@ -23,16 +22,13 @@ function moveFolder(src, dst, onProgress) {
}
async function importGame(srcDir, mode, onProgress) {
// a prefix (or its drive_c / pfx) is not an app folder: copy/move would take
// the prefix's C: drive with it and leave the prefix gutted
// a prefix (or its drive_c/pfx) is not an app folder
if (isPrefixRoot(srcDir)) return { ok: false, error: 'prefix-root' }
const name = path.basename(srcDir)
// Validate before touching the user's files: refuse an import without any
// .exe (and don't half-move a folder we'd then have to roll back).
// validate before touching anything: no .exe -> refuse
const preExe = await findDefaultExe(srcDir)
if (!preExe) return { ok: false, error: 'no-exe-found' }
// The id doubles as the imported folder name, so generate it first and
// create files/apps/<id> - no dedupe suffixes, ids can never collide.
// the id doubles as the imported folder name
const id = newGameId()
let workdir = srcDir
if (mode === 'copy' || mode === 'move') {
@ -47,9 +43,7 @@ async function importGame(srcDir, mode, onProgress) {
await copyTree(srcDir, dst, p => emit('copy', { files: p.done, bytes: p.bytes, totalFiles: total.files, totalBytes: total.bytes }))
} else await moveFolder(srcDir, dst, onProgress)
workdir = dst
// anything the game linked to INSIDE the launcher data (its own folder, an
// icon, a shared prefix file) becomes a relative link, so the imported app
// survives the launcher being moved - the same healing wine builds get
// in-data link targets become relative, so the import survives a move
try {
await rewireSymlinks(dst, [DIRS.FILES])
} catch {}
@ -60,8 +54,7 @@ async function importGame(srcDir, mode, onProgress) {
}
const game = newGame({ id, name, exePath, workdir, mode })
game.imported = mode === 'copy' || mode === 'move'
// the exe was picked automatically -> grab its icon right away; a missing
// icon is not an import failure (game can still get one from config)
// auto-picked exe -> grab its icon; a missing icon is not a failure
const icon = path.join(DIRS.DATA, 'icons', id, 'icon.ico')
if (extractIconFromExe(resolvePortablePath(exePath), icon).ok) {
game.iconSource = icon
@ -69,22 +62,14 @@ async function importGame(srcDir, mode, onProgress) {
return addGame(game)
}
// Import from a wine prefix: the folder is picked under the prefix's
// drive_c and left in place - the game paths are stored relative to the
// prefix, so wine sees the app exactly as if it had been installed
// normally into C:\...
// Import from a prefix: the folder stays in place, paths stored prefix-relative.
async function importGameFromPrefix(prefix, srcDir, onProgress) {
if (!listPrefixes().prefixes.some(p => p.name === prefix)) return { ok: false, error: 'prefix-not-found' }
// The folder must live inside one of the prefix's C: drives so the C:\
// mapping holds - drive_c sits at the prefix top for plain wine, under pfx/
// for Proton-managed prefixes, and a prefix used with BOTH kinds has both.
// Every existing drive_c counts; the stored path keeps its drive_c segment
// either way, so it resolves in the layout the app will run in.
// the folder must live inside one of the prefix's C: drives
const driveRoots = prefixDriveRoots(prefix)
if (!driveRoots.length) return { ok: false, error: 'no-drive-c' }
const src = path.resolve(String(srcDir))
// realpath on both sides: a unified prefix reaches drive_c through the pfx
// symlink too, and that path is the same folder (see ensureUnifiedPrefix)
// realpath both sides: a unified prefix reaches drive_c through pfx too
let realSrc
try {
realSrc = fs.realpathSync(src)
@ -108,8 +93,7 @@ async function importGameFromPrefix(prefix, srcDir, onProgress) {
const game = newGame({ id, name: path.basename(src), exePath: preExe, workdir: src, mode: 'prefix' })
game.importSource = 'prefix'
game.prefix = prefix
// exe picked automatically -> grab its icon right away; a missing icon is
// not an import failure (game can still get one from config)
// auto-picked exe -> grab its icon; a missing icon is not a failure
const icon = path.join(DIRS.DATA, 'icons', id, 'icon.ico')
if (extractIconFromExe(resolvePortablePath(preExe), icon).ok) {
game.iconSource = icon
@ -117,9 +101,7 @@ async function importGameFromPrefix(prefix, srcDir, onProgress) {
return addGame(game)
}
// Largest .exe in the tree, skipping redistributable folders. Files are
// stat'ed in parallel and folders walked breadth-first so big games don't
// turn the picker into a sequential crawl.
// Largest .exe in the tree, skipping redistributable folders.
async function findDefaultExe(dir) {
let best = ''
let bestSize = -1