'use strict' const path = require('path') const fs = require('fs') // Packaged builds run from /olcxja.plwl/resources/app.asar: // the portable layout (/files) is two levels above the resources dir // and files bundled with the release are unpacked next to the asar. function computeRoots() { const m = __dirname.match(/^(.*)[/\\]app\.asar([/\\]|$)/) if (m) { const RESOURCES = path.resolve(m[1]) const APP_ROOT = path.resolve(RESOURCES, '..', '..') return { RESOURCES, APP_ROOT, // FILES = BUILD/files, so BUILD is the portable root itself. BUILD: process.env.PLWL_BUILD_DIR || APP_ROOT, // Bundled release files live inside the asar (Electron's patched fs // can read them directly from the archive). BUNDLED: fs.existsSync(path.join(RESOURCES, 'app.asar', 'files', 'translations')) ? path.join(RESOURCES, 'app.asar', 'files') : path.join(APP_ROOT, 'files') } } const APP_ROOT = path.resolve(__dirname, '..') return { RESOURCES: APP_ROOT, APP_ROOT, BUILD: process.env.PLWL_BUILD_DIR || path.join(APP_ROOT, 'build'), BUNDLED: path.join(APP_ROOT, 'files') } } const { RESOURCES, APP_ROOT, BUILD, BUNDLED } = computeRoots() const FILES = path.join(BUILD, 'files') const DATA = path.join(FILES, 'data') const DIRS = { BUILD, APP_ROOT, FILES, DATA, WINES: path.join(FILES, 'wines'), PREFIXES: path.join(FILES, 'wineprefixes'), APPS: path.join(FILES, 'apps'), TRANSLATIONS: path.join(FILES, 'translations'), BUNDLED_TRANSLATIONS: path.join(BUNDLED, 'translations'), FAKE_STEAM: path.join(DATA, 'fake-steam') } // Bundled translation files are launcher-owned: on every start they are // refreshed in the portable files/translations folder WHENEVER THEIR CONTENT // CHANGED, so an update shipping new or changed keys reaches existing // installs too (the old copy-only-if-missing logic froze translations at the // version that happened to run first). Extra files in the target folder that // the bundle does not ship (user-added languages) are left untouched. function filesDiffer(a, b) { try { return !fs.readFileSync(a).equals(fs.readFileSync(b)) } catch { return true // target missing/unreadable -> needs a (re)copy } } function syncBundledTranslations(bundledDir, targetDir) { let names = [] try { names = fs.readdirSync(bundledDir) } catch { return 0 } let updated = 0 for (const name of names) { const src = path.join(bundledDir, name) const dst = path.join(targetDir, name) try { if (!fs.statSync(src).isFile()) continue } catch { continue } if (!filesDiffer(src, dst)) continue try { fs.copyFileSync(src, dst) updated++ } catch {} } return updated } function ensureDirs() { for (const dir of [FILES, DATA, DIRS.WINES, DIRS.PREFIXES, DIRS.APPS, DIRS.TRANSLATIONS, DIRS.FAKE_STEAM]) { fs.mkdirSync(dir, { recursive: true }) } if (DIRS.TRANSLATIONS !== DIRS.BUNDLED_TRANSLATIONS) { syncBundledTranslations(DIRS.BUNDLED_TRANSLATIONS, DIRS.TRANSLATIONS) } } // Portable paths: anything stored under FILES/ goes into the registry as a // path RELATIVE to FILES (e.g. apps//game.exe), so moving the whole // launcher folder keeps the library working. Absolute paths outside FILES // (games left in place) are stored as-is. Resolve on read, normalize on write. function toPortablePath(p) { if (typeof p !== 'string' || !p || !path.isAbsolute(p)) return p const rel = path.relative(FILES, p) if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return p return rel } function resolvePortablePath(p) { if (typeof p !== 'string' || !p || path.isAbsolute(p)) return p return path.join(FILES, p) } // The launcher's own icon (renderer/plwl.png), used by the tray, the app // icon and as the .desktop shortcut fallback. // // The module-relative copy comes FIRST: it is the only candidate that exists // in every layout, because a packaged build keeps renderer/ inside // app.asar and Electron's patched fs reads straight out of the archive (a // release only ships olcxja.plwl.png next to the portable root, so looking at // APP_ROOT alone made the tray icon vanish in installed builds). Cached: it // never moves at runtime. function launcherIconCandidates() { const bundled = path.join(__dirname, '..', 'renderer') return [ path.join(bundled, 'plwl.png'), path.join(bundled, 'plwl.svg'), path.join(APP_ROOT, 'renderer', 'plwl.png'), path.join(APP_ROOT, 'renderer', 'plwl.svg'), path.join(APP_ROOT, 'olcxja.plwl.png') ] } function firstExisting(candidates) { for (const candidate of candidates) { try { if (fs.existsSync(candidate)) return candidate } catch {} } return null } let launcherIcon = null function launcherIconPath() { if (launcherIcon === null) launcherIcon = firstExisting(launcherIconCandidates()) || '' return launcherIcon || null } // The same icon, but only as a real file on disk: a .desktop entry's Icon= is // resolved by the desktop environment, which cannot look inside app.asar. function launcherIconDiskPath() { const onDisk = launcherIconCandidates().filter(candidate => !candidate.includes(`.asar${path.sep}`)) return firstExisting(onDisk) } function readJson(file, fallback) { try { return JSON.parse(fs.readFileSync(file, 'utf8')) } catch { return fallback } } function writeJson(file, data) { fs.mkdirSync(path.dirname(file), { recursive: true }) const tmp = file + '.tmp' fs.writeFileSync(tmp, JSON.stringify(data, null, 2)) fs.renameSync(tmp, file) } module.exports = { RESOURCES, APP_ROOT, BUILD, FILES, DATA, DIRS, ensureDirs, syncBundledTranslations, launcherIconPath, launcherIconDiskPath, readJson, writeJson, toPortablePath, resolvePortablePath }