diff --git a/README.md b/README.md index 0a88a00..005b7fe 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,6 @@ cd PLWL 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. -## "Open with PLWL" in the file manager - -The desktop entry (`MimeType` + `Exec %f`) makes PLWL show up in Dolphin/Nautilus under **Open With** for Windows executables (`.exe`, `.msi`, `.bat`, `.cmd`, `.lnk`). Opening a file that way shows a small popup in the launcher where you pick the **Wine/Proton version** and the **prefix** for this run — both default to the launcher's defaults, nothing is imported into the library. A second launcher instance hands the path to the running one and exits (same single-instance mechanism the shortcuts use). - ## Wine & Proton support - **System Wine** — uses the `wine` binary from PATH. @@ -56,5 +52,3 @@ The desktop entry (`MimeType` + `Exec %f`) makes PLWL show up in Dolphin/Nautilu ## Translations All UI text comes from JSON files in `files/translations/`. The `data` object carries the language names; `texts` maps keys to strings, with `{0}`, `{1}` ... placeholders filled at runtime. - -The translations bundled with a release are launcher-owned: on every start they are refreshed into `files/translations/` whenever their content changed, so launcher updates deliver new/changed keys to existing installs. Extra language files the bundle does not ship (user-added) are never touched. diff --git a/files/translations/en-us.json b/files/translations/en-us.json index 8c455f7..c06afd5 100644 --- a/files/translations/en-us.json +++ b/files/translations/en-us.json @@ -144,16 +144,6 @@ "settings.saved": "Settings saved", "settings.openFiles": "Open files folder", "settings.closeToTray": "Close window to tray (keep tracking playtime in background)", - "openwith.title": "Open with Wine", - "openwith.file": "File", - "openwith.fileMissing": "This file no longer exists (or is not a Windows executable).", - "openwith.wine": "Wine / Proton version", - "openwith.wineDefault": "Default ({0})", - "openwith.prefix": "Wine prefix", - "openwith.prefixDefault": "Default prefix ({0})", - "openwith.run": "Run", - "openwith.startFailed": "Could not start {0}: {1}", - "openwith.notWindows": "This is not a Windows executable (exe/msi/bat/cmd/lnk).", "dialog.ok": "OK", "dialog.cancel": "Cancel", "dialog.close": "Close", diff --git a/files/translations/pl.json b/files/translations/pl.json index 7536f51..7c8568e 100644 --- a/files/translations/pl.json +++ b/files/translations/pl.json @@ -144,16 +144,6 @@ "settings.saved": "Ustawienia zapisane", "settings.openFiles": "Otwórz folder files", "settings.closeToTray": "Zamykaj okno do zasobnika systemowego (licz czas gry w tle)", - "openwith.title": "Otwórz przez Wine", - "openwith.file": "Plik", - "openwith.fileMissing": "Ten plik już nie istnieje (albo nie jest plikiem wykonywalnym Windows).", - "openwith.wine": "Wersja Wine / Proton", - "openwith.wineDefault": "Domyślna ({0})", - "openwith.prefix": "Prefiks Wine", - "openwith.prefixDefault": "Prefiks domyślny ({0})", - "openwith.run": "Uruchom", - "openwith.startFailed": "Nie udało się uruchomić {0}: {1}", - "openwith.notWindows": "To nie jest plik wykonywalny Windows (exe/msi/bat/cmd/lnk).", "dialog.ok": "OK", "dialog.cancel": "Anuluj", "dialog.close": "Zamknij", diff --git a/package.json b/package.json index e071dd5..7a04c09 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,13 @@ { "name": "PLWL", "productName": "PLWL", - "version": "0.1.2", + "version": "0.1.1", "description": "Portable Linux Wine Launcher", "main": "src/main.js", "scripts": { - "start": "electron ." + "start": "electron .", + "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 009e501..f9ccae2 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -597,114 +597,6 @@ async function doImport(srcDir, mode) { } } -// --- "Open with PLWL" (file manager -> Open With -> PLWL) ----------------------- - -const openwithTexts = {} - -function ot(key, ...args) { - let text = openwithTexts[key] ?? t(key) - args.forEach((v, i) => { - text = String(text).split(`{${i}}`).join(String(v)) - }) - return text -} - -async function loadOpenwithTexts() { - try { - Object.assign(openwithTexts, await window.plwl.openwithTexts(state.lang)) - } catch {} -} - -function isWindowsExecutable(p) { - return /\.(exe|msi|bat|cmd|lnk)$/i.test(p || '') -} - -// The popup: wine version + prefix selects defaulting to the LAUNCHER's -// defaults (settings.defaultWine / prefixes.default), like the per-app -// config's "Default (...)" options - just without importing anything. -async function showOpenWithModal(openPath) { - // always fresh: wines/prefixes/default can change while the launcher sits - // in the tray - try { - const [wines, prefixes, settings] = await Promise.all([ - window.plwl.winesList(), - window.plwl.prefixesList(), - window.plwl.settingsGet() - ]) - state.wines = wines - state.prefixes = prefixes - state.settings = settings - } catch {} - const modal = showModal(` -

${ot('openwith.title')}

-

${ot('openwith.file')}:

- - - - - - - `) - modal.querySelector('#ow-file').textContent = openPath - modal.querySelector('#ow-file').title = openPath - const missing = !window.plwl.fileExists(openPath) || !isWindowsExecutable(openPath) - modal.querySelector('#ow-missing').style.display = missing ? '' : 'none' - const defWine = (state.settings && state.settings.defaultWine) || 'system' - const defPrefix = state.prefixes.default || '' - const wineOpts = [ - { value: 'default', label: ot('openwith.wineDefault', wineLabel(defWine)) }, - ...state.wines.map(w => ({ value: w.id, label: w.label })) - ] - const prefixOpts = [ - { value: '', label: ot('openwith.prefixDefault', defPrefix || 'Main') }, - ...state.prefixes.prefixes.filter(p => p.name !== defPrefix).map(p => ({ value: p.name, label: p.name })) - ] - new CustomSelect(modal.querySelector('#ow-wine'), wineOpts, 'default') - new CustomSelect(modal.querySelector('#ow-prefix'), prefixOpts, '') - modal.querySelector('#ow-cancel').onclick = closeModal - modal.querySelector('#ow-run').onclick = async () => { - if (missing) { - closeModal() - return - } - const runBtn = modal.querySelector('#ow-run') - runBtn.disabled = true - const res = await window.plwl.openwithRun({ - openPath, - wine: modal.querySelector('#ow-wine').dataset.value || 'default', - prefix: modal.querySelector('#ow-prefix').dataset.value || '' - }) - closeModal() - if (res.ok) { - toast(t('toast.started', window.plwl.basename(openPath)), 'success') - } else { - const raw = res.error === 'file-missing' ? ot('openwith.fileMissing') : (res.error === 'not-windows' ? ot('openwith.notWindows') : (res.error || '')) - toast(ot('openwith.startFailed', window.plwl.basename(openPath), raw), 'error') - } - } -} - -// Entry point: the main process hands over a file opened through PLWL -// (boot argv or a second-instance handoff). -async function handleOpenWith(openPath) { - if (!openPath) return - await loadOpenwithTexts() - showOpenWithModal(openPath) -} - -function wireOpenWith() { - // Both delivery paths (live handoff and the boot queue) converge on - // openwithTake(): the main-process slot is drained exactly when a popup - // shows, so a reloaded window can never resurrect an old popup and two - // quick handoffs collapse into one (the newest file wins). - window.plwl.onOpenWith(() => window.plwl.openwithTake().then(handleOpenWith)) - // file queued before the window existed (boot handoff): pick it up once - window.plwl.openwithTake().then(handleOpenWith) -} - // Import from a prefix, step 2: register the app in place - nothing is // copied, paths stay prefix-relative. async function doPrefixImport(prefix, srcDir) { @@ -1183,7 +1075,6 @@ async function boot() { applyI18n() wireEvents() wireRealtime() - wireOpenWith() qs('#loading').remove() qs('#main').style.display = 'flex' showView('library') diff --git a/renderer/style.css b/renderer/style.css index 2af013a..ab71bc6 100644 --- a/renderer/style.css +++ b/renderer/style.css @@ -627,36 +627,6 @@ select { .modal .mode-buttons { display: flex; flex-direction: column; gap: 0.6rem; } .modal .mode-buttons .button { justify-content: flex-start; } -/* --- "Open with PLWL" picker ---------------------------------------------- */ -.modal .openwith-file-row { - word-break: break-all; - margin-top: -0.3rem; -} -.modal .openwith-file-row code { - font-family: inherit; - color: var(--text-color); - background-color: rgba(255, 255, 255, 0.06); - border-radius: 0.4rem; - padding: 0.1rem 0.4rem; -} -.button:disabled { - opacity: 0.45; - pointer-events: none; -} - -/* --- form fields inside modals (shared by every modal with inputs) --------- */ -/* .modal is a flex column with gap: 0.8rem. That gap ADDS to the field - margins reused from the config forms (.field-label margin-top + control - margin), so label->control spacing in modals ended up ~3x bigger than in - .form-card (where block-layout margins collapse). Inside modals the gap - owns the group spacing, so the repeated field styles collapse to the same - rhythm as the config forms: label hugs its control (~0.4rem), groups stay - 0.8rem apart. */ -.modal .field-label { margin-top: 0; margin-bottom: -0.4rem; } -.modal .custom-select, -.modal input, -.modal textarea { margin: 0; } - .toasts { position: fixed; bottom: 1rem; diff --git a/src/argv.js b/src/argv.js deleted file mode 100644 index cf93cc0..0000000 --- a/src/argv.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict' - -// Shared command-line parsing for the launcher process itself. Both boot -// (process.argv) and the single-instance handoff (second-instance argv) use -// it, so a shortcut, a "Open with PLWL" file-open and Chromium's own injected -// flags are handled the same way everywhere. -// -// Supported forms: -// --launch | --launch= auto-start that library app -// --open | --open= open an .exe with the wine picker -// a bare existing-file path same as --open (plumbing for Exec %f) - -function firstNonFlagAfter(args, i) { - // Chromium injects its own flags into the second instance's argv and they - // can land BETWEEN the option and its value (e.g. `--launch - // --allow-file-access-from-files 1790414564627`), so skip anything that - // looks like a flag and take the first non-flag token. - for (let j = i + 1; j < args.length; j++) { - if (!args[j].startsWith('--')) return args[j] - } - return null -} - -function findValue(args, names) { - for (const name of names) { - const i = args.indexOf(name) - if (i !== -1) return firstNonFlagAfter(args, i) - const eq = args.find(a => a.startsWith(name + '=')) - if (eq) return eq.slice(name.length + 1) - } - return null -} - -// PLWL's own options must not swallow Chromium's flags (--flag=... style), -// so anything after a flag that itself carries a value is skipped here by -// looking only at the KNOWN option names above. -function parseArgv(argv) { - const args = Array.isArray(argv) ? argv : process.argv - const launchId = findValue(args, ['--launch']) - let openPath = findValue(args, ['--open']) - if (!openPath && !launchId) { - // "Open with" plumbing (Exec %f): the file path arrives as the only - // non-flag argument - never pick a bare token when a real flag carries - // the intent, and skip this process's own binary (electron dev mode - // passes it as the first argument). - const bare = args.find( - a => - !a.startsWith('-') && - /\.(exe|msi|bat|cmd|lnk)$/i.test(a) && - !/(electron|olcxja\.plwl)(\.exe)?$/i.test(a) - ) - if (bare) openPath = bare - } - return { launchId, openPath } -} - -module.exports = { parseArgv, findValue } diff --git a/src/ipc/games.js b/src/ipc/games.js index 5c89a12..1679fa6 100644 --- a/src/ipc/games.js +++ b/src/ipc/games.js @@ -31,14 +31,6 @@ function createGamesService({ getWindow, onGamesChanged }) { const isRunning = id => running.has(id) const listRunning = () => [...running.keys()] - // "Open with PLWL" handoff: queue the file for the renderer (picker popup - // with wine version + prefix). Safe to call at any time - if the window is - // not up yet, the renderer picks the file up right after boot. - function openExternally(openPath) { - require('../openwith').setPending(openPath) - sendToWindow(getWindow(), 'openwith:file', openPath) - } - function resolveWine(game) { const system = { id: 'system', type: 'system', label: 'System Wine' } const wanted = game.wine === 'default' ? (library.loadSettings().defaultWine || 'system') : (game.wine || 'system') @@ -128,7 +120,7 @@ function createGamesService({ getWindow, onGamesChanged }) { return importGameFromPrefix(prefix, srcDir) } - return { isRunning, listRunning, start, kill, runToolFor, import: importTo, importFromPrefix, setOnGameExit, openExternally } + return { isRunning, listRunning, start, kill, runToolFor, import: importTo, importFromPrefix, setOnGameExit } } module.exports = { createGamesService, killProcessGroup, sendToWindow } diff --git a/src/ipc/index.js b/src/ipc/index.js index ab720c8..19f8211 100644 --- a/src/ipc/index.js +++ b/src/ipc/index.js @@ -12,7 +12,6 @@ const runner = require('../runner') const { extractIconFromExe } = require('../icons') const { createGamesService, sendToWindow } = require('./games') const shortcuts = require('../shortcuts') -const openwith = require('../openwith') const ICON_EXTS = ['png', 'jpe?g', 'svg', 'ico', 'webp', 'gif', 'bmp'] @@ -111,7 +110,6 @@ function registerIpc({ getWindow, onGamesChanged }) { } }) ipcMain.handle('games:shortcutPaths', (e, id) => shortcuts.shortcutPaths(id)) - ipcMain.handle('openwith:texts', (e, lang) => openwith.textsFor(lang)) ipcMain.handle('games:clearPlaytime', (e, id) => library.clearPlaytime(id)) // games: icons --------------------------------------------------------------------- @@ -158,29 +156,6 @@ function registerIpc({ getWindow, onGamesChanged }) { return extractGameIcon(id, game.exePath) }) - // "Open with PLWL" ------------------------------------------------------------------ - // The renderer asks for the file handed in via `--open` (Exec %f), then - // launches it with the wine/prefix picked in the popup. Nothing is imported - // into the library. - ipcMain.handle('openwith:take', async () => { - const p = openwith.getPending() - if (!p) return null - openwith.clearPending() - return p - }) - ipcMain.handle('openwith:run', async (e, payload) => { - try { - return await openwith.run({ - openPath: payload && payload.openPath, - wine: payload && payload.wine, - prefix: payload && payload.prefix, - lang: library.loadSettings().language - }) - } catch (err) { - return errorResult(err) - } - }) - // games: run -------------------------------------------------------------------------- ipcMain.handle('games:run', (e, id) => games.start(getWindow(), id)) ipcMain.handle('games:kill', (e, id) => games.kill(id)) diff --git a/src/main.js b/src/main.js index 15f2697..f66d261 100644 --- a/src/main.js +++ b/src/main.js @@ -7,8 +7,6 @@ const wines = require('./wines') const { registerIpc } = require('./ipc') const { createMainWindow } = require('./window') const { setAppIcon } = require('./appicon') -const { parseArgv } = require('./argv') -const openwith = require('./openwith') const path = require('path') const fs = require('fs') @@ -25,7 +23,6 @@ let tray = null let quitting = false let headless = false // started via --launch: no window until the user asks let autoLaunchId = null // game id from --launch -let autoOpenPath = null // file path from --open ("Open with PLWL") let autoQuitWhenDone = false // headless boot: quit after that game exits const getWindow = () => win @@ -53,10 +50,9 @@ if (!gotLock) { } else { app.on('second-instance', (e, argv) => { trace(`second-instance argv=${argv.join(' ')}`) - const { launchId, openPath } = parseArgv(argv) + const id = parseLaunchId(argv) lastHandoffAt = Date.now() - if (openPath) openWith(openPath) - else if (launchId) launchGame(launchId) + if (id) launchGame(id) else showMainWindow() }) } @@ -66,20 +62,24 @@ if (!gotLock) { // another game" can never kill the launcher mid-handoff let lastHandoffAt = 0 -// --- "Open with PLWL" (Exec %f from the file manager) -------------------------- -// Hands the file to the window (renderer shows the wine/prefix picker popup); -// headless boots open the window for it - a file cannot be launched without -// the user choosing how. -async function openWith(filePath) { - trace(`openWith ${filePath} serviceReady=${!!gamesService}`) - showMainWindow() - if (gamesService) gamesService.openExternally(filePath) - else openwith.setPending(filePath) +function parseLaunchId(argv) { + const args = Array.isArray(argv) ? argv : process.argv + const i = args.indexOf('--launch') + if (i !== -1) { + // Chromium injects its own flags into the second instance's argv and they + // can land BETWEEN --launch and the value (e.g. `--launch + // --allow-file-access-from-files 1790414564627`), so skip anything that + // looks like a flag and take the first non-flag token. + for (let j = i + 1; j < args.length; j++) { + if (!args[j].startsWith('--')) return args[j] + } + return null + } + const eq = args.find(a => a.startsWith('--launch=')) + return eq ? eq.slice('--launch='.length) : null } -const auto = parseArgv(process.argv) -autoLaunchId = auto.launchId -autoOpenPath = auto.openPath +autoLaunchId = parseLaunchId(process.argv) headless = !!autoLaunchId autoQuitWhenDone = headless // booting just to run one game -> exit when it ends @@ -241,11 +241,6 @@ app.whenReady().then(async () => { } else { win = createMainWindow() wireWindow(win) - // file open handed over while the service was still booting (or queued - // by a second instance racing the first boot) - if (autoOpenPath || openwith.hasPending()) { - openWith(autoOpenPath || openwith.getPending()) - } } }) diff --git a/src/openwith.js b/src/openwith.js deleted file mode 100644 index 57ec493..0000000 --- a/src/openwith.js +++ /dev/null @@ -1,108 +0,0 @@ -'use strict' - -// "Open with PLWL" (file manager -> Open With -> PLWL): a file handed in via -// `--open ` (or the Exec %f plumbing) is parked here until the renderer -// picks it up, then launched ad hoc with the wine version + prefix chosen in -// the picker popup. Nothing is imported into the library. - -const fs = require('fs') -const path = require('path') - -let pending = null // file path waiting for the picker (service not ready yet) - -function setPending(p) { - pending = p -} - -function getPending() { - return pending -} - -function hasPending() { - return !!pending -} - -function clearPending() { - pending = null -} - -// The launcher's own translations may not cover openwith texts in older -// installs - fall back to English keys so the UI never shows raw key names. -function textsFor(code) { - let texts = {} - try { - texts = require('./i18n').getLang(code).texts || {} - } catch {} - const fallback = { - 'openwith.title': 'Open with Wine', - 'openwith.file': 'File', - 'openwith.fileMissing': 'This file no longer exists.', - 'openwith.wine': 'Wine / Proton version', - 'openwith.wineDefault': 'Default ({0})', - 'openwith.prefix': 'Wine prefix', - 'openwith.prefixDefault': 'Default prefix ({0})', - 'openwith.run': 'Run', - 'openwith.startFailed': 'Could not start {0}: {1}', - 'openwith.notWindows': 'This is not a Windows executable (exe/msi/bat/cmd/lnk).' - } - return { ...fallback, ...texts } -} - -function resolveWine(id) { - const system = { id: 'system', type: 'system', label: 'System Wine' } - const wanted = id === 'default' ? require('./library').loadSettings().defaultWine || 'system' : id - if (!wanted || wanted === 'system') return system - return require('./wines').getWine(wanted) || system -} - -function resolvePrefix(name) { - const defs = require('./prefixes').listPrefixes() - const known = name && defs.prefixes.some(p => p.name === name) ? name : '' - return known || defs.default || 'Main' -} - -// Ad-hoc run target: shaped exactly like a library game (runner.buildEnv and -// the playtime/log paths expect those fields) but nothing is persisted. -function makeTarget(openPath, wineId, prefixName) { - const dir = path.dirname(openPath) - return { - id: 'openwith', - name: path.basename(openPath), - mode: 'open', - path: dir, - exePath: openPath, - workdir: dir, - wine: wineId || 'default', - prefix: prefixName || '', - args: '', - dllOverrides: [], - env: {}, - iconSource: '', - playSeconds: 0, - created: 0, - lastPlayed: 0 - } -} - -// Launch without importing: resolves the chosen wine/prefix, builds the ad -// hoc target and runs it exactly like a library app (same env, same logging, -// same detached process group). -async function run({ openPath, wine: wineId, prefix: prefixName0, lang, onStarted }) { - if (!openPath || !fs.existsSync(openPath)) return { ok: false, error: 'file-missing' } - if (!/\.(exe|msi|bat|cmd|lnk)$/i.test(openPath)) return { ok: false, error: 'not-windows' } - const wine = resolveWine(wineId) - const prefixName = resolvePrefix(prefixName0) - const runner = require('./runner') - try { - // runAdHoc builds the synthetic target internally (shared `openwith` id, - // so logs/playtime never collide with a library app id); the resolved - // prefix is passed through so buildEnv uses the chosen one - const res = await runner.runAdHoc({ openPath, wine, systemWine: wine.type === 'system', prefix: prefixName }) - if (onStarted) onStarted({ name: path.basename(openPath), pid: res.pid }) - return { ok: true, pid: res.pid || null, logFile: res.logFile || null } - } catch (err) { - return { ok: false, error: String((err && err.message) || err) } - } -} - -module.exports = { setPending, getPending, hasPending, clearPending, run, makeTarget, resolveWine, resolvePrefix, textsFor } diff --git a/src/paths.js b/src/paths.js index b244087..71310c1 100644 --- a/src/paths.js +++ b/src/paths.js @@ -50,51 +50,17 @@ const DIRS = { 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) + try { + for (const f of fs.readdirSync(DIRS.BUNDLED_TRANSLATIONS)) { + const dst = path.join(DIRS.TRANSLATIONS, f) + if (!fs.existsSync(dst)) fs.copyFileSync(path.join(DIRS.BUNDLED_TRANSLATIONS, f), dst) + } + } catch {} } } @@ -129,4 +95,4 @@ function writeJson(file, data) { fs.renameSync(tmp, file) } -module.exports = { RESOURCES, APP_ROOT, BUILD, FILES, DATA, DIRS, ensureDirs, syncBundledTranslations, readJson, writeJson, toPortablePath, resolvePortablePath } +module.exports = { RESOURCES, APP_ROOT, BUILD, FILES, DATA, DIRS, ensureDirs, readJson, writeJson, toPortablePath, resolvePortablePath } diff --git a/src/preload.js b/src/preload.js index 0472793..fbecbb2 100644 --- a/src/preload.js +++ b/src/preload.js @@ -10,15 +10,6 @@ ipcRenderer.on('import:progress', (e, data) => { if (importProgressCb) importProgressCb(data) }) -// "Open with PLWL": single-slot callback for files handed over via --open -// (Exec %f) - the renderer shows the wine/prefix picker popup for it. -let openWithCb = null -ipcRenderer.on('openwith:file', (e, openPath) => { - if (openWithCb) openWithCb(openPath) -}) -const fs = require('fs') -const path = require('path') - contextBridge.exposeInMainWorld('plwl', { FILES_DIR: DIRS.FILES, i18nList: () => ipcRenderer.invoke('i18n:list'), @@ -35,20 +26,6 @@ contextBridge.exposeInMainWorld('plwl', { prefixesCreate: name => ipcRenderer.invoke('prefixes:create', name), prefixesDelete: name => ipcRenderer.invoke('prefixes:delete', name), prefixesSetDefault: name => ipcRenderer.invoke('prefixes:setDefault', name), - openwithTake: () => ipcRenderer.invoke('openwith:take'), - openwithRun: payload => ipcRenderer.invoke('openwith:run', payload), - openwithTexts: lang => ipcRenderer.invoke('openwith:texts', lang), - onOpenWith: cb => { - openWithCb = cb - }, - basename: p => (p ? path.basename(p) : ''), - fileExists: p => { - try { - return !!p && fs.existsSync(p) - } catch { - return false - } - }, gamesList: () => ipcRenderer.invoke('games:list'), gamesGet: id => ipcRenderer.invoke('games:get', id), gamesUpdate: (id, patch) => ipcRenderer.invoke('games:update', id, patch), diff --git a/src/runner.js b/src/runner.js index 2dee951..24cf499 100644 --- a/src/runner.js +++ b/src/runner.js @@ -87,11 +87,9 @@ const GRACE_MS = 60000 // (a number of seconds) every 30s - apps.json is left untouched to spare the // disk. lastPlayed is stamped once at start, final totals are mirrored into // the registry once on exit. -// `base` is the seconds total this run adds up to. It is resolved by the -// CALLER from the live playtime file (not the registry) so ad-hoc runs that -// share one id (see runAdHoc) never mix two apps' playtime. -function startPlaytimeTicker({ game, startedAt, base }) { +function startPlaytimeTicker({ game, startedAt }) { const library = require('./library') + const base = game.playSeconds || 0 const timer = setInterval(() => { const counted = Math.max(0, Math.round((Date.now() - startedAt - GRACE_MS) / 1000)) library.writePlaySeconds(game.id, base + counted) @@ -147,10 +145,7 @@ function runGame({ game, wine, systemWine, onExit }) { child.on('error', reject) child.unref() const startedAt = Date.now() - // read the LIVE playtime file, not the (possibly shared/stale) registry - // value - ad-hoc "open with" runs share one id and must not mix playtime - const basePlaySeconds = require('./library').readPlaySeconds(game.id) ?? (game.playSeconds || 0) - const stopTicker = startPlaytimeTicker({ game, startedAt, base: basePlaySeconds }) + const stopTicker = startPlaytimeTicker({ game, startedAt }) // stamp "last played" once at start so it is correct even if we crash later require('./library').updateGame(game.id, { lastPlayed: startedAt }) child.on('exit', (code, signal) => { @@ -159,7 +154,7 @@ function runGame({ game, wine, systemWine, onExit }) { const counted = Math.max(0, Math.round((Date.now() - startedAt - GRACE_MS) / 1000)) require('./library').updateGame(game.id, { lastPlayed: startedAt, - playSeconds: basePlaySeconds + counted + playSeconds: (game.playSeconds || 0) + counted }) if (onExit) onExit(code, signal) }) @@ -194,14 +189,4 @@ function runTool({ game, wine, tool, args, systemWine }) { }) } -// Ad-hoc launch ("Open with PLWL"): the exact runGame pipeline, but with a -// synthetic target under the shared `openwith` id - log and playtime files -// live in files/data/appdata/openwith/ and no library entry is created. -// `prefix` carries the picker's choice ('' = launcher default, resolved by -// buildEnv); wine/systemWine are the already-resolved build objects. -function runAdHoc({ openPath, wine, systemWine, prefix, onExit }) { - const game = require('./openwith').makeTarget(openPath, 'default', prefix || '') - return runGame({ game, wine, systemWine, onExit }) -} - -module.exports = { buildEnv, runGame, runTool, runAdHoc, parseArgs, latestLogPath } +module.exports = { buildEnv, runGame, runTool, parseArgs, latestLogPath } diff --git a/tools/createshortcut.sh b/tools/createshortcut.sh index 54a0d29..5eac756 100644 --- a/tools/createshortcut.sh +++ b/tools/createshortcut.sh @@ -24,20 +24,16 @@ if [ -f "$ICON_SRC" ]; then fi echo "2. Creating shortcut..." -# MimeType + Exec %f: "Open with ..." in Dolphin/Nautilus lists PLWL for -# Windows executables and hands the file path over as an argument (the -# launcher then shows the wine/prefix picker). mkdir -p "$APP_DIR" cat > "$APP_DIR/olcxja.plwl.desktop" << EOF [Desktop Entry] Type=Application Name=PLWL Comment=Portable Linux Wine Launcher -Exec="$EXE" %f +Exec="$EXE" Icon=olcxja.plwl Terminal=false Categories=Game; -MimeType=application/x-ms-dos-executable;application/x-msi;application/x-msdownload; EOF chmod +x "$APP_DIR/olcxja.plwl.desktop" command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$APP_DIR" 2>/dev/null || true diff --git a/tools/install.sh b/tools/install.sh index 6d04b9b..8dab12f 100644 --- a/tools/install.sh +++ b/tools/install.sh @@ -40,19 +40,15 @@ if [ -f "$ICON_SRC" ]; then fi echo "3. Creating shortcut..." -# MimeType + Exec %f: "Open with ..." in Dolphin/Nautilus lists PLWL for -# Windows executables and hands the file path over as an argument (the -# launcher then shows the wine/prefix picker). cat > "$APP_DIR/olcxja.plwl.desktop" << EOF [Desktop Entry] Type=Application Name=PLWL Comment=Portable Linux Wine Launcher -Exec="$EXE" %f +Exec="$EXE" Icon=olcxja.plwl Terminal=false Categories=Game; -MimeType=application/x-ms-dos-executable;application/x-msi;application/x-msdownload; EOF chmod +x "$APP_DIR/olcxja.plwl.desktop"