158 lines
4.7 KiB
JavaScript
158 lines
4.7 KiB
JavaScript
'use strict'
|
|
|
|
const path = require('path')
|
|
const fs = require('fs')
|
|
|
|
// Packaged: resources/app.asar two levels below the portable root.
|
|
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
|
|
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 translations are refreshed on start whenever their content changed,
|
|
// so updates reach existing installs; user-added languages are untouched.
|
|
function filesDiffer(a, b) {
|
|
try {
|
|
// compare as text: Buffer.equals() throws on a string
|
|
return fs.readFileSync(a, 'utf8') !== fs.readFileSync(b, 'utf8')
|
|
} 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)
|
|
}
|
|
}
|
|
|
|
// Anything under FILES/ is stored FILES-relative; absolute paths outside are
|
|
// kept 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) for tray, app icon, shortcut
|
|
// fallback. The module-relative copy is the only candidate in every layout.
|
|
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 icon as a real file: a .desktop Icon= cannot live 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 }
|