diff --git a/files/translations/en-us.json b/files/translations/en-us.json index f3648ff..fae2169 100644 --- a/files/translations/en-us.json +++ b/files/translations/en-us.json @@ -45,7 +45,7 @@ "import.prefix.title": "Import from prefix", "import.prefix.hint": "Pick the wine prefix the app lives in. Nothing is copied, the app is launched from the prefix, so Windows paths keep working (C:\\...).", "import.pickPrefixFolder": "Choose the app folder inside the prefix", - "import.prefix.outside": "the selected folder is outside the prefix's C: drive (drive_c or pfx/drive_c)", + "import.prefix.outside": "the selected folder is outside the prefix's drive_c", "import.prefix.noDriveC": "this prefix has no drive_c yet, run any app in it so wine creates one", "import.pickFolder": "Choose the app folder", "import.progress": "Importing {0}…", @@ -59,7 +59,6 @@ "import.done": "Import finished", "import.failed": "Import failed: {0}", "import.noExe": "no .exe file found in the selected folder", - "import.prefixRoot": "this folder is a wine prefix (or its C: drive) - import an app from inside its drive_c instead", "config.title": "App configuration", "config.icon.auto": "Extract from exe", "config.icon.browse": "Choose image", diff --git a/files/translations/pl.json b/files/translations/pl.json index e00eae0..0decbf4 100644 --- a/files/translations/pl.json +++ b/files/translations/pl.json @@ -45,7 +45,7 @@ "import.prefix.title": "Import z prefiksu", "import.prefix.hint": "Wybierz prefiks wine, w którym jest aplikacja. Nic nie jest kopiowane, aplikacja odpalana jest z prefiksu, więc ścieżki windowsowe (C:\\...) dalej działają.", "import.pickPrefixFolder": "Wybierz folder aplikacji wewnątrz prefiksu", - "import.prefix.outside": "wybrany folder jest poza dyskiem C: prefiksu (drive_c lub pfx/drive_c)", + "import.prefix.outside": "wybrany folder jest poza drive_c prefiksu", "import.prefix.noDriveC": "ten prefiks nie ma jeszcze drive_c, uruchom w nim dowolną apkę, żeby wine go utworzył", "import.pickFolder": "Wybierz folder aplikacji", "import.progress": "Importowanie {0}…", @@ -59,7 +59,6 @@ "import.done": "Import zakończony", "import.failed": "Import nie powiódł się: {0}", "import.noExe": "nie znaleziono pliku .exe w wybranym folderze", - "import.prefixRoot": "ten folder to prefiks wine (lub jego dysk C:) - zaimportuj aplikację z wnętrza jego drive_c", "config.title": "Konfiguracja aplikacji", "config.icon.auto": "Wyciągnij z exe", "config.icon.browse": "Wybierz obrazek", diff --git a/package.json b/package.json index 77ac71b..67a274d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "PLWL", "productName": "PLWL", - "version": "0.3.1", + "version": "0.3.0", "description": "Portable Linux Wine Launcher", "main": "src/main.js", "scripts": { diff --git a/renderer/app.js b/renderer/app.js index 5e4888b..fc18481 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -783,10 +783,7 @@ async function doImport(srcDir, mode) { closeModal() if (!game || game.ok === false) { const raw = (game && game.error) || '' - const errText = - raw === 'no-exe-found' ? t('import.noExe') - : raw === 'prefix-root' ? t('import.prefixRoot') - : (raw || 'unknown error') + const errText = raw === 'no-exe-found' ? t('import.noExe') : (raw || 'unknown error') toast(t('import.failed', errText), 'error') return } @@ -926,8 +923,7 @@ async function doPrefixImport(prefix, srcDir) { const errText = raw === 'no-exe-found' ? t('import.noExe') : raw === 'no-drive-c' ? t('import.prefix.noDriveC') - : raw === 'outside-drive-c' ? t('import.prefix.outside') - : (raw || 'unknown error') + : t('import.prefix.outside') toast(t('import.failed', errText), 'error') return } diff --git a/src/importer.js b/src/importer.js index 78cba15..1fb015b 100644 --- a/src/importer.js +++ b/src/importer.js @@ -5,7 +5,7 @@ const path = require('path') const { DIRS, resolvePortablePath } = require('./paths') const { newGame, newGameId, addGame } = require('./library') const { extractIconFromExe } = require('./icons') -const { listPrefixes, prefixDriveRoots, isPrefixRoot } = require('./prefixes') +const { listPrefixes, prefixDriveRoot } = require('./prefixes') const { rewireSymlinks } = require('./symlinks') const { copyTree, treeSize } = require('./fsutil') @@ -22,9 +22,6 @@ 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 - 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). @@ -74,23 +71,17 @@ async function importGame(srcDir, mode, onProgress) { // normally into C:\... 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. - const driveRoots = prefixDriveRoots(prefix) - if (!driveRoots.length) return { ok: false, error: 'no-drive-c' } - const src = path.resolve(String(srcDir)) - const insideDrive = driveRoots.some(root => { - const rel = path.relative(root, src) - return rel && !rel.startsWith('..') && !path.isAbsolute(rel) - }) - if (!insideDrive) return { ok: false, error: 'outside-drive-c' } - const preExe = await findDefaultExe(src) + // 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 = 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' } + const preExe = await findDefaultExe(srcDir) if (!preExe) return { ok: false, error: 'no-exe-found' } const id = newGameId() - const game = newGame({ id, name: path.basename(src), exePath: preExe, workdir: src, mode: 'prefix' }) + const game = newGame({ id, name: path.basename(srcDir), exePath: preExe, workdir: srcDir, mode: 'prefix' }) game.importSource = 'prefix' game.prefix = prefix // exe picked automatically -> grab its icon right away; a missing icon is diff --git a/src/ipc/index.js b/src/ipc/index.js index 207d49f..03b23a4 100644 --- a/src/ipc/index.js +++ b/src/ipc/index.js @@ -260,14 +260,11 @@ function registerIpc({ getWindow, onGamesChanged }) { // shows up for wine as a regular C:\ install ipcMain.handle('games:pickPrefixFolder', async (e, prefix) => { // open the picker straight in the prefix's C: drive (plain wine: top - // level, Proton: under pfx/); a prefix used with BOTH kinds has two of - // them, so it opens at the prefix folder - the only spot that keeps - // drive_c and pfx/drive_c one click away - const driveRoots = prefixes.prefixDriveRoots(prefix) - const start = driveRoots.length === 1 ? driveRoots[0] : prefixes.prefixPath(prefix) + // level, Proton: under pfx/) + const driveC = prefixes.prefixDriveRoot(prefix) const res = await dialog.showOpenDialog(getWindow(), { title: i18n.translate(library.loadSettings().language, 'import.pickPrefixFolder'), - defaultPath: start, + defaultPath: driveC || prefixes.prefixPath(prefix), properties: ['openDirectory'] }) return res.canceled || !res.filePaths[0] ? null : res.filePaths[0] diff --git a/src/prefixes.js b/src/prefixes.js index 1a7b237..81befde 100644 --- a/src/prefixes.js +++ b/src/prefixes.js @@ -41,39 +41,22 @@ function prefixExists(name) { } } -// Every Windows C: drive a prefix has: plain wine keeps one at the top -// (WINEPREFIX=), Proton-managed prefixes nest one under pfx/ -// (WINEPREFIX=/pfx). A prefix used with BOTH wine kinds has both, -// and an import may come from either one - so callers that can accept -// several get them all, top level first. -function prefixDriveRoots(name) { +// Where the Windows C: drive of a prefix actually lives: plain wine keeps +// it at the top (WINEPREFIX=), Proton-managed prefixes nest it +// under pfx/ (WINEPREFIX=/pfx). system.reg marks the real prefix +// root; a fresh prefix may not have it yet, so fall back to drive_c itself. +function prefixDriveRoot(name) { const root = prefixPath(name) - const found = [] - for (const base of [root, path.join(root, 'pfx')]) { - try { - const driveC = path.join(base, 'drive_c') - if (fs.statSync(driveC).isDirectory()) found.push(driveC) - } catch {} + for (const pass of [true, false]) { + for (const base of [root, path.join(root, 'pfx')]) { + try { + const driveC = path.join(base, 'drive_c') + if (!fs.statSync(driveC).isDirectory()) continue + if (!pass || fs.existsSync(path.join(base, 'system.reg'))) return driveC + } catch {} + } } - return found -} - -// True when a path IS one of a prefix's own roots: the prefix folder, its -// pfx/ or either drive_c. A copy/move import of such a folder would move the -// prefix's C: drive (or the whole prefix) into files/apps, gutting it - the -// "import from a prefix" flow is the one that works in place. Both drive -// layouts are checked literally, not through prefixDriveRoots(): a prefix used -// with wine AND Proton has a drive_c each and BOTH are off limits. -function isPrefixRoot(p) { - if (!p) return false - const abs = path.resolve(String(p)) - if (path.resolve(DIRS.PREFIXES) === abs) return true - for (const entry of listPrefixes().prefixes) { - const root = prefixPath(entry.name) - if (abs === root || abs === path.join(root, 'pfx')) return true - if (abs === path.join(root, 'drive_c') || abs === path.join(root, 'pfx', 'drive_c')) return true - } - return false + return null } function createPrefix(name) { @@ -153,4 +136,4 @@ function setDefaultPrefix(name) { return { ok: true } } -module.exports = { listPrefixes, savePrefixes, createPrefix, deletePrefix, renamePrefix, setDefaultPrefix, prefixPath, prefixExists, prefixDriveRoots, isPrefixRoot, healPrefix, MAIN } +module.exports = { listPrefixes, savePrefixes, createPrefix, deletePrefix, renamePrefix, setDefaultPrefix, prefixPath, prefixExists, prefixDriveRoot, healPrefix, MAIN } diff --git a/src/runner.js b/src/runner.js index 635febc..c27a827 100644 --- a/src/runner.js +++ b/src/runner.js @@ -95,13 +95,8 @@ function buildEnv({ game, wine }) { env.PROTON_LOG_DIR = path.join(DIRS.DATA, 'logs') fs.mkdirSync(env.PROTON_LOG_DIR, { recursive: true }) linkPrefix(env.STEAM_COMPAT_DATA_PATH, prefixDir) - // No silent fallback to a real directory: Proton would then set its prefix - // up under files/data/compat//pfx, outside files/wineprefixes/ where - // PLWL tracks it - rename, delete, the in-use guards and the LAN sync all - // look at the launcher prefix, so its C: drive would be orphaned. Only a - // missing path is refused; a populated one is linkPrefix's deliberate case. if (!fs.existsSync(env.STEAM_COMPAT_DATA_PATH)) { - throw new Error('prefix-link-failed') + fs.mkdirSync(env.STEAM_COMPAT_DATA_PATH, { recursive: true }) } } else { // plain wine (system or imported build)