- make prefix imported games paths relative to prefix
- show placeholder icon as fallback
- fix crossed working/exe directories
- fix broken working dir in prefix imported games
This commit is contained in:
olcxja 2026-09-30 16:27:31 +02:00
commit 66f42f30cc
8 changed files with 74 additions and 10 deletions

View file

@ -166,6 +166,7 @@ class CustomSelect {
this.options = options
this.value = value
this.onChange = onChange
this.disabled = false
this.render()
if (!this.host.dataset.wired) {
this.wire()
@ -173,6 +174,11 @@ class CustomSelect {
}
}
setDisabled(v) {
this.disabled = !!v
this.host.classList.toggle('disabled', this.disabled)
}
render() {
const selected = this.options.find(o => o.value === this.value)
this.host.classList.add('custom-select')
@ -219,6 +225,7 @@ class CustomSelect {
}
toggle() {
if (this.disabled) return
const wasOpen = this.host.classList.contains('open')
CustomSelect.closeAll()
if (!wasOpen) this.host.classList.add('open')
@ -357,15 +364,23 @@ function fmtPlaytime(seconds) {
}
async function attachCardIcon(card, game) {
// placeholder first so every card has a stable icon slot; swapped for the
// real icon once it loads - games without one just keep the placeholder
const ph = document.createElement('div')
ph.className = 'game-icon placeholder'
card.prepend(ph)
const p = await window.plwl.gamesIconPath(game.id)
if (!p) return
const img = document.createElement('img')
img.className = 'game-icon'
img.alt = ''
img.onerror = () => img.remove()
img.onload = () => card.prepend(img)
img.onerror = () => img.remove() // placeholder stays visible
img.onload = () => {
ph.remove()
card.prepend(img)
}
img.src = iconUrl(game.id)
if (img.complete && img.naturalWidth > 0) card.prepend(img)
if (img.complete && img.naturalWidth > 0) img.onload()
}
function renderLibrary() {
@ -817,6 +832,12 @@ async function openGameConfig(id) {
qs('#gc-env').value = Object.entries(game.env || {}).map(([k, v]) => `${k}=${v}`).join('\n')
renderWineOptions('gc-wine', game.wine)
renderPrefixOptions('gc-prefix', game.prefix)
// apps imported from a prefix are pinned to it (their paths are prefix-
// relative) - the select is locked, not just corrected on save
const prefixLocked = game.importSource === 'prefix'
selects['gc-prefix'].setDisabled(prefixLocked)
const lockHint = qs('#gc-prefix-lock-hint')
if (lockHint) lockHint.style.display = prefixLocked ? '' : 'none'
renderOverrides(game.dllOverrides || [])
renderTools()
}
@ -994,12 +1015,16 @@ function wireEvents() {
qs('#btn-gc-shortcut').addEventListener('click', createAppShortcut)
qs('#gc-override-add').addEventListener('click', addOverrideRow)
qs('#gc-exe-browse').addEventListener('click', async () => {
const cur = absPathInput(qs('#gc-workdir').value.trim())
// start from the field being edited (exe picker from the exe field) and
// resolve the SHOWN path (prefix apps display pfx/drive_c/...) to an
// absolute one - absPathInput alone would pass the prefix-relative form
// through and the picker would open at the process cwd ('/')
const cur = absGamePathInput(qs('#gc-exe').value.trim(), state.currentGame)
const p = await window.plwl.dialogPickFile({ properties: ['openFile'], defaultPath: cur || window.plwl.FILES_DIR })
if (p) qs('#gc-exe').value = displayGamePath(p, state.currentGame)
})
qs('#gc-workdir-browse').addEventListener('click', async () => {
const cur = absPathInput(qs('#gc-exe').value.trim())
const cur = absGamePathInput(qs('#gc-workdir').value.trim(), state.currentGame)
const p = await window.plwl.dialogPickFile({ properties: ['openDirectory'], defaultPath: cur || window.plwl.FILES_DIR })
if (p) qs('#gc-workdir').value = displayGamePath(p, state.currentGame)
})
@ -1012,8 +1037,12 @@ function wireEvents() {
applyIconResult(await window.plwl.gamesAutoIcon(state.currentGame.id))
})
qs('#gc-icon-browse').addEventListener('click', async () => {
// start in the app's own folder (prefix apps included) - that is where
// the exe with the embedded icon most likely lives
const start = absGamePathInput(qs('#gc-workdir').value.trim(), state.currentGame)
const p = await window.plwl.dialogPickFile({
properties: ['openFile'],
defaultPath: start || window.plwl.FILES_DIR,
title: t('config.icon.pick'),
filters: [{ name: t('config.icon.pick'), extensions: ['png', 'jpg', 'jpeg', 'svg', 'ico', 'webp', 'gif', 'bmp', 'exe'] }]
})

View file

@ -146,6 +146,7 @@
<label class="field-label" data-t="config.prefix"></label>
<span id="gc-prefix"></span>
<p class="subtitle" id="gc-prefix-lock-hint" data-t="config.prefixLockedHint" style="display:none"></p>
<label class="field-label" data-t="config.env"></label>
<textarea id="gc-env" rows="3" data-ph="config.envHint"></textarea>

View file

@ -476,6 +476,21 @@ select {
background: rgba(255, 255, 255, 0.03);
align-self: flex-start;
}
.game-icon.placeholder {
display: flex;
align-items: center;
justify-content: center;
color: var(--darker-text-color);
opacity: 0.45;
}
.game-icon.placeholder::before {
content: '';
width: 2rem;
height: 2rem;
background: currentColor;
-webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2'><rect x='3' y='3' width='18' height='18' rx='2'/><circle cx='8.5' cy='8.5' r='1.5'/><path d='M21 15l-5-5L5 21'/></svg>") center / contain no-repeat;
mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2'><rect x='3' y='3' width='18' height='18' rx='2'/><circle cx='8.5' cy='8.5' r='1.5'/><path d='M21 15l-5-5L5 21'/></svg>") center / contain no-repeat;
}
.game-card:hover { border-color: rgba(255, 255, 255, 0.2); }
.game-card h3 {
font-size: 1.05rem;