cleanup & sync summary
This commit is contained in:
parent
cd0007fc7d
commit
be4f67fb60
17 changed files with 637 additions and 207 deletions
149
renderer/app.js
149
renderer/app.js
|
|
@ -818,6 +818,17 @@ async function startPrefixImportFlow() {
|
|||
}
|
||||
}
|
||||
|
||||
// One place for the technical error tokens the import handlers answer with.
|
||||
function importErrorText(raw) {
|
||||
if (raw === 'no-exe-found') return t('import.noExe')
|
||||
if (raw === 'no-linux-exe') return t('import.noLinuxExe')
|
||||
if (raw === 'prefix-root') return t('import.prefixRoot')
|
||||
if (raw === 'no-drive-c') return t('import.prefix.noDriveC')
|
||||
if (raw === 'outside-drive-c') return t('import.prefix.outside')
|
||||
if (raw === 'copy-incomplete') return t('import.copyIncomplete')
|
||||
return raw || ''
|
||||
}
|
||||
|
||||
async function doImport(srcDir, mode, kind) {
|
||||
const name = srcDir.split('/').filter(Boolean).pop()
|
||||
const modal = showModal(`
|
||||
|
|
@ -842,13 +853,7 @@ async function doImport(srcDir, mode, kind) {
|
|||
const game = await window.plwl.gamesImport(srcDir, mode, kind)
|
||||
closeModal()
|
||||
if (!game || game.ok === false) {
|
||||
const raw = (game && game.error) || ''
|
||||
const errText =
|
||||
raw === 'no-exe-found' ? t('import.noExe')
|
||||
: raw === 'no-linux-exe' ? t('import.noLinuxExe')
|
||||
: raw === 'prefix-root' ? t('import.prefixRoot')
|
||||
: (raw || 'unknown error')
|
||||
toast(t('import.failed', errText), 'error')
|
||||
toast(t('import.failed', importErrorText((game && game.error) || '')), 'error')
|
||||
return
|
||||
}
|
||||
state.games = await window.plwl.gamesList()
|
||||
|
|
@ -1005,13 +1010,7 @@ async function doPrefixImport(prefix, srcDir) {
|
|||
const game = await window.plwl.gamesImportFromPrefix(prefix, srcDir)
|
||||
closeModal()
|
||||
if (!game || game.ok === false) {
|
||||
const raw = (game && game.error) || ''
|
||||
const errText =
|
||||
raw === 'no-exe-found' ? t('import.noExe')
|
||||
: raw === 'no-drive-c' ? t('import.prefix.noDriveC')
|
||||
: raw === 'outside-drive-c' ? t('import.prefix.outside')
|
||||
: (raw || 'unknown error')
|
||||
toast(t('import.failed', errText), 'error')
|
||||
toast(t('import.failed', importErrorText((game && game.error) || '')), 'error')
|
||||
return
|
||||
}
|
||||
state.games = await window.plwl.gamesList()
|
||||
|
|
@ -1633,6 +1632,52 @@ async function runWithProgress({ title, action }) {
|
|||
}
|
||||
}
|
||||
|
||||
// End-of-sync popup: what moved, what was already there, and how much space
|
||||
// the synced trees take. A toast is too easy to miss for a transfer that can
|
||||
// run for minutes.
|
||||
function showSyncSummary({ from, received, categories, summary }) {
|
||||
const items = (summary && summary.items) || {}
|
||||
const cats = new Set(categories || [])
|
||||
const apps = Math.max(0, Number(items.apps) || 0)
|
||||
const wines = Math.max(0, Number(items.wines) || 0)
|
||||
const prefixes = Math.max(0, Number(items.prefixes) || 0)
|
||||
const files = Math.max(0, Number(summary && summary.uploaded) || 0)
|
||||
const skipped = Math.max(0, Number(summary && summary.skipped) || 0)
|
||||
const deleted = Math.max(0, Number(summary && summary.deleted) || 0)
|
||||
const bytes = Math.max(0, Number(summary && summary.bytes) || 0)
|
||||
const sizeBytes = Math.max(0, Number(summary && summary.sizeBytes) || 0)
|
||||
const rows = []
|
||||
const row = (label, value) => rows.push(
|
||||
`<div class="sync-summary-row"><span>${label}</span><strong>${value}</strong></div>`
|
||||
)
|
||||
// a count alone also shows for a peer that did not send its categories
|
||||
if (apps || cats.has('apps-portable') || cats.has('apps-prefix')) row(t('lansync.summaryApps'), apps)
|
||||
if (wines || cats.has('wines')) row(t('lansync.wines'), wines)
|
||||
if (prefixes || cats.has('prefixes')) row(t('lansync.prefixes'), prefixes)
|
||||
if (cats.has('settings')) row(t('lansync.summarySettings'), t('lansync.summaryYes'))
|
||||
if (files || skipped || deleted) {
|
||||
row(t(received ? 'lansync.summaryReceived' : 'lansync.summarySent'), files)
|
||||
if (skipped) row(t('lansync.summarySkipped'), skipped)
|
||||
if (deleted) row(t('lansync.summaryDeleted'), deleted)
|
||||
}
|
||||
if (bytes) row(t('lansync.summaryTransferred'), formatBytes(bytes))
|
||||
if (sizeBytes) row(t('lansync.summarySize'), formatBytes(sizeBytes))
|
||||
const modal = showModal(`
|
||||
<h2>${t('lansync.summaryTitle')}</h2>
|
||||
<p class="subtitle" id="ls-summary-from"></p>
|
||||
<div class="sync-summary">${rows.join('')}</div>
|
||||
<div class="modal-actions">
|
||||
<button class="button primary" id="ls-summary-ok">${t('dialog.ok')}</button>
|
||||
</div>
|
||||
`)
|
||||
// the device name comes off the network: never interpolate it into HTML
|
||||
modal.querySelector('#ls-summary-from').textContent = t(
|
||||
received ? 'lansync.summaryReceivedFrom' : 'lansync.summarySentTo',
|
||||
from || ''
|
||||
)
|
||||
modal.querySelector('#ls-summary-ok').onclick = closeModal
|
||||
}
|
||||
|
||||
// "340 MB / 1.2 GB" once the total is known, otherwise just the moved bytes.
|
||||
function lanBytesLine(bytes, totalBytes) {
|
||||
const sent = formatBytes(bytes)
|
||||
|
|
@ -1785,15 +1830,16 @@ async function runLanSync(device, code, categories, force) {
|
|||
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, formatBytes(summary.bytes || 0)), 'success')
|
||||
// swap the locked progress popup into the result, not a toast
|
||||
showSyncSummary({ from: device.name, received: false, categories, summary: res.summary || {} })
|
||||
await renderLanSync()
|
||||
return
|
||||
}
|
||||
closeModal()
|
||||
const err = (res && res.error) || ''
|
||||
if (err === 'bad-code') {
|
||||
// the server answers a wrong code as 'auth-failed'; both mean "ask again"
|
||||
if (err === 'bad-code' || err === 'auth-failed') {
|
||||
await window.plwl.lansyncForget(device.id)
|
||||
toast(t('lansync.badCode'), 'error')
|
||||
openLanSyncModal({ ...device, code: '' }, 'lansync.badCode')
|
||||
|
|
@ -1801,7 +1847,7 @@ async function runLanSync(device, code, categories, force) {
|
|||
}
|
||||
const message =
|
||||
err === 'busy' ? t('lansync.busy')
|
||||
: err === 'offline' ? t('lansync.offline')
|
||||
: err === 'offline' || err === 'receiver-lost' ? t('lansync.offline')
|
||||
: err === 'rejected' ? t('lansync.rejected')
|
||||
: err === 'incoming-timeout' ? t('lansync.incomingTimedOut')
|
||||
: (err || t('lansync.unknownError'))
|
||||
|
|
@ -1877,6 +1923,9 @@ function onLanIncoming(info) {
|
|||
}
|
||||
|
||||
let receiving = false
|
||||
// The last apply payload, for the end-of-sync summary (it carries files,
|
||||
// bytes, skipped files and the total size of the synced trees).
|
||||
let lastApplied = null
|
||||
// Only the sender's 'done' ends this; a watchdog-closed popup may reopen.
|
||||
let receiveEnded = false
|
||||
let receiveWatchdog = null
|
||||
|
|
@ -1935,12 +1984,25 @@ 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') receiveEnded = false
|
||||
if (info.stage === 'done') receiveEnded = true
|
||||
if (info.stage === 'start') {
|
||||
receiveEnded = false
|
||||
lastApplied = null
|
||||
}
|
||||
if (info.stage === 'done') {
|
||||
receiveEnded = true
|
||||
// the popup may have been closed by the watchdog: still show the result
|
||||
// if nothing else owns the modal slot
|
||||
if (!receiving) {
|
||||
if (lastApplied && qs('#modal-overlay').style.display === 'none') showReceiveSummary(info)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!receiving) {
|
||||
// reopen a watchdog-closed popup unless the sync already ended
|
||||
if (receiveEnded) return
|
||||
if (qs('#modal-overlay').style.display !== 'none') return
|
||||
// a completion summary underneath gives way to the next sync's progress
|
||||
const summaryOpen = !!qs('#modal').querySelector('#ls-summary-ok')
|
||||
if (!summaryOpen && qs('#modal-overlay').style.display !== 'none') return
|
||||
receiving = true
|
||||
const modal = showModal(`
|
||||
<h2 id="ls-recv-title"></h2>
|
||||
|
|
@ -1979,15 +2041,47 @@ function renderReceiving(info) {
|
|||
parts.stats.textContent = lanStatsLine(info.items, info.itemsDone, info.totals)
|
||||
parts.bytes.textContent = lanBytesLine(info.bytes, info.totalBytes)
|
||||
if (info.stage === 'done') {
|
||||
// let the totals be read, then close (the library reloads itself)
|
||||
// hold the finished bar for a moment, then swap it for the summary
|
||||
if (receiveCloseTimer) clearTimeout(receiveCloseTimer)
|
||||
receiveCloseTimer = setTimeout(() => {
|
||||
receiveCloseTimer = null
|
||||
endReceiving('')
|
||||
}, 4000)
|
||||
showReceiveSummary(info)
|
||||
}, 1200)
|
||||
}
|
||||
}
|
||||
|
||||
// Swap the finished progress popup for the result; the library has already
|
||||
// been reloaded by the apply event.
|
||||
function showReceiveSummary(info) {
|
||||
if (receiveWatchdog) {
|
||||
clearTimeout(receiveWatchdog)
|
||||
receiveWatchdog = null
|
||||
}
|
||||
if (receiveStall) {
|
||||
clearTimeout(receiveStall)
|
||||
receiveStall = null
|
||||
}
|
||||
if (receiveCloseTimer) {
|
||||
clearTimeout(receiveCloseTimer)
|
||||
receiveCloseTimer = null
|
||||
}
|
||||
receiving = false
|
||||
const applied = lastApplied || {}
|
||||
showSyncSummary({
|
||||
from: applied.from || info.from || '',
|
||||
received: true,
|
||||
categories: applied.categories || [],
|
||||
summary: {
|
||||
uploaded: applied.files ?? info.done,
|
||||
skipped: applied.skipped || 0,
|
||||
deleted: applied.deleted ?? info.deleted,
|
||||
bytes: applied.bytes ?? info.bytes,
|
||||
sizeBytes: applied.sizeBytes ?? info.sizeBytes,
|
||||
items: applied.items || info.items || {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// A received sync changed local files: reload every list (settings included).
|
||||
async function reloadLocalData() {
|
||||
const [wines, prefixes, games, settings] = await Promise.all([
|
||||
|
|
@ -2014,7 +2108,8 @@ function wireLanSync() {
|
|||
window.plwl.onLanSyncReceiving(info => onLanReceiving(info))
|
||||
window.plwl.onLanSyncIncoming(info => onLanIncoming(info))
|
||||
window.plwl.onLanSyncApplied(async info => {
|
||||
toast(t('lansync.applied', info.from || '', info.files || 0, formatBytes(info.bytes || 0)), 'success')
|
||||
// the summary popup replaces the toast; reload behind it
|
||||
lastApplied = info
|
||||
await reloadLocalData()
|
||||
})
|
||||
qs('#ls-reroll-id').addEventListener('click', async () => {
|
||||
|
|
@ -2079,7 +2174,7 @@ function wireEvents() {
|
|||
toast(t('wines.imported', res.path), 'success')
|
||||
state.wines = await window.plwl.winesList()
|
||||
} else {
|
||||
toast(t('toast.importFailed') + ': ' + (res.error || ''), 'error')
|
||||
toast(t('import.failed', importErrorText((res && res.error) || '')), 'error')
|
||||
}
|
||||
renderWines()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -195,6 +195,20 @@ content {
|
|||
user has to notice, so it stands out from the subtitles around it. */
|
||||
.sync-warning { color: var(--big-red); margin-top: 0.6rem; }
|
||||
|
||||
/* End-of-sync summary: label left, count/size right, one row per fact. */
|
||||
.sync-summary { display: flex; flex-direction: column; }
|
||||
.sync-summary-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.45rem 0;
|
||||
border-bottom: var(--border-width) solid var(--light-border-color);
|
||||
}
|
||||
.sync-summary-row:last-child { border-bottom: none; padding-bottom: 0; }
|
||||
.sync-summary-row span { color: var(--darker-text-color); }
|
||||
.sync-summary-row strong { font-weight: 700; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.button {
|
||||
height: var(--button-height);
|
||||
padding: 0 1rem;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue