Compare commits
No commits in common. "944d8f86c2438b540edd4f2963e1e43aea2f1879" and "9265558161e8547d9af1727fd915d60831b943d6" have entirely different histories.
944d8f86c2
...
9265558161
13 changed files with 48 additions and 217 deletions
|
|
@ -58,8 +58,3 @@ The desktop entry (`MimeType` + `Exec %f`) makes PLWL show up in Dolphin/Nautilu
|
||||||
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.
|
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.
|
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.
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
Codebase overview, conventions and rules for contributors: [development.md](development.md).
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
# Development
|
|
||||||
|
|
||||||
Short guide for anyone working on the PLWL codebase.
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
src/ main process (Node/Electron backend)
|
|
||||||
main.js entry point: tray, single-instance handoff, --launch / --open
|
|
||||||
argv.js command-line parsing shared by boot and second-instance handoff
|
|
||||||
window.js main window creation
|
|
||||||
ipc/ all ipcMain handlers
|
|
||||||
index.js handler registration, icon protocol, dialogs
|
|
||||||
games.js games service: run/stop state, wine resolution, open-with handoff
|
|
||||||
runner.js spawns wine/proton (env, prefix, playtime, logs)
|
|
||||||
library.js apps registry (apps.json) + settings + playtime files
|
|
||||||
wines.js wine/proton build scan, registry, portable symlink healing
|
|
||||||
prefixes.js prefix registry (files/wineprefixes)
|
|
||||||
importer.js app import (copy/move/leave/prefix)
|
|
||||||
openwith.js "Open with PLWL": ad-hoc runs, no library entry
|
|
||||||
shortcuts.js .desktop shortcut generation
|
|
||||||
paths.js portable layout, FILES-relative path helpers, translation sync
|
|
||||||
i18n.js translation loading + {0} placeholder filling
|
|
||||||
preload.js contextBridge API exposed to the renderer
|
|
||||||
appicon.js window/taskbar icon
|
|
||||||
renderer/ UI (plain HTML/CSS/JS, no framework)
|
|
||||||
app.js views, modals, custom-select, all renderer logic
|
|
||||||
index.html static markup for every view
|
|
||||||
style.css the whole look; CSS variables at the top
|
|
||||||
files/ portable data the launcher ships/creates at runtime
|
|
||||||
translations/ en-us.json, pl.json (copied to the data dir on start)
|
|
||||||
test/ plain node scripts, run them with `node test/<name>.test.js`
|
|
||||||
tools/ install.sh / createshortcut.sh / build helpers
|
|
||||||
```
|
|
||||||
|
|
||||||
Main process vs renderer: the renderer never touches Node/Electron directly -
|
|
||||||
everything goes through the `window.plwl` bridge (preload.js) and ipcMain
|
|
||||||
handlers in src/ipc/. Keep new APIs in that pattern.
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
- Keep it simple. Plain functions over classes, no abstractions "for later".
|
|
||||||
- Comments explain the WHY (quirks, workarounds, races), not the WHAT. If the
|
|
||||||
code says it, don't repeat it in words. No comments at all when nothing
|
|
||||||
needs explaining.
|
|
||||||
- No duplicate logic: the second time you copy something, move it into a
|
|
||||||
helper (a shared module in src/ or a function next to its users) and call
|
|
||||||
it from both places.
|
|
||||||
- Everything user-visible goes through the translation files (en-us.json,
|
|
||||||
pl.json) with `{0}`, `{1}` placeholders. No hardcoded strings in app.js.
|
|
||||||
- Paths stored on disk are FILES-relative (portable layout): write with
|
|
||||||
toPortablePath(), read with resolvePortablePath() - never store absolute
|
|
||||||
paths for anything inside files/.
|
|
||||||
- Registries (apps.json, wines.json, prefixes.json) are rewritten whole via
|
|
||||||
writeJson(); playtime lives in its own tiny file so the registry is not
|
|
||||||
rewritten while a game runs.
|
|
||||||
- Deleting user data needs a confirm modal; best-effort cleanup is
|
|
||||||
try/catch-and-ignore with a comment saying why it is safe to ignore.
|
|
||||||
- Windows apps run through runGame()/buildEnv() - never spawn wine directly
|
|
||||||
somewhere else (prefix handling, Proton env and playtime live there).
|
|
||||||
- CSS: reuse the existing variables/classes (.button, .field-label,
|
|
||||||
.custom-select, form-card); spacing rhythm comes from --button-margin and
|
|
||||||
the flex gap of the container, not per-element magic numbers.
|
|
||||||
- Tests are plain assert-by-print scripts (see test/): set PLWL_BUILD_DIR to
|
|
||||||
a temp dir, check(), exit 1 on failure. Add one when fixing a bug.
|
|
||||||
|
|
||||||
## Verify before you commit
|
|
||||||
|
|
||||||
```
|
|
||||||
node --check <changed js files> # syntax
|
|
||||||
node test/import.test.js # ...and the other test files
|
|
||||||
npm start # click through the flow you touched
|
|
||||||
```
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"nav.settings": "Settings",
|
"nav.settings": "Settings",
|
||||||
"library.title": "Apps Library",
|
"library.title": "Apps Library",
|
||||||
"library.empty": "No apps yet",
|
"library.empty": "No apps yet",
|
||||||
"library.emptyHint": "Import an app to get started. You have a lot import options to choose from!",
|
"library.emptyHint": "Import an app to get started. The launcher can copy it into files/apps to stay fully portable, or just remember where it lives.",
|
||||||
"library.import": "Import app",
|
"library.import": "Import app",
|
||||||
"library.play": "Play",
|
"library.play": "Play",
|
||||||
"library.stop": "Stop",
|
"library.stop": "Stop",
|
||||||
|
|
@ -20,9 +20,9 @@
|
||||||
"library.config": "Configure",
|
"library.config": "Configure",
|
||||||
"library.remove": "Remove",
|
"library.remove": "Remove",
|
||||||
"library.openFolder": "Open folder",
|
"library.openFolder": "Open folder",
|
||||||
"library.lastPlayed": "Last played: {0}",
|
"library.lastPlayed": "Last played {0}",
|
||||||
"library.playtime": "Playtime: {0}",
|
"library.playtime": "Playtime: {0}",
|
||||||
"library.playtimeSeconds": "{0} sec",
|
"library.playtimeSeconds": "{0}s",
|
||||||
"library.playtimeMin": "{0} min",
|
"library.playtimeMin": "{0} min",
|
||||||
"library.playtimeHM": "{0}h {1} min",
|
"library.playtimeHM": "{0}h {1} min",
|
||||||
"library.never": "Never",
|
"library.never": "Never",
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
"library.inPrefix": "In prefix",
|
"library.inPrefix": "In prefix",
|
||||||
"library.removeConfirm": "Remove {0} from the launcher?{1}",
|
"library.removeConfirm": "Remove {0} from the launcher?{1}",
|
||||||
"library.removeFilesNote": "\nIts imported files in files/apps will also be deleted.",
|
"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.removePrefixNote": "\nFiles stay in the prefix - only the launcher entry is removed.",
|
||||||
"library.tools": "Wine tools",
|
"library.tools": "Wine tools",
|
||||||
"library.viewLogs": "View logs",
|
"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.question": "Should the app files be brought into the launcher so it stays portable, or left where they are?",
|
||||||
|
|
@ -40,10 +40,10 @@
|
||||||
"import.mode.leave": "Leave in place",
|
"import.mode.leave": "Leave in place",
|
||||||
"import.mode.prefix": "Import from a wine prefix",
|
"import.mode.prefix": "Import from a wine prefix",
|
||||||
"import.prefix.title": "Import from 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.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.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.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.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.pickFolder": "Choose the app folder",
|
||||||
"import.progress": "Importing {0}…",
|
"import.progress": "Importing {0}…",
|
||||||
"import.fileDone": "{0} files copied",
|
"import.fileDone": "{0} files copied",
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
"config.wineDefault": "Default ({0})",
|
"config.wineDefault": "Default ({0})",
|
||||||
"config.prefix": "Wine prefix",
|
"config.prefix": "Wine prefix",
|
||||||
"config.prefixDefault": "Default prefix ({0})",
|
"config.prefixDefault": "Default prefix ({0})",
|
||||||
"config.prefixLocked": "This app was imported from a prefix, its prefix cannot be changed",
|
"config.prefixLocked": "This app was imported from a prefix - its prefix cannot be changed",
|
||||||
"config.prefixLockedHint": "Pinned to the prefix it was imported from (its paths are stored relative to it)",
|
"config.prefixLockedHint": "Pinned to the prefix it was imported from (its paths are stored relative to it)",
|
||||||
"config.dllOverrides": "DLL overrides",
|
"config.dllOverrides": "DLL overrides",
|
||||||
"config.overrides.add": "Add override",
|
"config.overrides.add": "Add override",
|
||||||
|
|
@ -99,7 +99,7 @@
|
||||||
"tools.customPlaceholder": "e.g. winetricks vcrun2019",
|
"tools.customPlaceholder": "e.g. winetricks vcrun2019",
|
||||||
"tools.run": "Run",
|
"tools.run": "Run",
|
||||||
"prefixes.title": "Wine prefixes",
|
"prefixes.title": "Wine prefixes",
|
||||||
"prefixes.subtitle": "Wine 'containers' that live in files/wineprefixes. Each one is like separate wine install.",
|
"prefixes.subtitle": "Prefixes live in files/wineprefixes and are prepared by wine on first use.",
|
||||||
"prefixes.create": "Create prefix",
|
"prefixes.create": "Create prefix",
|
||||||
"prefixes.namePlaceholder": "Prefix name",
|
"prefixes.namePlaceholder": "Prefix name",
|
||||||
"prefixes.default": "Default",
|
"prefixes.default": "Default",
|
||||||
|
|
@ -114,7 +114,7 @@
|
||||||
"prefixes.invalidName": "Invalid prefix name",
|
"prefixes.invalidName": "Invalid prefix name",
|
||||||
"prefixes.mainLocked": "The Main prefix cannot be deleted",
|
"prefixes.mainLocked": "The Main prefix cannot be deleted",
|
||||||
"wines.title": "Wine versions",
|
"wines.title": "Wine versions",
|
||||||
"wines.subtitle": "You can import your wine/proton builds here. Proton runs fully without Steam.",
|
"wines.subtitle": "Drop Wine or Proton builds into files/wines, then rescan. Proton runs fully without Steam.",
|
||||||
"wines.system": "System Wine",
|
"wines.system": "System Wine",
|
||||||
"wines.systemHint": "Uses the wine binary installed on your system",
|
"wines.systemHint": "Uses the wine binary installed on your system",
|
||||||
"wines.rescan": "Rescan folder",
|
"wines.rescan": "Rescan folder",
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"nav.settings": "Ustawienia",
|
"nav.settings": "Ustawienia",
|
||||||
"library.title": "Biblioteka aplikacji",
|
"library.title": "Biblioteka aplikacji",
|
||||||
"library.empty": "Brak aplikacji",
|
"library.empty": "Brak aplikacji",
|
||||||
"library.emptyHint": "Zaimportuj aplikację aby zacząć. Do wyboru masz mnóstwo opcji importu!",
|
"library.emptyHint": "Zaimportuj aplikację, aby zacząć. Launcher może skopiować ją do files/apps, żeby być w pełni przenośny, albo po prostu zapamiętać, gdzie się znajduje.",
|
||||||
"library.import": "Importuj aplikację",
|
"library.import": "Importuj aplikację",
|
||||||
"library.play": "Graj",
|
"library.play": "Graj",
|
||||||
"library.stop": "Zatrzymaj",
|
"library.stop": "Zatrzymaj",
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
"library.config": "Konfiguracja",
|
"library.config": "Konfiguracja",
|
||||||
"library.remove": "Usuń",
|
"library.remove": "Usuń",
|
||||||
"library.openFolder": "Otwórz folder",
|
"library.openFolder": "Otwórz folder",
|
||||||
"library.lastPlayed": "Ostatnio grane: {0}",
|
"library.lastPlayed": "Ostatnio grane {0}",
|
||||||
"library.playtime": "Czas gry: {0}",
|
"library.playtime": "Czas gry: {0}",
|
||||||
"library.playtimeSeconds": "{0} s",
|
"library.playtimeSeconds": "{0} s",
|
||||||
"library.playtimeMin": "{0} min",
|
"library.playtimeMin": "{0} min",
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
"library.inPrefix": "W prefiksie",
|
"library.inPrefix": "W prefiksie",
|
||||||
"library.removeConfirm": "Usunąć {0} z launchera?{1}",
|
"library.removeConfirm": "Usunąć {0} z launchera?{1}",
|
||||||
"library.removeFilesNote": "\nJej zaimportowane pliki w files/apps również zostaną usunięte.",
|
"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.removePrefixNote": "\nPliki zostają w prefiksie - usuwany jest tylko wpis w launcherze.",
|
||||||
"library.tools": "Narzędzia Wine",
|
"library.tools": "Narzędzia Wine",
|
||||||
"library.viewLogs": "Podejrzyj logi",
|
"library.viewLogs": "Podejrzyj logi",
|
||||||
"import.question": "Czy pliki aplikacji mają trafić do launchera, aby był przenośny, czy mają zostać tam, gdzie są?",
|
"import.question": "Czy pliki aplikacji mają trafić do launchera, aby był przenośny, czy mają zostać tam, gdzie są?",
|
||||||
|
|
@ -40,10 +40,10 @@
|
||||||
"import.mode.leave": "Zostaw w miejscu",
|
"import.mode.leave": "Zostaw w miejscu",
|
||||||
"import.mode.prefix": "Importuj z prefiksu wine",
|
"import.mode.prefix": "Importuj z prefiksu wine",
|
||||||
"import.prefix.title": "Import z prefiksu",
|
"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.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.pickPrefixFolder": "Wybierz folder aplikacji wewnątrz prefiksu (drive_c)",
|
||||||
"import.prefix.outside": "wybrany folder jest poza drive_c prefiksu",
|
"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.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.pickFolder": "Wybierz folder aplikacji",
|
||||||
"import.progress": "Importowanie {0}…",
|
"import.progress": "Importowanie {0}…",
|
||||||
"import.fileDone": "Skopiowane pliki: {0}",
|
"import.fileDone": "Skopiowane pliki: {0}",
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
"config.wineDefault": "Domyślna ({0})",
|
"config.wineDefault": "Domyślna ({0})",
|
||||||
"config.prefix": "Prefiks Wine",
|
"config.prefix": "Prefiks Wine",
|
||||||
"config.prefixDefault": "Prefiks domyślny ({0})",
|
"config.prefixDefault": "Prefiks domyślny ({0})",
|
||||||
"config.prefixLocked": "Ta aplikacja została zaimportowana z prefiksu, nie można zmienić jej prefiksu",
|
"config.prefixLocked": "Ta aplikacja została zaimportowana z prefiksu - nie można zmienić jej prefiksu",
|
||||||
"config.prefixLockedHint": "Przypięta do prefiksu, z którego została zaimportowana (ścieżki trzymane są względem niego)",
|
"config.prefixLockedHint": "Przypięta do prefiksu, z którego została zaimportowana (ścieżki trzymane są względem niego)",
|
||||||
"config.dllOverrides": "Nadpisania DLL",
|
"config.dllOverrides": "Nadpisania DLL",
|
||||||
"config.overrides.add": "Dodaj nadpisanie",
|
"config.overrides.add": "Dodaj nadpisanie",
|
||||||
|
|
@ -99,7 +99,7 @@
|
||||||
"tools.customPlaceholder": "np. winetricks vcrun2019",
|
"tools.customPlaceholder": "np. winetricks vcrun2019",
|
||||||
"tools.run": "Uruchom",
|
"tools.run": "Uruchom",
|
||||||
"prefixes.title": "Prefiksy Wine",
|
"prefixes.title": "Prefiksy Wine",
|
||||||
"prefixes.subtitle": "'Kontenery' wine trzymane w files/wineprefixes. Każdy z nich działa jak osobna instalacja wine.",
|
"prefixes.subtitle": "Prefiksy znajdują się w files/wineprefixes i są przygotowywane przez wine przy pierwszym użyciu.",
|
||||||
"prefixes.create": "Utwórz prefiks",
|
"prefixes.create": "Utwórz prefiks",
|
||||||
"prefixes.namePlaceholder": "Nazwa prefiksu",
|
"prefixes.namePlaceholder": "Nazwa prefiksu",
|
||||||
"prefixes.default": "Domyślny",
|
"prefixes.default": "Domyślny",
|
||||||
|
|
@ -114,7 +114,7 @@
|
||||||
"prefixes.invalidName": "Nieprawidłowa nazwa prefiksu",
|
"prefixes.invalidName": "Nieprawidłowa nazwa prefiksu",
|
||||||
"prefixes.mainLocked": "Prefiks Main nie może być usunięty",
|
"prefixes.mainLocked": "Prefiks Main nie może być usunięty",
|
||||||
"wines.title": "Wersje Wine",
|
"wines.title": "Wersje Wine",
|
||||||
"wines.subtitle": "Tutaj możesz zaimportować swoje buildy wine/proton. Proton działa w pełni bez Steama.",
|
"wines.subtitle": "Wrzuć buildy Wine lub Proton do files/wines i przeskanuj ponownie. Proton działa w pełni bez Steama.",
|
||||||
"wines.system": "Wine systemowe",
|
"wines.system": "Wine systemowe",
|
||||||
"wines.systemHint": "Używa binarki wine zainstalowanej w systemie",
|
"wines.systemHint": "Używa binarki wine zainstalowanej w systemie",
|
||||||
"wines.rescan": "Skanuj ponownie",
|
"wines.rescan": "Skanuj ponownie",
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,6 @@ function toast(message, kind) {
|
||||||
// import mode -> prefix picker happens within milliseconds), so showModal
|
// import mode -> prefix picker happens within milliseconds), so showModal
|
||||||
// cancels any pending teardown and clears the closing state.
|
// cancels any pending teardown and clears the closing state.
|
||||||
let modalTeardownTimer = null
|
let modalTeardownTimer = null
|
||||||
let modalSwapTimer = null
|
|
||||||
|
|
||||||
function showModal(html) {
|
function showModal(html) {
|
||||||
if (modalTeardownTimer) {
|
if (modalTeardownTimer) {
|
||||||
|
|
@ -119,55 +118,12 @@ function showModal(html) {
|
||||||
const overlay = qs('#modal-overlay')
|
const overlay = qs('#modal-overlay')
|
||||||
const modal = qs('#modal')
|
const modal = qs('#modal')
|
||||||
modal.classList.remove('closing')
|
modal.classList.remove('closing')
|
||||||
// Opening over an already-visible modal (import mode -> prefix picker):
|
qs('#modal').innerHTML = html
|
||||||
// morph instead of close-then-open - the box glides to the new height
|
|
||||||
// while old content dissolves into the new one.
|
|
||||||
const swapping = overlay.style.display !== 'none' && !!modal.innerHTML.trim()
|
|
||||||
if (swapping) swapModalContent(modal, html)
|
|
||||||
else modal.innerHTML = html
|
|
||||||
overlay.style.display = 'flex'
|
overlay.style.display = 'flex'
|
||||||
requestAnimationFrame(() => requestAnimationFrame(() => overlay.classList.add('show')))
|
requestAnimationFrame(() => requestAnimationFrame(() => overlay.classList.add('show')))
|
||||||
return qs('#modal')
|
return qs('#modal')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Animated content swap while the modal stays open. The new HTML is put in
|
|
||||||
// SYNCHRONOUSLY (callers wire their buttons right after showModal returns);
|
|
||||||
// an opaque snapshot of the old content is laid on top and fades out while
|
|
||||||
// the box animates to the new content's height. Same feel as switching tabs.
|
|
||||||
function swapModalContent(modal, html) {
|
|
||||||
if (modalSwapTimer) {
|
|
||||||
clearTimeout(modalSwapTimer)
|
|
||||||
modalSwapTimer = null
|
|
||||||
}
|
|
||||||
const prevGhost = modal.querySelector('.modal-ghost')
|
|
||||||
if (prevGhost) prevGhost.remove() // rapid double swap: snapshot clean state
|
|
||||||
modal.classList.remove('swapping')
|
|
||||||
const oldH = modal.offsetHeight
|
|
||||||
const ghost = document.createElement('div')
|
|
||||||
ghost.className = 'modal-ghost'
|
|
||||||
ghost.innerHTML = modal.innerHTML
|
|
||||||
modal.classList.add('resizing') // clip content while the box glides
|
|
||||||
modal.style.height = oldH + 'px'
|
|
||||||
modal.innerHTML = html
|
|
||||||
// measure the new natural height without an intermediate paint
|
|
||||||
modal.style.height = 'auto'
|
|
||||||
const newH = modal.offsetHeight
|
|
||||||
modal.style.height = oldH + 'px'
|
|
||||||
void modal.offsetHeight // flush styles so the height transition starts here
|
|
||||||
modal.appendChild(ghost)
|
|
||||||
void ghost.offsetHeight // commit the ghost's initial opacity:1 so the fade transition runs
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
modal.style.height = newH + 'px'
|
|
||||||
modal.classList.add('swapping') // fades the ghost out over the new content
|
|
||||||
modalSwapTimer = setTimeout(() => {
|
|
||||||
modalSwapTimer = null
|
|
||||||
ghost.remove()
|
|
||||||
modal.classList.remove('swapping', 'resizing')
|
|
||||||
modal.style.height = '' // back to auto (dropdowns may overflow again)
|
|
||||||
}, 240)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
const overlay = qs('#modal-overlay')
|
const overlay = qs('#modal-overlay')
|
||||||
const modal = qs('#modal')
|
const modal = qs('#modal')
|
||||||
|
|
@ -177,13 +133,8 @@ function closeModal() {
|
||||||
if (modalTeardownTimer) clearTimeout(modalTeardownTimer)
|
if (modalTeardownTimer) clearTimeout(modalTeardownTimer)
|
||||||
modalTeardownTimer = setTimeout(() => {
|
modalTeardownTimer = setTimeout(() => {
|
||||||
modalTeardownTimer = null
|
modalTeardownTimer = null
|
||||||
if (modalSwapTimer) {
|
|
||||||
clearTimeout(modalSwapTimer)
|
|
||||||
modalSwapTimer = null
|
|
||||||
}
|
|
||||||
overlay.style.display = 'none'
|
overlay.style.display = 'none'
|
||||||
modal.classList.remove('closing', 'swapping', 'resizing')
|
modal.classList.remove('closing')
|
||||||
modal.style.height = ''
|
|
||||||
modal.innerHTML = ''
|
modal.innerHTML = ''
|
||||||
}, 160)
|
}, 160)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -603,7 +603,6 @@ select {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.6);
|
transform: scale(0.6);
|
||||||
transition: all 0.2s var(--spring);
|
transition: all 0.2s var(--spring);
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
.modal-overlay.show .modal {
|
.modal-overlay.show .modal {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
@ -614,40 +613,6 @@ select {
|
||||||
transform: scale(0.8);
|
transform: scale(0.8);
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
}
|
}
|
||||||
/* --- animated content swap (import mode -> prefix picker etc.) -------------- */
|
|
||||||
/* During the morph the box keeps a fixed pixel height and glides to the new
|
|
||||||
content's height; overflow is clipped ONLY then (a permanent clip would
|
|
||||||
cut off open custom-select dropdowns). */
|
|
||||||
.modal.resizing {
|
|
||||||
overflow: hidden;
|
|
||||||
transition: height 0.22s var(--spring);
|
|
||||||
will-change: height;
|
|
||||||
}
|
|
||||||
/* Opaque snapshot of the OLD content laid over the new one; fades out while
|
|
||||||
the box glides -> content dissolves from old to new (like switching tabs). */
|
|
||||||
.modal .modal-ghost {
|
|
||||||
position: absolute;
|
|
||||||
inset: 1.4rem 1.6rem; /* overlay the box's own padding area */
|
|
||||||
z-index: 5;
|
|
||||||
pointer-events: none;
|
|
||||||
background: var(--panel-color);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.8rem; /* mirror the modal's layout so the snapshot aligns 1:1 */
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.modal.swapping .modal-ghost {
|
|
||||||
transition: opacity 0.18s ease;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
/* new content fades in under the fading-out ghost -> real crossfade */
|
|
||||||
.modal.swapping > :not(.modal-ghost) {
|
|
||||||
animation: modal-content-in 0.2s ease;
|
|
||||||
}
|
|
||||||
@keyframes modal-content-in {
|
|
||||||
from { opacity: 0; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
|
||||||
.modal h2 {
|
.modal h2 {
|
||||||
font-family: 'Nunito Black', 'Nunito', sans-serif;
|
font-family: 'Nunito Black', 'Nunito', sans-serif;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,20 @@
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
const { launcherIconPath } = require('./paths')
|
const path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
|
function findPng() {
|
||||||
|
const p = path.resolve(__dirname, '..', 'renderer', 'plwl.png')
|
||||||
|
try {
|
||||||
|
fs.accessSync(p)
|
||||||
|
return p
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setAppIcon(app) {
|
function setAppIcon(app) {
|
||||||
const png = launcherIconPath()
|
const png = findPng()
|
||||||
if (!png) return
|
if (!png) return
|
||||||
try {
|
try {
|
||||||
app.setIcon(png)
|
app.setIcon(png)
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,10 @@ function createGamesService({ getWindow, onGamesChanged }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveWine(game) {
|
function resolveWine(game) {
|
||||||
return require('../wines').resolveWine(game.wine || 'system')
|
const system = { id: 'system', type: 'system', label: 'System Wine' }
|
||||||
|
const wanted = game.wine === 'default' ? (library.loadSettings().defaultWine || 'system') : (game.wine || 'system')
|
||||||
|
if (!wanted || wanted === 'system') return system
|
||||||
|
return require('../wines').getWine(wanted) || system
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleExit(id) {
|
function handleExit(id) {
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,8 @@ autoQuitWhenDone = headless // booting just to run one game -> exit when it ends
|
||||||
// Lives as long as the app does: shows "active in background" even when the
|
// Lives as long as the app does: shows "active in background" even when the
|
||||||
// window is closed, and offers running games + Quit.
|
// window is closed, and offers running games + Quit.
|
||||||
function trayIconPath() {
|
function trayIconPath() {
|
||||||
return require('./paths').launcherIconPath() || undefined
|
const png = path.resolve(__dirname, '..', 'renderer', 'plwl.png')
|
||||||
|
return fs.existsSync(png) ? png : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTray() {
|
function createTray() {
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,10 @@ function textsFor(code) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveWine(id) {
|
function resolveWine(id) {
|
||||||
return require('./wines').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) {
|
function resolvePrefix(name) {
|
||||||
|
|
|
||||||
18
src/paths.js
18
src/paths.js
|
|
@ -114,22 +114,6 @@ function resolvePortablePath(p) {
|
||||||
return path.join(FILES, 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. Cached: it never moves at
|
|
||||||
// runtime.
|
|
||||||
let launcherIcon = null
|
|
||||||
function launcherIconPath() {
|
|
||||||
if (launcherIcon === null) {
|
|
||||||
const p = path.join(APP_ROOT, 'renderer', 'plwl.png')
|
|
||||||
try {
|
|
||||||
launcherIcon = fs.existsSync(p) ? p : ''
|
|
||||||
} catch {
|
|
||||||
launcherIcon = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return launcherIcon || null
|
|
||||||
}
|
|
||||||
|
|
||||||
function readJson(file, fallback) {
|
function readJson(file, fallback) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
||||||
|
|
@ -145,4 +129,4 @@ function writeJson(file, data) {
|
||||||
fs.renameSync(tmp, file)
|
fs.renameSync(tmp, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { RESOURCES, APP_ROOT, BUILD, FILES, DATA, DIRS, ensureDirs, syncBundledTranslations, launcherIconPath, readJson, writeJson, toPortablePath, resolvePortablePath }
|
module.exports = { RESOURCES, APP_ROOT, BUILD, FILES, DATA, DIRS, ensureDirs, syncBundledTranslations, readJson, writeJson, toPortablePath, resolvePortablePath }
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,8 @@ function buildDesktopEntry(game) {
|
||||||
} catch {}
|
} catch {}
|
||||||
if (!icon) {
|
if (!icon) {
|
||||||
// fall back to the launcher icon so the shortcut is never naked
|
// fall back to the launcher icon so the shortcut is never naked
|
||||||
icon = require('./paths').launcherIconPath() || ''
|
const png = path.resolve(__dirname, '..', 'renderer', 'plwl.png')
|
||||||
|
if (fs.existsSync(png)) icon = png
|
||||||
}
|
}
|
||||||
const lines = [
|
const lines = [
|
||||||
'[Desktop Entry]',
|
'[Desktop Entry]',
|
||||||
|
|
|
||||||
12
src/wines.js
12
src/wines.js
|
|
@ -156,16 +156,6 @@ function getDefaultWine() {
|
||||||
return require('./library').loadSettings().defaultWine || 'system'
|
return require('./library').loadSettings().defaultWine || 'system'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the wine build a run should use: 'default'/'system'/unknown all
|
|
||||||
// fall back to System Wine. Shared by the games service and the ad-hoc
|
|
||||||
// "open with" runner.
|
|
||||||
function resolveWine(id) {
|
|
||||||
const system = { id: 'system', type: 'system', label: 'System Wine' }
|
|
||||||
const wanted = id === 'default' ? getDefaultWine() : id
|
|
||||||
if (!wanted || wanted === 'system') return system
|
|
||||||
return getWine(wanted) || system
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDefaultWine(id) {
|
function setDefaultWine(id) {
|
||||||
const wines = getWines()
|
const wines = getWines()
|
||||||
if (id !== 'system' && !wines.some(w => w.id === id)) return { ok: false, error: 'notFound' }
|
if (id !== 'system' && !wines.some(w => w.id === id)) return { ok: false, error: 'notFound' }
|
||||||
|
|
@ -286,4 +276,4 @@ async function importWineFolder(srcDir) {
|
||||||
return { ok: true, path: toPortablePath(dst), symlinksFixed }
|
return { ok: true, path: toPortablePath(dst), symlinksFixed }
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { refreshWines, getWines, getWine, resolveWine, setWineDisabled, getDefaultWine, setDefaultWine, deleteWine, importWineFolder, findWineBinary, detectType, fixBuildSymlinks }
|
module.exports = { refreshWines, getWines, getWine, setWineDisabled, getDefaultWine, setDefaultWine, deleteWine, importWineFolder, findWineBinary, detectType, fixBuildSymlinks }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue