more accurate sync progress report
This commit is contained in:
parent
8e3512b5cd
commit
c0f6e802ed
12 changed files with 390 additions and 63 deletions
177
renderer/app.js
177
renderer/app.js
|
|
@ -15,6 +15,10 @@ const state = {
|
|||
games: [],
|
||||
currentGame: null,
|
||||
lansync: null,
|
||||
// true while this device pushes a sync (and while it shows the progress
|
||||
// popup): only one such popup exists, the pushing side wins over an
|
||||
// incoming sync
|
||||
lanPushing: false,
|
||||
currentView: 'library'
|
||||
}
|
||||
|
||||
|
|
@ -114,8 +118,13 @@ function toast(message, kind) {
|
|||
// cancels any pending teardown and clears the closing state.
|
||||
let modalTeardownTimer = null
|
||||
let modalSwapTimer = null
|
||||
// A running sync must not be thrown away by a stray click: a locked modal
|
||||
// ignores the backdrop and Escape, only its own buttons (or the end of the
|
||||
// operation) close it.
|
||||
let modalLocked = false
|
||||
|
||||
function showModal(html) {
|
||||
function showModal(html, opts) {
|
||||
modalLocked = !!(opts && opts.locked)
|
||||
if (modalTeardownTimer) {
|
||||
clearTimeout(modalTeardownTimer)
|
||||
modalTeardownTimer = null
|
||||
|
|
@ -189,6 +198,7 @@ function closeModal() {
|
|||
modal.classList.remove('closing', 'swapping', 'resizing')
|
||||
modal.style.height = ''
|
||||
modal.innerHTML = ''
|
||||
modalLocked = false
|
||||
}, 160)
|
||||
}
|
||||
|
||||
|
|
@ -1137,6 +1147,41 @@ const LAN_CATEGORIES = [
|
|||
{ id: 'prefixes', label: 'lansync.prefixes' }
|
||||
]
|
||||
|
||||
// Byte sizes for the progress popups. Decimal units, the way a file manager
|
||||
// or a download shows them, because that is what a "1.2 GB" on screen means
|
||||
// to the user.
|
||||
function formatBytes(bytes) {
|
||||
const value = Math.max(0, Number(bytes) || 0)
|
||||
if (value < 1000) return `${Math.round(value)} B`
|
||||
if (value < 1000 * 1000) return `${(value / 1000).toFixed(1)} kB`
|
||||
if (value < 1000 * 1000 * 1000) return `${(value / 1000 / 1000).toFixed(1)} MB`
|
||||
return `${(value / 1000 / 1000 / 1000).toFixed(2)} GB`
|
||||
}
|
||||
|
||||
// "Games: 3 · Prefixes: 1 · Wine versions: 2" - only the categories this
|
||||
// sync actually carries are listed.
|
||||
function lanStatsLine(items) {
|
||||
if (!items) return ''
|
||||
const rows = [
|
||||
[items.apps, 'lansync.statsApps'],
|
||||
[items.prefixes, 'lansync.statsPrefixes'],
|
||||
[items.wines, 'lansync.statsWines']
|
||||
]
|
||||
return rows
|
||||
.filter(([count]) => count > 0)
|
||||
.map(([count, key]) => t(key, count))
|
||||
.join(' · ')
|
||||
}
|
||||
|
||||
// "340 MB / 1.2 GB" once the total is known (the sending side learns it as it
|
||||
// plans the units, the receiving side as the manifests arrive), otherwise
|
||||
// just what has moved so far - so the line is never a lying zero.
|
||||
function lanBytesLine(bytes, totalBytes) {
|
||||
const sent = formatBytes(bytes)
|
||||
if (!totalBytes) return bytes > 0 ? t('lansync.progressSent', sent) : ''
|
||||
return t('lansync.progressBytes', sent, formatBytes(totalBytes))
|
||||
}
|
||||
|
||||
async function renderLanSync() {
|
||||
const info = await window.plwl.lansyncGet()
|
||||
state.lansync = info
|
||||
|
|
@ -1243,17 +1288,23 @@ async function openLanSyncModal(device, errorKey) {
|
|||
}
|
||||
|
||||
async function runLanSync(device, code, categories, force) {
|
||||
// locked: a running sync is not cancellable by a stray click on the backdrop
|
||||
const modal = showModal(`
|
||||
<h2 id="ls-run-title"></h2>
|
||||
<div class="progress-row"><progress></progress></div>
|
||||
<p class="subtitle" id="ls-run-phase"></p>
|
||||
<p class="subtitle" id="ls-run-count"></p>
|
||||
`)
|
||||
<p class="subtitle" id="ls-run-stats"></p>
|
||||
<p class="subtitle" id="ls-run-bytes"></p>
|
||||
`, { locked: true })
|
||||
modal.querySelector('#ls-run-title').textContent = t('lansync.progress', device.name)
|
||||
const bar = modal.querySelector('progress')
|
||||
const phase = modal.querySelector('#ls-run-phase')
|
||||
const count = modal.querySelector('#ls-run-count')
|
||||
const stats = modal.querySelector('#ls-run-stats')
|
||||
const bytes = modal.querySelector('#ls-run-bytes')
|
||||
let active = true
|
||||
state.lanPushing = true
|
||||
window.plwl.onLanSyncProgress(data => {
|
||||
if (!active) return
|
||||
const key = 'lansync.phase.' + data.phase
|
||||
|
|
@ -1266,13 +1317,22 @@ async function runLanSync(device, code, categories, force) {
|
|||
bar.removeAttribute('value')
|
||||
count.textContent = ''
|
||||
}
|
||||
stats.textContent = lanStatsLine(data.items)
|
||||
bytes.textContent = lanBytesLine(data.bytes, data.totalBytes)
|
||||
})
|
||||
const res = await window.plwl.lansyncSync({ deviceId: device.id, code, categories, force })
|
||||
active = false
|
||||
let res = null
|
||||
try {
|
||||
res = await window.plwl.lansyncSync({ deviceId: device.id, code, categories, force })
|
||||
} catch (err) {
|
||||
res = { ok: false, error: (err && err.message) || '' }
|
||||
} finally {
|
||||
active = false
|
||||
state.lanPushing = false
|
||||
}
|
||||
closeModal()
|
||||
if (res && res.ok) {
|
||||
const summary = res.summary || {}
|
||||
toast(t('lansync.done', device.name, summary.uploaded || 0, summary.skipped || 0, summary.deleted || 0), 'success')
|
||||
toast(t('lansync.done', device.name, summary.uploaded || 0, summary.skipped || 0, summary.deleted || 0, formatBytes(summary.bytes || 0)), 'success')
|
||||
await renderLanSync()
|
||||
return
|
||||
}
|
||||
|
|
@ -1287,6 +1347,107 @@ async function runLanSync(device, code, categories, force) {
|
|||
toast(t('lansync.failed', message), 'error')
|
||||
}
|
||||
|
||||
// --- receiving a sync (the other device is the source of truth) -------------
|
||||
|
||||
// A sync pushed onto THIS device must also be visible here - otherwise the
|
||||
// library changes under the user's hands with no explanation. Same popup, but
|
||||
// driven by the events the receiving server emits, and equally not closable
|
||||
// by a click.
|
||||
const RECEIVE_WATCHDOG_MS = 5 * 60 * 1000
|
||||
let receiving = false
|
||||
let receiveWatchdog = null
|
||||
let receiveCloseTimer = null
|
||||
|
||||
function receiveModalParts() {
|
||||
const modal = qs('#modal')
|
||||
if (!receiving || !modal) return null
|
||||
const title = modal.querySelector('#ls-recv-title')
|
||||
if (!title) return null
|
||||
return {
|
||||
title,
|
||||
bar: modal.querySelector('#ls-recv-modal progress'),
|
||||
phase: modal.querySelector('#ls-recv-phase'),
|
||||
count: modal.querySelector('#ls-recv-count'),
|
||||
stats: modal.querySelector('#ls-recv-stats'),
|
||||
bytes: modal.querySelector('#ls-recv-bytes')
|
||||
}
|
||||
}
|
||||
|
||||
function endReceiving(message) {
|
||||
if (receiveWatchdog) {
|
||||
clearTimeout(receiveWatchdog)
|
||||
receiveWatchdog = null
|
||||
}
|
||||
if (receiveCloseTimer) {
|
||||
clearTimeout(receiveCloseTimer)
|
||||
receiveCloseTimer = null
|
||||
}
|
||||
const wasReceiving = receiving
|
||||
receiving = false
|
||||
if (!wasReceiving) return
|
||||
if (message) toast(message, 'error')
|
||||
closeModal()
|
||||
}
|
||||
|
||||
// A sender that crashed halfway through must not leave this window locked: the
|
||||
// popup closes itself if no event arrives for a while (the server drops the
|
||||
// session after the same idle time).
|
||||
function armReceiveWatchdog() {
|
||||
if (receiveWatchdog) clearTimeout(receiveWatchdog)
|
||||
receiveWatchdog = setTimeout(() => endReceiving(t('lansync.recvIdle')), RECEIVE_WATCHDOG_MS)
|
||||
}
|
||||
|
||||
function onLanReceiving(info) {
|
||||
if (!info || !info.stage) return
|
||||
if (state.lanPushing) return // our own push owns the (single) popup
|
||||
if (!receiving) {
|
||||
// open the popup only for the start of a sync, and only when the user is
|
||||
// not in the middle of another dialog (there is one modal at a time)
|
||||
if (info.stage !== 'start') return
|
||||
if (qs('#modal-overlay').style.display !== 'none') return
|
||||
receiving = true
|
||||
const modal = showModal(`
|
||||
<h2 id="ls-recv-title"></h2>
|
||||
<div class="progress-row" id="ls-recv-modal"><progress></progress></div>
|
||||
<p class="subtitle" id="ls-recv-phase"></p>
|
||||
<p class="subtitle" id="ls-recv-count"></p>
|
||||
<p class="subtitle" id="ls-recv-stats"></p>
|
||||
<p class="subtitle" id="ls-recv-bytes"></p>
|
||||
`, { locked: true })
|
||||
modal.querySelector('#ls-recv-title').textContent = t('lansync.receiving', info.from || info.deviceId || '')
|
||||
armReceiveWatchdog()
|
||||
return renderReceiving(info)
|
||||
}
|
||||
armReceiveWatchdog()
|
||||
renderReceiving(info)
|
||||
}
|
||||
|
||||
function renderReceiving(info) {
|
||||
const parts = receiveModalParts()
|
||||
if (!parts) return
|
||||
const phaseKey = 'lansync.phase.' + info.stage
|
||||
parts.phase.textContent = (info.stage === 'start' ? t('lansync.recvWaiting') : t(phaseKey, info.label || '')).trim()
|
||||
if (info.total > 0) {
|
||||
parts.bar.max = info.total
|
||||
parts.bar.value = info.done
|
||||
parts.count.textContent = t('lansync.progressFiles', info.done, info.total)
|
||||
} else {
|
||||
parts.bar.removeAttribute('value')
|
||||
parts.count.textContent = ''
|
||||
}
|
||||
parts.stats.textContent = lanStatsLine(info.items)
|
||||
parts.bytes.textContent = lanBytesLine(info.bytes, info.totalBytes)
|
||||
if (info.stage === 'done') {
|
||||
// let the totals be read, then get out of the way (the library reloads
|
||||
// itself through lansync:applied)
|
||||
if (receiveCloseTimer) clearTimeout(receiveCloseTimer)
|
||||
receiveCloseTimer = setTimeout(() => {
|
||||
receiveCloseTimer = null
|
||||
endReceiving('')
|
||||
}, 4000)
|
||||
}
|
||||
}
|
||||
|
||||
// Data received from another device changed the local files: reload every
|
||||
// list so the views show the merged library instead of stale state.
|
||||
async function reloadLocalData() {
|
||||
|
|
@ -1308,8 +1469,9 @@ function wireLanSync() {
|
|||
if (state.lansync) state.lansync.devices = devices
|
||||
if (state.currentView === 'lansync') renderLanDevices(devices)
|
||||
})
|
||||
window.plwl.onLanSyncReceiving(info => onLanReceiving(info))
|
||||
window.plwl.onLanSyncApplied(async info => {
|
||||
toast(t('lansync.applied', info.from || '', info.files || 0), 'success')
|
||||
toast(t('lansync.applied', info.from || '', info.files || 0, formatBytes(info.bytes || 0)), 'success')
|
||||
await reloadLocalData()
|
||||
})
|
||||
qs('#ls-reroll-id').addEventListener('click', async () => {
|
||||
|
|
@ -1422,10 +1584,11 @@ function wireEvents() {
|
|||
toast(t('config.playtimeCleared'), 'success')
|
||||
})
|
||||
qs('#modal-overlay').addEventListener('click', e => {
|
||||
if (modalLocked) return // a sync in progress: the backdrop is not a cancel button
|
||||
if (e.target === qs('#modal-overlay')) closeModal()
|
||||
})
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') closeModal()
|
||||
if (e.key === 'Escape' && !modalLocked) closeModal()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue