add app and apps icons
This commit is contained in:
parent
e0173d410b
commit
7508811374
16 changed files with 554 additions and 7 deletions
|
|
@ -262,6 +262,31 @@ function wineLabel(id) {
|
|||
return w ? w.label : (id === 'system' ? t('wines.system') : id)
|
||||
}
|
||||
|
||||
function iconUrl(id) {
|
||||
return `plgl-icon://${encodeURIComponent(id)}/icon`
|
||||
}
|
||||
|
||||
function fmtPlaytime(seconds) {
|
||||
const s = Math.round(seconds || 0)
|
||||
if (s < 60) return t('library.playtimeSeconds', s)
|
||||
const h = Math.floor(s / 3600)
|
||||
const m = Math.floor((s % 3600) / 60)
|
||||
if (h > 0) return t('library.playtimeHM', h, m)
|
||||
return t('library.playtimeMin', m)
|
||||
}
|
||||
|
||||
async function attachCardIcon(card, game) {
|
||||
const p = await window.plgl.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.src = iconUrl(game.id)
|
||||
if (img.complete && img.naturalWidth > 0) card.prepend(img)
|
||||
}
|
||||
|
||||
function renderLibrary() {
|
||||
const list = qs('#game-list')
|
||||
list.innerHTML = ''
|
||||
|
|
@ -280,6 +305,7 @@ function renderLibrary() {
|
|||
<span class="badge">${wineLabel(game.wine)}</span>
|
||||
</div>
|
||||
<div class="meta">${t('library.lastPlayed', last)}</div>
|
||||
<div class="meta">${t('library.playtime', fmtPlaytime(game.playSeconds || 0))}</div>
|
||||
<div class="actions">
|
||||
<button class="button primary small" data-act="play">${t('library.play')}</button>
|
||||
<button class="button small danger" data-act="kill" style="display:none">${t('library.stop')}</button>
|
||||
|
|
@ -321,6 +347,7 @@ function renderLibrary() {
|
|||
renderLibrary()
|
||||
}
|
||||
list.appendChild(card)
|
||||
attachCardIcon(card, game)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -571,6 +598,31 @@ function renderPrefixOptions(selectId, selected) {
|
|||
else selects[selectId].update(opts, selected || '')
|
||||
}
|
||||
|
||||
async function refreshConfigIcon() {
|
||||
const game = state.currentGame
|
||||
if (!game) return
|
||||
const p = await window.plgl.gamesIconPath(game.id)
|
||||
const img = qs('#gc-icon')
|
||||
const ph = qs('#gc-icon-placeholder')
|
||||
img.onerror = () => {
|
||||
img.style.display = 'none'
|
||||
img.removeAttribute('src')
|
||||
ph.style.display = 'flex'
|
||||
}
|
||||
if (p) {
|
||||
img.onload = () => {
|
||||
img.style.display = ''
|
||||
ph.style.display = 'none'
|
||||
}
|
||||
img.src = iconUrl(game.id) + '?t=' + Date.now()
|
||||
if (img.complete && img.naturalWidth > 0) img.onload()
|
||||
} else {
|
||||
img.style.display = 'none'
|
||||
img.removeAttribute('src')
|
||||
ph.style.display = 'flex'
|
||||
}
|
||||
}
|
||||
|
||||
async function openGameConfig(id) {
|
||||
const game = state.games.find(g => g.id === id)
|
||||
if (!game) return
|
||||
|
|
@ -578,6 +630,7 @@ async function openGameConfig(id) {
|
|||
showView('gameconfig')
|
||||
qs('#gc-title').textContent = game.name
|
||||
qs('#gc-name').value = game.name
|
||||
refreshConfigIcon()
|
||||
qs('#gc-exe').value = game.exePath
|
||||
qs('#gc-workdir').value = game.workdir
|
||||
qs('#gc-args').value = game.args || ''
|
||||
|
|
@ -744,6 +797,46 @@ function wireEvents() {
|
|||
if (p) qs('#gc-workdir').value = p
|
||||
})
|
||||
qs('#gc-tool-run').addEventListener('click', runCustomTool)
|
||||
qs('#gc-icon-auto').addEventListener('click', async () => {
|
||||
const res = await window.plgl.gamesAutoIcon(state.currentGame.id)
|
||||
if (res.ok) {
|
||||
toast(t('config.icon.set'), 'success')
|
||||
refreshConfigIcon()
|
||||
} else {
|
||||
toast(t('config.icon.noIcon'), 'error')
|
||||
}
|
||||
})
|
||||
qs('#gc-icon-browse').addEventListener('click', async () => {
|
||||
const p = await window.plgl.dialogPickFile({
|
||||
properties: ['openFile'],
|
||||
title: t('config.icon.pick'),
|
||||
filters: [{ name: t('config.icon.pick'), extensions: ['png', 'jpg', 'jpeg', 'svg', 'ico', 'webp', 'gif', 'bmp', 'exe'] }]
|
||||
})
|
||||
if (!p) return
|
||||
let res
|
||||
if (p.toLowerCase().endsWith('.exe')) {
|
||||
res = await window.plgl.gamesSetIconFile(state.currentGame.id, p)
|
||||
if (res.ok) toast(t('config.icon.set'), 'success')
|
||||
else toast(t('config.icon.noIcon'), 'error')
|
||||
} else {
|
||||
res = await window.plgl.gamesSetIconFile(state.currentGame.id, p)
|
||||
if (res.ok) toast(t('config.icon.set'), 'success')
|
||||
else toast(t('config.icon.noIcon'), 'error')
|
||||
}
|
||||
refreshConfigIcon()
|
||||
})
|
||||
qs('#gc-icon-reset').addEventListener('click', async () => {
|
||||
await window.plgl.gamesResetIcon(state.currentGame.id)
|
||||
refreshConfigIcon()
|
||||
})
|
||||
qs('#btn-gc-clear-playtime').addEventListener('click', async () => {
|
||||
const ok = await confirmModal(t('config.clearPlaytimeConfirm'), t('config.clearPlaytime'))
|
||||
if (!ok) return
|
||||
await window.plgl.gamesClearPlaytime(state.currentGame.id)
|
||||
state.games = await window.plgl.gamesList()
|
||||
state.currentGame = state.games.find(g => g.id === state.currentGame.id)
|
||||
toast(t('config.playtimeCleared'), 'success')
|
||||
})
|
||||
qs('#modal-overlay').addEventListener('click', e => {
|
||||
if (e.target === qs('#modal-overlay')) closeModal()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: plgl-icon:;">
|
||||
<title>PLGL</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
|
@ -117,8 +117,21 @@
|
|||
<button class="button" id="btn-gc-back" data-t="config.back"></button>
|
||||
</div>
|
||||
<div class="form-card">
|
||||
<label class="field-label" data-t="config.name"></label>
|
||||
<input type="text" id="gc-name">
|
||||
<div class="gc-head">
|
||||
<div class="gc-icon-box">
|
||||
<img id="gc-icon" alt="" style="display:none">
|
||||
<div id="gc-icon-placeholder" class="gc-icon-placeholder"></div>
|
||||
</div>
|
||||
<div class="gc-head-main">
|
||||
<label class="field-label" data-t="config.name"></label>
|
||||
<input type="text" id="gc-name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="icon-actions">
|
||||
<button class="button" id="gc-icon-auto" data-t="config.icon.auto"></button>
|
||||
<button class="button" id="gc-icon-browse" data-t="config.icon.browse"></button>
|
||||
<button class="button danger" id="gc-icon-reset" data-t="config.icon.remove"></button>
|
||||
</div>
|
||||
|
||||
<label class="field-label" data-t="config.exePath"></label>
|
||||
<div class="path-row">
|
||||
|
|
@ -149,6 +162,7 @@
|
|||
<button class="button" id="gc-override-add" data-t="config.overrides.add"></button>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="button danger" id="btn-gc-clear-playtime" data-t="config.clearPlaytime"></button>
|
||||
<button class="button primary" id="btn-gc-save" data-t="config.save"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
BIN
renderer/plgl.png
Normal file
BIN
renderer/plgl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
14
renderer/plgl.svg
Normal file
14
renderer/plgl.svg
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#2a2a2e"/>
|
||||
<stop offset="1" stop-color="#161618"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="16" y="16" width="224" height="224" rx="48" fill="url(#bg)" stroke="#3c3c42" stroke-width="4"/>
|
||||
<g fill="none" stroke="#eaeaf0" stroke-width="14" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M78 88l40 40-40 40"/>
|
||||
<path d="M132 168h46"/>
|
||||
</g>
|
||||
<path d="M186 64c0 22-14 34-30 38v18h-16v-18c-16-4-30-16-30-38 0-4 3-8 8-8s8 4 8 8c0 12 8 20 22 20s22-8 22-20c0-4 3-8 8-8s8 4 8 8z" fill="#eaeaf0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 688 B |
|
|
@ -162,6 +162,54 @@ content {
|
|||
font-size: 1.8rem;
|
||||
}
|
||||
.header-actions { display: flex; gap: 0.6rem; align-items: center; }
|
||||
|
||||
.gc-head {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.gc-head-main { flex: 1; }
|
||||
.gc-head-main .field-label { margin-top: 0; }
|
||||
.gc-icon-box {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
border-radius: 0.9rem;
|
||||
border: var(--border-width) solid var(--light-border-color);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.gc-icon-box img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
.gc-icon-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--darker-text-color);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.gc-icon-placeholder::before {
|
||||
content: '';
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
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;
|
||||
}
|
||||
.icon-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.subtitle { color: var(--darker-text-color); font-size: 0.9rem; margin-top: 0.2rem; }
|
||||
|
||||
.button {
|
||||
|
|
@ -392,6 +440,14 @@ select {
|
|||
gap: 0.5rem;
|
||||
animation: view-in 0.3s var(--spring);
|
||||
}
|
||||
.game-icon {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
object-fit: contain;
|
||||
border-radius: 0.6rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
align-self: flex-start;
|
||||
}
|
||||
.game-card:hover { border-color: rgba(255, 255, 255, 0.2); }
|
||||
.game-card h3 {
|
||||
font-size: 1.05rem;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue