a LOT sync fixes...
This commit is contained in:
parent
c0f6e802ed
commit
806e173e43
14 changed files with 1073 additions and 240 deletions
129
renderer/app.js
129
renderer/app.js
|
|
@ -1144,7 +1144,9 @@ const LAN_CATEGORIES = [
|
|||
{ id: 'apps-portable', label: 'lansync.appsPortable' },
|
||||
{ id: 'apps-prefix', label: 'lansync.appsPrefix' },
|
||||
{ id: 'wines', label: 'lansync.wines' },
|
||||
{ id: 'prefixes', label: 'lansync.prefixes' }
|
||||
{ id: 'prefixes', label: 'lansync.prefixes' },
|
||||
// opt-in: without it a sync never touches the other machine's defaults
|
||||
{ id: 'defaults', label: 'lansync.defaults' }
|
||||
]
|
||||
|
||||
// Byte sizes for the progress popups. Decimal units, the way a file manager
|
||||
|
|
@ -1158,19 +1160,45 @@ function formatBytes(bytes) {
|
|||
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 ''
|
||||
// "Apps: 2/5 · Prefixes: 1/2 · Wine versions: 0/1" - how much of each category
|
||||
// is done out of how much the sync carries, so the line says what is still
|
||||
// coming instead of only listing what has already been walked over. A peer
|
||||
// that does not announce its totals falls back to the plain count.
|
||||
function lanStatsLine(items, itemsDone, totals) {
|
||||
const rows = [
|
||||
[items.apps, 'lansync.statsApps'],
|
||||
[items.prefixes, 'lansync.statsPrefixes'],
|
||||
[items.wines, 'lansync.statsWines']
|
||||
['apps', 'lansync.statsApps'],
|
||||
['prefixes', 'lansync.statsPrefixes'],
|
||||
['wines', 'lansync.statsWines']
|
||||
]
|
||||
return rows
|
||||
.filter(([count]) => count > 0)
|
||||
.map(([count, key]) => t(key, count))
|
||||
.join(' · ')
|
||||
const out = []
|
||||
for (const [kind, key] of rows) {
|
||||
const listed = Number(items && items[kind]) || 0
|
||||
const total = Number(totals && totals[kind]) || 0
|
||||
const known = itemsDone && Number.isFinite(Number(itemsDone[kind]))
|
||||
const done = known ? Number(itemsDone[kind]) : listed
|
||||
if (total > 0) out.push(t(key, `${Math.min(done, total)}/${total}`))
|
||||
else if (listed > 0) out.push(t(key, listed))
|
||||
}
|
||||
return out.join(' · ')
|
||||
}
|
||||
|
||||
// The bar follows BYTES, not file counts: a sync spends its time on the big
|
||||
// files, and a file-count bar sits at 99% while the last gigabyte crawls in.
|
||||
// It also follows the PHASE it is in (barBytes/barTotalBytes): a phase starts
|
||||
// from an empty bar instead of inheriting a full one from the phase before, so
|
||||
// hashing never looks finished while it has just begun.
|
||||
function renderProgressBar(bar, bytes, totalBytes) {
|
||||
const sent = Math.max(0, Number(bytes) || 0)
|
||||
const total = Math.max(0, Number(totalBytes) || 0)
|
||||
bar.max = Math.max(total, 1)
|
||||
bar.value = Math.min(sent, bar.max)
|
||||
}
|
||||
|
||||
// Older peers only send the overall byte totals - then those drive the bar.
|
||||
function barValues(info) {
|
||||
const bytes = info.barBytes == null ? info.bytes : info.barBytes
|
||||
const total = info.barTotalBytes == null ? info.totalBytes : info.barTotalBytes
|
||||
return [bytes, total]
|
||||
}
|
||||
|
||||
// "340 MB / 1.2 GB" once the total is known (the sending side learns it as it
|
||||
|
|
@ -1296,8 +1324,10 @@ async function runLanSync(device, code, categories, force) {
|
|||
<p class="subtitle" id="ls-run-count"></p>
|
||||
<p class="subtitle" id="ls-run-stats"></p>
|
||||
<p class="subtitle" id="ls-run-bytes"></p>
|
||||
<p class="subtitle sync-warning" id="ls-run-warning"></p>
|
||||
`, { locked: true })
|
||||
modal.querySelector('#ls-run-title').textContent = t('lansync.progress', device.name)
|
||||
modal.querySelector('#ls-run-warning').textContent = t('lansync.keepOpen')
|
||||
const bar = modal.querySelector('progress')
|
||||
const phase = modal.querySelector('#ls-run-phase')
|
||||
const count = modal.querySelector('#ls-run-count')
|
||||
|
|
@ -1307,17 +1337,14 @@ async function runLanSync(device, code, categories, force) {
|
|||
state.lanPushing = true
|
||||
window.plwl.onLanSyncProgress(data => {
|
||||
if (!active) return
|
||||
const key = 'lansync.phase.' + data.phase
|
||||
phase.textContent = t(key, data.label || '')
|
||||
if (data.total > 0) {
|
||||
bar.max = data.total
|
||||
bar.value = data.done
|
||||
count.textContent = t('lansync.progressFiles', data.done, data.total)
|
||||
} else {
|
||||
bar.removeAttribute('value')
|
||||
count.textContent = ''
|
||||
}
|
||||
stats.textContent = lanStatsLine(data.items)
|
||||
// Every line keeps its value while the sync moves between phases: hashing
|
||||
// reports its own file counter, copying its own, and the bar follows the
|
||||
// bytes the whole way through
|
||||
phase.textContent = t('lansync.phase.' + data.phase, data.label || '').trim()
|
||||
renderProgressBar(bar, ...barValues(data))
|
||||
if (data.total > 0) count.textContent = t('lansync.progressFiles', data.done, data.total)
|
||||
else count.textContent = ''
|
||||
stats.textContent = lanStatsLine(data.items, data.itemsDone, data.totals)
|
||||
bytes.textContent = lanBytesLine(data.bytes, data.totalBytes)
|
||||
})
|
||||
let res = null
|
||||
|
|
@ -1353,9 +1380,19 @@ async function runLanSync(device, code, categories, force) {
|
|||
// 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
|
||||
// The sending machine does its hashing before anything is written here, which
|
||||
// can be minutes of complete silence - so the popup switches to "waiting"
|
||||
// instead of closing, and only a really long silence drops it. Events after
|
||||
// that reopen it (the sync is still running, the user just cannot see it).
|
||||
const RECEIVE_WATCHDOG_MS = 15 * 60 * 1000
|
||||
const RECEIVE_STALL_MS = 6 * 1000
|
||||
// phases whose wording differs on this side (everything else is neutral:
|
||||
// "Comparing X", "Updating the library", "Finishing up")
|
||||
const RECV_PHASE_KEYS = { copy: 'lansync.recvPhase.copy' }
|
||||
let receiving = false
|
||||
let receiveClosed = false
|
||||
let receiveWatchdog = null
|
||||
let receiveStall = null
|
||||
let receiveCloseTimer = null
|
||||
|
||||
function receiveModalParts() {
|
||||
|
|
@ -1378,32 +1415,47 @@ function endReceiving(message) {
|
|||
clearTimeout(receiveWatchdog)
|
||||
receiveWatchdog = null
|
||||
}
|
||||
if (receiveStall) {
|
||||
clearTimeout(receiveStall)
|
||||
receiveStall = null
|
||||
}
|
||||
if (receiveCloseTimer) {
|
||||
clearTimeout(receiveCloseTimer)
|
||||
receiveCloseTimer = null
|
||||
}
|
||||
const wasReceiving = receiving
|
||||
receiving = false
|
||||
receiveClosed = true
|
||||
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).
|
||||
// popup closes itself if nothing at all arrives for a long time (the server
|
||||
// drops the session after the same idle time). While it waits, the phase line
|
||||
// says so rather than the popup disappearing mid-transfer.
|
||||
function armReceiveWatchdog() {
|
||||
if (receiveWatchdog) clearTimeout(receiveWatchdog)
|
||||
receiveWatchdog = setTimeout(() => endReceiving(t('lansync.recvIdle')), RECEIVE_WATCHDOG_MS)
|
||||
if (receiveStall) clearTimeout(receiveStall)
|
||||
receiveStall = setTimeout(() => {
|
||||
receiveStall = null
|
||||
const parts = receiveModalParts()
|
||||
if (parts) parts.phase.textContent = t('lansync.recvStalled')
|
||||
}, RECEIVE_STALL_MS)
|
||||
}
|
||||
|
||||
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 (!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
|
||||
// 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
|
||||
// that already ended stays closed.
|
||||
if (receiveClosed && info.stage !== 'start') return
|
||||
if (qs('#modal-overlay').style.display !== 'none') return
|
||||
receiving = true
|
||||
const modal = showModal(`
|
||||
|
|
@ -1413,8 +1465,10 @@ function onLanReceiving(info) {
|
|||
<p class="subtitle" id="ls-recv-count"></p>
|
||||
<p class="subtitle" id="ls-recv-stats"></p>
|
||||
<p class="subtitle" id="ls-recv-bytes"></p>
|
||||
<p class="subtitle sync-warning" id="ls-recv-warning"></p>
|
||||
`, { locked: true })
|
||||
modal.querySelector('#ls-recv-title').textContent = t('lansync.receiving', info.from || info.deviceId || '')
|
||||
modal.querySelector('#ls-recv-warning').textContent = t('lansync.keepOpen')
|
||||
armReceiveWatchdog()
|
||||
return renderReceiving(info)
|
||||
}
|
||||
|
|
@ -1425,17 +1479,14 @@ function onLanReceiving(info) {
|
|||
function renderReceiving(info) {
|
||||
const parts = receiveModalParts()
|
||||
if (!parts) return
|
||||
const phaseKey = 'lansync.phase.' + info.stage
|
||||
// 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
|
||||
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)
|
||||
renderProgressBar(parts.bar, ...barValues(info))
|
||||
if (info.total > 0) parts.count.textContent = t('lansync.progressFiles', info.done, info.total)
|
||||
else parts.count.textContent = ''
|
||||
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 get out of the way (the library reloads
|
||||
|
|
|
|||
|
|
@ -191,6 +191,10 @@ content {
|
|||
}
|
||||
.subtitle { color: var(--darker-text-color); font-size: 0.9rem; margin-top: 0.2rem; }
|
||||
|
||||
/* "Do not close the app while this runs" - the one line on a sync popup the
|
||||
user has to notice, so it stands out from the subtitles around it. */
|
||||
.sync-warning { color: var(--big-red); margin-top: 0.6rem; }
|
||||
|
||||
.button {
|
||||
height: var(--button-height);
|
||||
padding: 0 1rem;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue