improvemets & fixes:

- added 'open with' handling to desktop shortcuts
- move logs button to app configuration
- fix sync progress disappearing after long hashing
- fix sync double file counter
- merge wine and proton prefix folders via symlink
This commit is contained in:
olcxja 2026-10-04 19:12:33 +02:00
commit 59ff0b1088
15 changed files with 520 additions and 113 deletions

View file

@ -45,7 +45,6 @@ const ICONS = {
folder: () => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>',
trash: () => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14M10 11v6M14 11v6"/></svg>',
terminal: () => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 17l6-5-6-5M12 19h8"/></svg>',
logs: () => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 3h9l5 5v13a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zM9 12h6M9 16h6"/></svg>',
star: () => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"><path d="M12 3l2.7 5.8 6.3.8-4.6 4.3 1.2 6.1-5.6-3.1-5.6 3.1 1.2-6.1L3 9.6l6.3-.8z"/></svg>',
download: () => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12M6 9l6 6 6-6"/><path d="M4 21h16"/></svg>',
chevronDown: () => '<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>'
@ -598,7 +597,6 @@ function renderLibrary() {
<button class="button small danger" data-act="kill" style="display:none">${t('library.stop')}</button>
<button class="button small" data-act="config" title="${t('library.config')}">${ICONS.config()}</button>
<button class="button small" data-act="folder" title="${t('library.openFolder')}">${ICONS.folder()}</button>
<button class="button small" data-act="logs" title="${t('library.viewLogs')}">${ICONS.logs()}</button>
<button class="button small danger" data-act="remove" title="${t('library.remove')}">${ICONS.trash()}</button>
</div>
`
@ -619,11 +617,6 @@ function renderLibrary() {
}
card.querySelector('[data-act=config]').onclick = () => openGameConfig(game.id)
card.querySelector('[data-act=folder]').onclick = () => window.plwl.shellOpenPath(game.workdir)
card.querySelector('[data-act=logs]').onclick = async () => {
const logs = await window.plwl.gamesGetLogs(game.id)
if (logs.length) window.plwl.shellShowItemInFolder(logs[logs.length - 1])
else toast(t('library.viewLogs'))
}
card.querySelector('[data-act=remove]').onclick = async () => {
const note = game.mode === 'copy' || game.mode === 'move'
? t('library.removeFilesNote')
@ -1250,6 +1243,16 @@ async function createAppShortcut() {
}
}
// Shows the newest run log in the file manager - it lives next to the app
// configuration, where the user is when something needs debugging.
async function showAppLogs() {
const game = state.currentGame
if (!game) return
const logs = await window.plwl.gamesGetLogs(game.id)
if (logs.length) window.plwl.shellShowItemInFolder(logs[logs.length - 1])
else toast(t('config.noLogs'), 'error')
}
function renderOverrides(overrides) {
const box = qs('#gc-overrides')
box.innerHTML = ''
@ -1401,8 +1404,10 @@ const LAN_CATEGORIES = [
{ id: 'apps-prefix', label: 'lansync.appsPrefix' },
{ id: 'wines', label: 'lansync.wines' },
{ id: 'prefixes', label: 'lansync.prefixes' },
// opt-in: without it a sync never touches the other machine's defaults
{ id: 'defaults', label: 'lansync.defaults' }
// registry-level settings: default wine/prefix, custom names of wine builds
// and prefixes, and each app's configuration plus its library order. No
// files move with this category - it only patches what already exists.
{ id: 'settings', label: 'lansync.settings' }
]
// Byte sizes for the progress popups. Decimal units, the way a file manager
@ -1742,7 +1747,11 @@ function onLanIncoming(info) {
}
let receiving = false
let receiveClosed = false
// true only once the sender's 'done' event ended the transfer. A popup the
// safety watchdog closed while the sender was still hashing must be allowed to
// reopen when events resume - permanently closing it made a running sync
// invisible even though the transfer continued.
let receiveEnded = false
let receiveWatchdog = null
let receiveStall = null
let receiveCloseTimer = null
@ -1777,7 +1786,6 @@ function endReceiving(message) {
}
const wasReceiving = receiving
receiving = false
receiveClosed = true
if (!wasReceiving) return
if (message) toast(message, 'error')
closeModal()
@ -1801,13 +1809,14 @@ function armReceiveWatchdog() {
function onLanReceiving(info) {
if (!info || !info.stage) return
if (state.lanPushing) return // our own push owns the (single) popup
if (info.stage === 'start') receiveClosed = false
if (info.stage === 'start') receiveEnded = false
if (info.stage === 'done') receiveEnded = true
if (!receiving) {
// The popup also opens mid-sync: the sender may have been hashing for
// minutes when this window starts listening, and a popup that the safety
// timeout closed has to come back for the rest of the transfer. A sync
// timeout closed comes back for the rest of the transfer. Only a sync
// that already ended stays closed.
if (receiveClosed && info.stage !== 'start') return
if (receiveEnded) return
if (qs('#modal-overlay').style.display !== 'none') return
receiving = true
const modal = showModal(`
@ -1831,6 +1840,15 @@ function onLanReceiving(info) {
function renderReceiving(info) {
const parts = receiveModalParts()
if (!parts) return
// A liveness ping from the sender (it is busy hashing a unit): the watchdog
// was re-armed by the caller, but what is on screen stays as it is - the
// sync is not stalled, just quiet. The exception is the popup this very ping
// just reopened (the safety timeout had closed it): every line would sit
// blank, so the phase says what is actually happening.
if (info.stage === 'wait') {
if (!parts.phase.textContent) parts.phase.textContent = t('lansync.recvWaiting')
return
}
// The same phases read from this side's point of view: a file arriving here
// is being received, not sent.
const phaseKey = RECV_PHASE_KEYS[info.stage] || 'lansync.phase.' + info.stage
@ -1955,6 +1973,7 @@ function wireEvents() {
qs('#btn-gc-back').addEventListener('click', () => showView('library'))
qs('#btn-gc-save').addEventListener('click', saveGameConfig)
qs('#btn-gc-shortcut').addEventListener('click', createAppShortcut)
qs('#btn-gc-logs').addEventListener('click', showAppLogs)
qs('#gc-override-add').addEventListener('click', addOverrideRow)
qs('#gc-exe-browse').addEventListener('click', async () => {
// start from the field being edited (exe picker from the exe field) and

View file

@ -196,6 +196,7 @@
<div class="form-actions">
<button class="button danger" id="btn-gc-clear-playtime" data-t="config.clearPlaytime"></button>
<button class="button" id="btn-gc-logs" data-t="config.viewLogs"></button>
<button class="button" id="btn-gc-shortcut" data-t="config.shortcut"></button>
<button class="button primary" id="btn-gc-save" data-t="config.save"></button>
</div>