From 6eeafe2fa67b52aaa4b5eaaee4742d207e5331c5 Mon Sep 17 00:00:00 2001 From: olcxja Date: Wed, 30 Sep 2026 18:32:55 +0200 Subject: [PATCH 1/3] update translations --- files/translations/en-us.json | 20 ++++++++++---------- files/translations/pl.json | 18 +++++++++--------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/files/translations/en-us.json b/files/translations/en-us.json index 8c455f7..9a634d9 100644 --- a/files/translations/en-us.json +++ b/files/translations/en-us.json @@ -12,7 +12,7 @@ "nav.settings": "Settings", "library.title": "Apps Library", "library.empty": "No apps yet", - "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.emptyHint": "Import an app to get started. You have a lot import options to choose from!", "library.import": "Import app", "library.play": "Play", "library.stop": "Stop", @@ -20,9 +20,9 @@ "library.config": "Configure", "library.remove": "Remove", "library.openFolder": "Open folder", - "library.lastPlayed": "Last played {0}", + "library.lastPlayed": "Last played: {0}", "library.playtime": "Playtime: {0}", - "library.playtimeSeconds": "{0}s", + "library.playtimeSeconds": "{0} sec", "library.playtimeMin": "{0} min", "library.playtimeHM": "{0}h {1} min", "library.never": "Never", @@ -31,7 +31,7 @@ "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.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?", @@ -40,10 +40,10 @@ "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.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 drive_c", - "import.prefix.noDriveC": "this prefix has no drive_c yet - run any app in it once so wine creates it", + "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}…", "import.fileDone": "{0} files copied", @@ -69,7 +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.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.dllOverrides": "DLL overrides", "config.overrides.add": "Add override", @@ -99,7 +99,7 @@ "tools.customPlaceholder": "e.g. winetricks vcrun2019", "tools.run": "Run", "prefixes.title": "Wine prefixes", - "prefixes.subtitle": "Prefixes live in files/wineprefixes and are prepared by wine on first use.", + "prefixes.subtitle": "Wine 'containers' that live in files/wineprefixes. Each one is like separate wine install.", "prefixes.create": "Create prefix", "prefixes.namePlaceholder": "Prefix name", "prefixes.default": "Default", @@ -114,7 +114,7 @@ "prefixes.invalidName": "Invalid prefix name", "prefixes.mainLocked": "The Main prefix cannot be deleted", "wines.title": "Wine versions", - "wines.subtitle": "Drop Wine or Proton builds into files/wines, then rescan. Proton runs fully without Steam.", + "wines.subtitle": "You can import your wine/proton builds here. Proton runs fully without Steam.", "wines.system": "System Wine", "wines.systemHint": "Uses the wine binary installed on your system", "wines.rescan": "Rescan folder", diff --git a/files/translations/pl.json b/files/translations/pl.json index 7536f51..a209418 100644 --- a/files/translations/pl.json +++ b/files/translations/pl.json @@ -12,7 +12,7 @@ "nav.settings": "Ustawienia", "library.title": "Biblioteka aplikacji", "library.empty": "Brak aplikacji", - "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.emptyHint": "Zaimportuj aplikację aby zacząć. Do wyboru masz mnóstwo opcji importu!", "library.import": "Importuj aplikację", "library.play": "Graj", "library.stop": "Zatrzymaj", @@ -20,7 +20,7 @@ "library.config": "Konfiguracja", "library.remove": "Usuń", "library.openFolder": "Otwórz folder", - "library.lastPlayed": "Ostatnio grane {0}", + "library.lastPlayed": "Ostatnio grane: {0}", "library.playtime": "Czas gry: {0}", "library.playtimeSeconds": "{0} s", "library.playtimeMin": "{0} min", @@ -31,7 +31,7 @@ "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.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ą?", @@ -40,10 +40,10 @@ "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.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 drive_c prefiksu", - "import.prefix.noDriveC": "ten prefiks nie ma jeszcze drive_c - uruchom w nim dowolną apkę raz, żeby wine je utworzył", + "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}…", "import.fileDone": "Skopiowane pliki: {0}", @@ -69,7 +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.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.dllOverrides": "Nadpisania DLL", "config.overrides.add": "Dodaj nadpisanie", @@ -99,7 +99,7 @@ "tools.customPlaceholder": "np. winetricks vcrun2019", "tools.run": "Uruchom", "prefixes.title": "Prefiksy Wine", - "prefixes.subtitle": "Prefiksy znajdują się w files/wineprefixes i są przygotowywane przez wine przy pierwszym użyciu.", + "prefixes.subtitle": "'Kontenery' wine trzymane w files/wineprefixes. Każdy z nich działa jak osobna instalacja wine.", "prefixes.create": "Utwórz prefiks", "prefixes.namePlaceholder": "Nazwa prefiksu", "prefixes.default": "Domyślny", @@ -114,7 +114,7 @@ "prefixes.invalidName": "Nieprawidłowa nazwa prefiksu", "prefixes.mainLocked": "Prefiks Main nie może być usunięty", "wines.title": "Wersje Wine", - "wines.subtitle": "Wrzuć buildy Wine lub Proton do files/wines i przeskanuj ponownie. Proton działa w pełni bez Steama.", + "wines.subtitle": "Tutaj możesz zaimportować swoje buildy wine/proton. Proton działa w pełni bez Steama.", "wines.system": "Wine systemowe", "wines.systemHint": "Używa binarki wine zainstalowanej w systemie", "wines.rescan": "Skanuj ponownie", From 6f9c486774568c1d58771fd34d92def1ea5d99de Mon Sep 17 00:00:00 2001 From: olcxja Date: Wed, 30 Sep 2026 19:02:41 +0200 Subject: [PATCH 2/3] tuff popup transition --- renderer/app.js | 53 ++++++++++++++++++++++++++++++++++++++++++++-- renderer/style.css | 35 ++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/renderer/app.js b/renderer/app.js index 009e501..484c407 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -109,6 +109,7 @@ function toast(message, kind) { // import mode -> prefix picker happens within milliseconds), so showModal // cancels any pending teardown and clears the closing state. let modalTeardownTimer = null +let modalSwapTimer = null function showModal(html) { if (modalTeardownTimer) { @@ -118,12 +119,55 @@ function showModal(html) { const overlay = qs('#modal-overlay') const modal = qs('#modal') modal.classList.remove('closing') - qs('#modal').innerHTML = html + // Opening over an already-visible modal (import mode -> prefix picker): + // 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' requestAnimationFrame(() => requestAnimationFrame(() => overlay.classList.add('show'))) 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() { const overlay = qs('#modal-overlay') const modal = qs('#modal') @@ -133,8 +177,13 @@ function closeModal() { if (modalTeardownTimer) clearTimeout(modalTeardownTimer) modalTeardownTimer = setTimeout(() => { modalTeardownTimer = null + if (modalSwapTimer) { + clearTimeout(modalSwapTimer) + modalSwapTimer = null + } overlay.style.display = 'none' - modal.classList.remove('closing') + modal.classList.remove('closing', 'swapping', 'resizing') + modal.style.height = '' modal.innerHTML = '' }, 160) } diff --git a/renderer/style.css b/renderer/style.css index 2af013a..ed7e8f5 100644 --- a/renderer/style.css +++ b/renderer/style.css @@ -603,6 +603,7 @@ select { opacity: 0; transform: scale(0.6); transition: all 0.2s var(--spring); + position: relative; } .modal-overlay.show .modal { opacity: 1; @@ -613,6 +614,40 @@ select { transform: scale(0.8); 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 { font-family: 'Nunito Black', 'Nunito', sans-serif; font-size: 1.2rem; From 944d8f86c2438b540edd4f2963e1e43aea2f1879 Mon Sep 17 00:00:00 2001 From: olcxja Date: Wed, 30 Sep 2026 19:16:52 +0200 Subject: [PATCH 3/3] add dev rules --- README.md | 5 ++++ development.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++++ src/appicon.js | 15 ++-------- src/ipc/games.js | 5 +--- src/main.js | 3 +- src/openwith.js | 5 +--- src/paths.js | 18 +++++++++++- src/shortcuts.js | 3 +- src/wines.js | 12 +++++++- 9 files changed, 112 insertions(+), 27 deletions(-) create mode 100644 development.md diff --git a/README.md b/README.md index 0a88a00..76d302b 100644 --- a/README.md +++ b/README.md @@ -58,3 +58,8 @@ 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. 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). + diff --git a/development.md b/development.md new file mode 100644 index 0000000..8e54565 --- /dev/null +++ b/development.md @@ -0,0 +1,73 @@ +# 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/.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 # syntax +node test/import.test.js # ...and the other test files +npm start # click through the flow you touched +``` diff --git a/src/appicon.js b/src/appicon.js index c9738c2..314cd90 100644 --- a/src/appicon.js +++ b/src/appicon.js @@ -1,20 +1,9 @@ 'use strict' -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 - } -} +const { launcherIconPath } = require('./paths') function setAppIcon(app) { - const png = findPng() + const png = launcherIconPath() if (!png) return try { app.setIcon(png) diff --git a/src/ipc/games.js b/src/ipc/games.js index 5c89a12..70f215b 100644 --- a/src/ipc/games.js +++ b/src/ipc/games.js @@ -40,10 +40,7 @@ function createGamesService({ getWindow, onGamesChanged }) { } function resolveWine(game) { - 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 + return require('../wines').resolveWine(game.wine || 'system') } function handleExit(id) { diff --git a/src/main.js b/src/main.js index 15f2697..75a28d7 100644 --- a/src/main.js +++ b/src/main.js @@ -87,8 +87,7 @@ 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 // window is closed, and offers running games + Quit. function trayIconPath() { - const png = path.resolve(__dirname, '..', 'renderer', 'plwl.png') - return fs.existsSync(png) ? png : undefined + return require('./paths').launcherIconPath() || undefined } function createTray() { diff --git a/src/openwith.js b/src/openwith.js index 57ec493..fb256a1 100644 --- a/src/openwith.js +++ b/src/openwith.js @@ -49,10 +49,7 @@ function textsFor(code) { } 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 + return require('./wines').resolveWine(id) } function resolvePrefix(name) { diff --git a/src/paths.js b/src/paths.js index b244087..24656ae 100644 --- a/src/paths.js +++ b/src/paths.js @@ -114,6 +114,22 @@ function resolvePortablePath(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) { try { return JSON.parse(fs.readFileSync(file, 'utf8')) @@ -129,4 +145,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, syncBundledTranslations, launcherIconPath, readJson, writeJson, toPortablePath, resolvePortablePath } diff --git a/src/shortcuts.js b/src/shortcuts.js index 0718b3b..aa69016 100644 --- a/src/shortcuts.js +++ b/src/shortcuts.js @@ -63,8 +63,7 @@ function buildDesktopEntry(game) { } catch {} if (!icon) { // fall back to the launcher icon so the shortcut is never naked - const png = path.resolve(__dirname, '..', 'renderer', 'plwl.png') - if (fs.existsSync(png)) icon = png + icon = require('./paths').launcherIconPath() || '' } const lines = [ '[Desktop Entry]', diff --git a/src/wines.js b/src/wines.js index fc761cd..abdece1 100644 --- a/src/wines.js +++ b/src/wines.js @@ -156,6 +156,16 @@ function getDefaultWine() { 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) { const wines = getWines() if (id !== 'system' && !wines.some(w => w.id === id)) return { ok: false, error: 'notFound' } @@ -276,4 +286,4 @@ async function importWineFolder(srcDir) { return { ok: true, path: toPortablePath(dst), symlinksFixed } } -module.exports = { refreshWines, getWines, getWine, setWineDisabled, getDefaultWine, setDefaultWine, deleteWine, importWineFolder, findWineBinary, detectType, fixBuildSymlinks } +module.exports = { refreshWines, getWines, getWine, resolveWine, setWineDisabled, getDefaultWine, setDefaultWine, deleteWine, importWineFolder, findWineBinary, detectType, fixBuildSymlinks }