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.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.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.progress": "Syncing to {0}…",
"lansync.phase.scan": "Preparing {0}",

View file

@ -167,6 +167,7 @@
"lansync.defaults": "Ustawienia domyślne (domyślne Wine i prefiks)",
"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.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.progress": "Synchronizowanie na {0}…",
"lansync.phase.scan": "Przygotowywanie {0}",

View file

@ -1337,13 +1337,13 @@ async function runLanSync(device, code, categories, force) {
state.lanPushing = true
window.plwl.onLanSyncProgress(data => {
if (!active) return
// 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
// Every line keeps its value while the sync moves between phases: the
// file counter is CUMULATIVE across units (it never resets to 0/N with
// 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()
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)
})
@ -1427,7 +1427,9 @@ function onLanIncoming(info) {
modal.querySelector('#ls-incoming-title').textContent = t('lansync.incomingTitle', 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-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-accept').textContent = t('lansync.accept')
const answer = accept => {
@ -1563,19 +1565,24 @@ function renderReceiving(info) {
}
// 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() {
const [wines, prefixes, games] = await Promise.all([
const [wines, prefixes, games, settings] = await Promise.all([
window.plwl.winesList(),
window.plwl.prefixesList(),
window.plwl.gamesList()
window.plwl.gamesList(),
window.plwl.settingsGet()
])
state.wines = wines
state.prefixes = prefixes
state.games = games
state.settings = settings
if (state.currentView === 'library') renderLibrary()
else if (state.currentView === 'wines') renderWines()
else if (state.currentView === 'prefixes') renderPrefixes()
else if (state.currentView === 'settings') renderSettings()
}
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
let plannedBytes = 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
// leftover from the phase before: hashing starts from an empty bar
let barBytes = 0
let barTotal = 0
const snapshot = (phase, unit, done, total) => ({
stage: phase,
phase,
kind: (unit && unit.kind) || '',
id: (unit && unit.id) || '',
key: (unit && unit.key) || '',
unit: (unit && unit.label) || '',
label: (unit && unit.label) || '',
name: (unit && unit.label) || '',
done,
total,
bytes: summary.bytes,
totalBytes: plannedBytes,
units: summary.units,
unitsDone: itemsSeen,
items: { ...summary.items },
itemsDone: { ...finished },
totals: { ...totals },
barBytes,
barTotalBytes: barTotal
})
const snapshot = (phase, unit, done, total) => {
const data = {
stage: phase,
phase,
kind: (unit && unit.kind) || '',
id: (unit && unit.id) || '',
key: (unit && unit.key) || '',
unit: (unit && unit.label) || '',
label: (unit && unit.label) || '',
name: (unit && unit.label) || '',
bytes: summary.bytes,
totalBytes: plannedBytes,
units: summary.units,
unitsDone: itemsSeen,
items: { ...summary.items },
itemsDone: { ...finished },
totals: { ...totals },
barBytes,
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
// every file but forwarded a few times per second (phase changes always
// go through, so nothing on screen ever jumps backwards)
@ -342,13 +356,15 @@ function createPusher(api) {
itemsSeen += 1
barBytes = 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) => {
barBytes = hashedBytes
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 })
if (!plan.data || !plan.data.ok) throw new Error((plan.data && plan.data.error) || `http-${plan.status}`)
const upload = new Set(plan.data.upload || [])
@ -365,6 +381,7 @@ function createPusher(api) {
pending.push(entry)
plannedBytes += entry.size || 0
}
allTotal += pending.length
// 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
// the files that were already identical never move, so the bar counts
@ -372,7 +389,7 @@ function createPusher(api) {
if (pending.length) {
barBytes = 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 done = 0
@ -387,12 +404,17 @@ function createPusher(api) {
summary.uploaded++
summary.bytes += 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)
else for (const entry of pending) await send(entry)
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: {} })
if (commit.data && 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
barTotal = barTotal || 1
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 })
if (!applied.data || !applied.data.ok) throw new Error((applied.data && applied.data.error) || `http-${applied.status}`)
for (const key of ['apps', 'wines', 'prefixes']) {
@ -412,7 +434,7 @@ function createPusher(api) {
if (deleted) summary[key].deleted += 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()
} catch (err) {
await end()