From b83e530392f7979e28ada0a2980eb8cfd0d58a21 Mon Sep 17 00:00:00 2001 From: olcxja Date: Wed, 30 Sep 2026 15:13:21 +0200 Subject: [PATCH] add importing from prefix --- README.md | 4 + files/translations/en-us.json | 9 ++ files/translations/pl.json | 9 ++ package.json | 4 +- renderer/app.js | 154 +++++++++++++++++++++++++++++++--- src/importer.js | 32 ++++++- src/ipc/games.js | 10 ++- src/ipc/index.js | 21 +++++ src/library.js | 33 ++++++-- src/prefixes.js | 20 ++++- src/preload.js | 13 ++- 11 files changed, 287 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index fb864fb..005b7fe 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,10 @@ cd PLWL ./olcxja.plwl/olcxja.plwl ``` +## Importing from a wine prefix + +Besides copy/move/leave, an app can be imported **from a wine prefix**: pick the prefix, then the app folder inside its `drive_c`. Nothing is copied — paths are stored relative to the prefix, so wine sees the app as if it were normally installed on `C:\` (registry entries pointing to `C:\...` keep working). Both layouts work: plain wine prefixes (`drive_c` at the top) and Proton-managed ones (`drive_c` under `pfx/`). The app stays tied to that prefix; removing the launcher entry never deletes files from the prefix. + ## Wine & Proton support - **System Wine** — uses the `wine` binary from PATH. diff --git a/files/translations/en-us.json b/files/translations/en-us.json index 7895a87..b317109 100644 --- a/files/translations/en-us.json +++ b/files/translations/en-us.json @@ -28,14 +28,22 @@ "library.never": "Never", "library.portable": "Portable", "library.external": "External", + "library.inPrefix": "In prefix", "library.removeConfirm": "Remove {0} from the launcher?{1}", "library.removeFilesNote": "\nIts imported files in files/apps will also be deleted.", + "library.removePrefixNote": "\nFiles stay in the prefix - only the launcher entry is removed.", "library.tools": "Wine tools", "library.viewLogs": "View logs", "import.question": "Should the app files be brought into the launcher so it stays portable, or left where they are?", "import.mode.copy": "Copy into launcher (portable)", "import.mode.move": "Move into launcher (portable)", "import.mode.leave": "Leave in place", + "import.mode.prefix": "Import from a wine prefix", + "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 (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 once so wine creates it", "import.pickFolder": "Choose the app folder", "import.progress": "Importing {0}…", "import.fileDone": "{0} files copied", @@ -61,6 +69,7 @@ "config.wineDefault": "Default ({0})", "config.prefix": "Wine prefix", "config.prefixDefault": "Default prefix ({0})", + "config.prefixLocked": "This app was imported from a prefix - its prefix cannot be changed", "config.dllOverrides": "DLL overrides", "config.overrides.add": "Add override", "config.overrides.dll": "DLL name", diff --git a/files/translations/pl.json b/files/translations/pl.json index 3c36138..e740f9e 100644 --- a/files/translations/pl.json +++ b/files/translations/pl.json @@ -28,14 +28,22 @@ "library.never": "Nigdy", "library.portable": "Przenośna", "library.external": "Zewnętrzna", + "library.inPrefix": "W prefiksie", "library.removeConfirm": "Usunąć {0} z launchera?{1}", "library.removeFilesNote": "\nJej zaimportowane pliki w files/apps również zostaną usunięte.", + "library.removePrefixNote": "\nPliki zostają w prefiksie - usuwany jest tylko wpis w launcherze.", "library.tools": "Narzędzia Wine", "library.viewLogs": "Podejrzyj logi", "import.question": "Czy pliki aplikacji mają trafić do launchera, aby był przenośny, czy mają zostać tam, gdzie są?", "import.mode.copy": "Skopiuj do launchera (przenośne)", "import.mode.move": "Przenieś do launchera (przenośne)", "import.mode.leave": "Zostaw w miejscu", + "import.mode.prefix": "Importuj z prefiksu wine", + "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 (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ę raz, żeby wine je utworzył", "import.pickFolder": "Wybierz folder aplikacji", "import.progress": "Importowanie {0}…", "import.fileDone": "Skopiowane pliki: {0}", @@ -61,6 +69,7 @@ "config.wineDefault": "Domyślna ({0})", "config.prefix": "Prefiks Wine", "config.prefixDefault": "Prefiks domyślny ({0})", + "config.prefixLocked": "Ta aplikacja została zaimportowana z prefiksu - nie można zmienić jej prefiksu", "config.dllOverrides": "Nadpisania DLL", "config.overrides.add": "Dodaj nadpisanie", "config.overrides.dll": "Nazwa DLL", diff --git a/package.json b/package.json index 8148eaa..7a04c09 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "main": "src/main.js", "scripts": { "start": "electron .", - "test": "node test/import.test.js && node test/icons.test.js && node test/shortcuts.test.js && node test/wines-portable.test.js", - "smoke": "node test/import.test.js && node test/icons.test.js && node test/shortcuts.test.js && node test/wines-portable.test.js" + "test": "node test/import.test.js && node test/prefix-import.test.js && node test/icons.test.js && node test/shortcuts.test.js && node test/wines-portable.test.js", + "smoke": "node test/import.test.js && node test/prefix-import.test.js && node test/icons.test.js && node test/shortcuts.test.js && node test/wines-portable.test.js" }, "devDependencies": { "@electron/asar": "^3.4.1", diff --git a/renderer/app.js b/renderer/app.js index f08d8cc..e357551 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -104,8 +104,20 @@ function toast(message, kind) { }) } +// The teardown of a closing modal is delayed (for the fade-out) - an +// eagerly armed timer must never kill a modal opened in the meantime (e.g. +// import mode -> prefix picker happens within milliseconds), so showModal +// cancels any pending teardown and clears the closing state. +let modalTeardownTimer = null + function showModal(html) { + if (modalTeardownTimer) { + clearTimeout(modalTeardownTimer) + modalTeardownTimer = null + } const overlay = qs('#modal-overlay') + const modal = qs('#modal') + modal.classList.remove('closing') qs('#modal').innerHTML = html overlay.style.display = 'flex' requestAnimationFrame(() => requestAnimationFrame(() => overlay.classList.add('show'))) @@ -118,7 +130,9 @@ function closeModal() { if (overlay.style.display === 'none') return modal.classList.add('closing') overlay.classList.remove('show') - setTimeout(() => { + if (modalTeardownTimer) clearTimeout(modalTeardownTimer) + modalTeardownTimer = setTimeout(() => { + modalTeardownTimer = null overlay.style.display = 'none' modal.classList.remove('closing') modal.innerHTML = '' @@ -310,6 +324,29 @@ function absPathInput(v) { return v } +// Prefix-imported apps: show paths the way wine sees them - relative to the +// prefix ("pfx/drive_c/..." or "drive_c/...") instead of the long +// files/wineprefixes//... form. +function displayGamePath(p, game) { + if (game && game.importSource === 'prefix' && game.prefix) { + const files = window.plwl.FILES_DIR + const base = files ? files + '/wineprefixes/' + game.prefix + '/' : '' + if (base && p && p.startsWith(base)) return p.slice(base.length) + } + return displayPath(p) +} + +// Inverse of displayGamePath: accept a prefix-relative (drive_c/... or +// pfx/drive_c/...) path and resolve it under the game's prefix; anything +// else falls through to the normal FILES/absolute handling. +function absGamePathInput(v, game) { + if (game && game.importSource === 'prefix' && game.prefix && /^(pfx\/)?drive_c([\/]|$)/i.test(v || '')) { + const files = window.plwl.FILES_DIR + if (files) return files + '/wineprefixes/' + game.prefix + '/' + v + } + return absPathInput(v) +} + function fmtPlaytime(seconds) { const s = Math.round(seconds || 0) if (s < 60) return t('library.playtimeSeconds', s) @@ -341,11 +378,15 @@ function renderLibrary() { card.className = 'game-card' card.style.animationDelay = `${idx * 0.04}s` const portable = game.mode === 'copy' || game.mode === 'move' + const inPrefix = game.importSource === 'prefix' + const sourceBadge = inPrefix + ? `${t('library.inPrefix')}${game.prefix ? ' · ' + game.prefix : ''}` + : `${portable ? t('library.portable') : t('library.external')}` const last = game.lastPlayed ? new Date(game.lastPlayed).toLocaleDateString() : t('library.never') card.innerHTML = `

- ${portable ? t('library.portable') : t('library.external')} + ${sourceBadge} ${wineLabel(game.wine)}
${t('library.lastPlayed', last)}
@@ -382,7 +423,9 @@ function renderLibrary() { else toast(t('library.viewLogs')) } card.querySelector('[data-act=remove]').onclick = async () => { - const note = game.mode === 'copy' || game.mode === 'move' ? t('library.removeFilesNote') : '' + const note = game.mode === 'copy' || game.mode === 'move' + ? t('library.removeFilesNote') + : (inPrefix ? t('library.removePrefixNote') : '') const ok = await confirmModal(t('library.removeConfirm', game.name, note), t('library.remove')) if (!ok) return await window.plwl.gamesRemove(game.id) @@ -446,6 +489,7 @@ async function startImportFlow() { +