sync fixes:

- file counter
- popup text
- default settings not syncing
This commit is contained in:
olcxja 2026-10-01 20:57:45 +02:00
commit b087db1b4d
4 changed files with 69 additions and 38 deletions

View file

@ -167,6 +167,7 @@
"lansync.defaults": "Default settings (default Wine and prefix)", "lansync.defaults": "Default settings (default Wine and prefix)",
"lansync.force": "Force sync all files (skip the hash check and overwrite everything)", "lansync.force": "Force sync all files (skip the hash check and overwrite everything)",
"lansync.mirrorWarning": "This device is the source of truth: on the other device, files and items that do not exist here will be deleted.", "lansync.mirrorWarning": "This device is the source of truth: on the other device, files and items that do not exist here will be deleted.",
"lansync.incomingMirror": "The other device is the source of truth: files and items on this device that do not exist there will be deleted.",
"lansync.start": "Start sync", "lansync.start": "Start sync",
"lansync.progress": "Syncing to {0}…", "lansync.progress": "Syncing to {0}…",
"lansync.phase.scan": "Preparing {0}", "lansync.phase.scan": "Preparing {0}",

View file

@ -167,6 +167,7 @@
"lansync.defaults": "Ustawienia domyślne (domyślne Wine i prefiks)", "lansync.defaults": "Ustawienia domyślne (domyślne Wine i prefiks)",
"lansync.force": "Wymuś synchronizację wszystkich plików (pomiń sprawdzanie hashy i nadpisz wszystko)", "lansync.force": "Wymuś synchronizację wszystkich plików (pomiń sprawdzanie hashy i nadpisz wszystko)",
"lansync.mirrorWarning": "To urządzenie jest źródłem prawdy: na drugim urządzeniu pliki i elementy, których tu nie ma, zostaną usunięte.", "lansync.mirrorWarning": "To urządzenie jest źródłem prawdy: na drugim urządzeniu pliki i elementy, których tu nie ma, zostaną usunięte.",
"lansync.incomingMirror": "Drugie urządzenie jest źródłem prawdy: pliki i elementy na tym urządzeniu, których tam nie ma, zostaną usunięte.",
"lansync.start": "Rozpocznij synchronizację", "lansync.start": "Rozpocznij synchronizację",
"lansync.progress": "Synchronizowanie na {0}…", "lansync.progress": "Synchronizowanie na {0}…",
"lansync.phase.scan": "Przygotowywanie {0}", "lansync.phase.scan": "Przygotowywanie {0}",

View file

@ -1337,13 +1337,13 @@ async function runLanSync(device, code, categories, force) {
state.lanPushing = true state.lanPushing = true
window.plwl.onLanSyncProgress(data => { window.plwl.onLanSyncProgress(data => {
if (!active) return if (!active) return
// Every line keeps its value while the sync moves between phases: hashing // Every line keeps its value while the sync moves between phases: the
// reports its own file counter, copying its own, and the bar follows the // file counter is CUMULATIVE across units (it never resets to 0/N with
// bytes the whole way through // the next app) and phases that move no files simply do not touch it -
// only the byte bar follows the phase being worked on
phase.textContent = t('lansync.phase.' + data.phase, data.label || '').trim() phase.textContent = t('lansync.phase.' + data.phase, data.label || '').trim()
renderProgressBar(bar, ...barValues(data)) renderProgressBar(bar, ...barValues(data))
if (data.total > 0) count.textContent = t('lansync.progressFiles', data.done, data.total) 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) stats.textContent = lanStatsLine(data.items, data.itemsDone, data.totals)
bytes.textContent = lanBytesLine(data.bytes, data.totalBytes) bytes.textContent = lanBytesLine(data.bytes, data.totalBytes)
}) })
@ -1427,7 +1427,9 @@ function onLanIncoming(info) {
modal.querySelector('#ls-incoming-title').textContent = t('lansync.incomingTitle', name) modal.querySelector('#ls-incoming-title').textContent = t('lansync.incomingTitle', name)
modal.querySelector('#ls-incoming-text').textContent = t('lansync.incomingText', name) modal.querySelector('#ls-incoming-text').textContent = t('lansync.incomingText', name)
modal.querySelector('#ls-incoming-what').textContent = parts.length ? parts.join(' · ') : t('lansync.incomingNothing') modal.querySelector('#ls-incoming-what').textContent = parts.length ? parts.join(' · ') : t('lansync.incomingNothing')
modal.querySelector('#ls-incoming-warning').textContent = t('lansync.mirrorWarning') // the SENDER is the source of truth: the warning is phrased from this
// (receiving) side's point of view, not the send modal's mirrorWarning
modal.querySelector('#ls-incoming-warning').textContent = t('lansync.incomingMirror')
modal.querySelector('#ls-incoming-decline').textContent = t('lansync.decline') modal.querySelector('#ls-incoming-decline').textContent = t('lansync.decline')
modal.querySelector('#ls-incoming-accept').textContent = t('lansync.accept') modal.querySelector('#ls-incoming-accept').textContent = t('lansync.accept')
const answer = accept => { const answer = accept => {
@ -1563,19 +1565,24 @@ function renderReceiving(info) {
} }
// Data received from another device changed the local files: reload every // Data received from another device changed the local files: reload every
// list so the views show the merged library instead of stale state. // list so the views show the merged library instead of stale state. Settings
// too - a sync with "Default settings" rewrote the default wine/prefix on
// disk, and the settings view would keep showing the values from before.
async function reloadLocalData() { async function reloadLocalData() {
const [wines, prefixes, games] = await Promise.all([ const [wines, prefixes, games, settings] = await Promise.all([
window.plwl.winesList(), window.plwl.winesList(),
window.plwl.prefixesList(), window.plwl.prefixesList(),
window.plwl.gamesList() window.plwl.gamesList(),
window.plwl.settingsGet()
]) ])
state.wines = wines state.wines = wines
state.prefixes = prefixes state.prefixes = prefixes
state.games = games state.games = games
state.settings = settings
if (state.currentView === 'library') renderLibrary() if (state.currentView === 'library') renderLibrary()
else if (state.currentView === 'wines') renderWines() else if (state.currentView === 'wines') renderWines()
else if (state.currentView === 'prefixes') renderPrefixes() else if (state.currentView === 'prefixes') renderPrefixes()
else if (state.currentView === 'settings') renderSettings()
} }
function wireLanSync() { function wireLanSync() {

View file

@ -297,31 +297,45 @@ function createPusher(api) {
// show "1.2 GB / 3.4 GB" instead of only what has already arrived // show "1.2 GB / 3.4 GB" instead of only what has already arrived
let plannedBytes = 0 let plannedBytes = 0
let itemsSeen = 0 let itemsSeen = 0
// files uploaded/announced in PREVIOUS units: the popup's file counter is
// cumulative across the whole sync (a per-unit counter resets with every
// next app, which reads like progress going backwards)
let allDone = 0
let allTotal = 0
// the bar tracks the PHASE being worked on (its own bytes), never a // the bar tracks the PHASE being worked on (its own bytes), never a
// leftover from the phase before: hashing starts from an empty bar // leftover from the phase before: hashing starts from an empty bar
let barBytes = 0 let barBytes = 0
let barTotal = 0 let barTotal = 0
const snapshot = (phase, unit, done, total) => ({ const snapshot = (phase, unit, done, total) => {
stage: phase, const data = {
phase, stage: phase,
kind: (unit && unit.kind) || '', phase,
id: (unit && unit.id) || '', kind: (unit && unit.kind) || '',
key: (unit && unit.key) || '', id: (unit && unit.id) || '',
unit: (unit && unit.label) || '', key: (unit && unit.key) || '',
label: (unit && unit.label) || '', unit: (unit && unit.label) || '',
name: (unit && unit.label) || '', label: (unit && unit.label) || '',
done, name: (unit && unit.label) || '',
total, bytes: summary.bytes,
bytes: summary.bytes, totalBytes: plannedBytes,
totalBytes: plannedBytes, units: summary.units,
units: summary.units, unitsDone: itemsSeen,
unitsDone: itemsSeen, items: { ...summary.items },
items: { ...summary.items }, itemsDone: { ...finished },
itemsDone: { ...finished }, totals: { ...totals },
totals: { ...totals }, barBytes,
barBytes, barTotalBytes: barTotal
barTotalBytes: barTotal }
}) // only phases that actually move the file counter carry one (the copy
// phase passes absolute, cumulative values); the rest omit it, so the
// popup freezes the last count instead of jumping backwards while this
// unit is still being scanned or compared
if (Number.isFinite(done) && Number.isFinite(total)) {
data.done = done
data.total = total
}
return data
}
// hashing and copying walk thousands of files: the state is snapshotted on // hashing and copying walk thousands of files: the state is snapshotted on
// every file but forwarded a few times per second (phase changes always // every file but forwarded a few times per second (phase changes always
// go through, so nothing on screen ever jumps backwards) // go through, so nothing on screen ever jumps backwards)
@ -342,13 +356,15 @@ function createPusher(api) {
itemsSeen += 1 itemsSeen += 1
barBytes = 0 barBytes = 0
barTotal = 0 barTotal = 0
emit('scan', unit, 0, 0, true) emit('scan', unit, null, null, true)
const entries = await buildManifest(roots, !!force, (done, total, hashedBytes, totalBytes) => { const entries = await buildManifest(roots, !!force, (done, total, hashedBytes, totalBytes) => {
barBytes = hashedBytes barBytes = hashedBytes
barTotal = totalBytes barTotal = totalBytes
emit('hash', unit, done, total) // the file counter is not touched here: until the plan comes back
// nobody knows how many of this unit's files will actually move
emit('hash', unit, null, null)
}) })
emit('plan', unit, 0, 0, true) emit('plan', unit, null, null, true)
const plan = await planUnit(device, token, { ...unit, roots }, entries, !!force, { totals }) const plan = await planUnit(device, token, { ...unit, roots }, entries, !!force, { totals })
if (!plan.data || !plan.data.ok) throw new Error((plan.data && plan.data.error) || `http-${plan.status}`) if (!plan.data || !plan.data.ok) throw new Error((plan.data && plan.data.error) || `http-${plan.status}`)
const upload = new Set(plan.data.upload || []) const upload = new Set(plan.data.upload || [])
@ -365,6 +381,7 @@ function createPusher(api) {
pending.push(entry) pending.push(entry)
plannedBytes += entry.size || 0 plannedBytes += entry.size || 0
} }
allTotal += pending.length
// a few files in flight at once: with thousands of small ones the // a few files in flight at once: with thousands of small ones the
// round trip to the other machine costs more than the bytes do // round trip to the other machine costs more than the bytes do
// the files that were already identical never move, so the bar counts // the files that were already identical never move, so the bar counts
@ -372,7 +389,7 @@ function createPusher(api) {
if (pending.length) { if (pending.length) {
barBytes = 0 barBytes = 0
barTotal = pending.reduce((sum, entry) => sum + (entry.size || 0), 0) barTotal = pending.reduce((sum, entry) => sum + (entry.size || 0), 0)
emit('copy', unit, 0, pending.length, true) emit('copy', unit, allDone, allTotal + pending.length, true)
} }
let failed = null let failed = null
let done = 0 let done = 0
@ -387,12 +404,17 @@ function createPusher(api) {
summary.uploaded++ summary.uploaded++
summary.bytes += entry.size || 0 summary.bytes += entry.size || 0
barBytes += entry.size || 0 barBytes += entry.size || 0
emit('copy', unit, done, pending.length) emit('copy', unit, allDone + done, allTotal + pending.length)
} }
if (typeof api.mapLimit === 'function') await api.mapLimit(pending, UPLOAD_CONCURRENCY, send) if (typeof api.mapLimit === 'function') await api.mapLimit(pending, UPLOAD_CONCURRENCY, send)
else for (const entry of pending) await send(entry) else for (const entry of pending) await send(entry)
if (failed) throw failed if (failed) throw failed
emit('copy', unit, done, pending.length, true) emit('copy', unit, allDone + done, allTotal + pending.length, true)
// this unit's files count towards the running totals only once they
// are really on the wire - the counter holds there while the receiver
// prunes and the next unit is scanned/compared
allDone += pending.length
allTotal += pending.length
const commit = await httpJson(device, 'POST', `/plwl/unit/commit?token=${encodeURIComponent(token)}&unit=${encodeURIComponent(unit.key)}`, { json: {} }) const commit = await httpJson(device, 'POST', `/plwl/unit/commit?token=${encodeURIComponent(token)}&unit=${encodeURIComponent(unit.key)}`, { json: {} })
if (commit.data && commit.data.deleted) { if (commit.data && commit.data.deleted) {
summary.deleted += commit.data.deleted summary.deleted += commit.data.deleted
@ -402,9 +424,9 @@ function createPusher(api) {
// a finished unit leaves a finished bar behind, even one that sent none // a finished unit leaves a finished bar behind, even one that sent none
barTotal = barTotal || 1 barTotal = barTotal || 1
barBytes = barTotal barBytes = barTotal
emit('commit', unit, 0, 0, true) emit('commit', unit, null, null, true)
} }
emit('registry', null, 0, 0, true) emit('registry', null, null, null, true)
const applied = await httpJson(device, 'POST', `/plwl/apply?token=${encodeURIComponent(token)}`, { json: buildRegistryPayload([...cats]), timeout: 60000 }) const applied = await httpJson(device, 'POST', `/plwl/apply?token=${encodeURIComponent(token)}`, { json: buildRegistryPayload([...cats]), timeout: 60000 })
if (!applied.data || !applied.data.ok) throw new Error((applied.data && applied.data.error) || `http-${applied.status}`) if (!applied.data || !applied.data.ok) throw new Error((applied.data && applied.data.error) || `http-${applied.status}`)
for (const key of ['apps', 'wines', 'prefixes']) { for (const key of ['apps', 'wines', 'prefixes']) {
@ -412,7 +434,7 @@ function createPusher(api) {
if (deleted) summary[key].deleted += deleted if (deleted) summary[key].deleted += deleted
} }
if (applied.data.deleted) summary.deleted += applied.data.deleted if (applied.data.deleted) summary.deleted += applied.data.deleted
emit('done', null, summary.uploaded, summary.uploaded, true) emit('done', null, allDone, allTotal, true)
await end() await end()
} catch (err) { } catch (err) {
await end() await end()