[TEST] add data sync
This commit is contained in:
parent
944d8f86c2
commit
8e3512b5cd
16 changed files with 1935 additions and 5 deletions
206
renderer/app.js
206
renderer/app.js
|
|
@ -14,6 +14,7 @@ const state = {
|
|||
prefixes: { default: '', prefixes: [] },
|
||||
games: [],
|
||||
currentGame: null,
|
||||
lansync: null,
|
||||
currentView: 'library'
|
||||
}
|
||||
|
||||
|
|
@ -33,6 +34,8 @@ const ICONS = {
|
|||
// wine glass - wine/proton runners
|
||||
wines: () => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 22h8"/><path d="M12 15v7"/><path d="M7 10h10"/><path d="M12 15a5 5 0 0 0 5-5c0-2-.5-4-2-8H9c-1.5 4-2 6-2 8a5 5 0 0 0 5 5Z"/></svg>',
|
||||
settings: () => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19 12a7 7 0 0 0-.1-1.2l2-1.5-2-3.5-2.4 1a7 7 0 0 0-2-1.2L14 3h-4l-.5 2.6a7 7 0 0 0-2 1.2l-2.4-1-2 3.5 2 1.5a7 7 0 0 0 0 2.4l-2 1.5 2 3.5 2.4-1a7 7 0 0 0 2 1.2L10 21h4l.5-2.6a7 7 0 0 0 2-1.2l2.4 1 2-3.5-2-1.5c.06-.4.1-.8.1-1.2z"/></svg>',
|
||||
// nodes connected to each other - devices on the local network
|
||||
lansync: () => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="4.5" r="2.5"/><circle cx="5" cy="19" r="2.5"/><circle cx="19" cy="19" r="2.5"/><path d="M10.6 6.8 6.5 16.7M13.4 6.8l4.1 9.9M7.5 19h9"/></svg>',
|
||||
// arrow into a tray - import
|
||||
config: () => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M1 14h6M9 8h6M17 16h6"/></svg>',
|
||||
folder: () => '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>',
|
||||
|
|
@ -62,7 +65,8 @@ function setButtonIcons() {
|
|||
'nav-library': ICONS.library(),
|
||||
'nav-prefixes': ICONS.prefixes(),
|
||||
'nav-wines': ICONS.wines(),
|
||||
'nav-settings': ICONS.settings()
|
||||
'nav-settings': ICONS.settings(),
|
||||
'nav-lansync': ICONS.lansync()
|
||||
}
|
||||
for (const [id, svg] of Object.entries(map)) {
|
||||
const el = document.getElementById(id)
|
||||
|
|
@ -322,6 +326,7 @@ function showView(name) {
|
|||
if (name === 'prefixes') renderPrefixes()
|
||||
if (name === 'wines') renderWines()
|
||||
if (name === 'settings') renderSettings()
|
||||
if (name === 'lansync') renderLanSync()
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
|
|
@ -1123,6 +1128,204 @@ async function runCustomTool() {
|
|||
else toast(t('toast.startFailed', tool, res.error || ''), 'error')
|
||||
}
|
||||
|
||||
// --- LAN syncing ------------------------------------------------------------
|
||||
|
||||
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' }
|
||||
]
|
||||
|
||||
async function renderLanSync() {
|
||||
const info = await window.plwl.lansyncGet()
|
||||
state.lansync = info
|
||||
qs('#ls-device-id').textContent = info.deviceId
|
||||
qs('#ls-sync-code').textContent = info.syncCode
|
||||
renderLanDevices(info.devices || [])
|
||||
}
|
||||
|
||||
function renderLanDevices(devices) {
|
||||
const list = qs('#ls-device-list')
|
||||
list.innerHTML = ''
|
||||
if (!devices.length) {
|
||||
const empty = document.createElement('p')
|
||||
empty.className = 'subtitle'
|
||||
empty.textContent = t('lansync.empty')
|
||||
list.appendChild(empty)
|
||||
return
|
||||
}
|
||||
for (const device of devices) {
|
||||
const row = document.createElement('div')
|
||||
row.className = 'list-row'
|
||||
row.innerHTML = `
|
||||
<div class="grow">
|
||||
<div class="title"></div>
|
||||
<div class="sub"></div>
|
||||
</div>
|
||||
${device.paired ? `<span class="badge primary">${t('lansync.paired')}</span>` : ''}
|
||||
<div class="actions">
|
||||
<button class="button small primary" data-act="sync"></button>
|
||||
</div>
|
||||
`
|
||||
row.querySelector('.title').textContent = device.name
|
||||
const sub = row.querySelector('.sub')
|
||||
sub.textContent = device.id
|
||||
sub.title = device.id
|
||||
const sync = row.querySelector('[data-act=sync]')
|
||||
sync.textContent = t('lansync.sync')
|
||||
sync.onclick = () => openLanSyncModal(device, '')
|
||||
list.appendChild(row)
|
||||
}
|
||||
}
|
||||
|
||||
// Modal that collects the other device's sync code plus what to push. Shown
|
||||
// again with a cleared code after a rejected one, so the user can retype it.
|
||||
async function openLanSyncModal(device, errorKey) {
|
||||
const modal = showModal(`
|
||||
<h2 id="ls-modal-title"></h2>
|
||||
<label class="field-label" id="ls-code-label"></label>
|
||||
<input type="text" id="ls-code" maxlength="8" autocomplete="off" spellcheck="false">
|
||||
<p class="subtitle" id="ls-code-error" style="display:none"></p>
|
||||
<label class="field-label" id="ls-what-label"></label>
|
||||
<div class="check-group" id="ls-categories"></div>
|
||||
<label class="check-label">
|
||||
<input type="checkbox" id="ls-force">
|
||||
<span id="ls-force-label"></span>
|
||||
</label>
|
||||
<p class="subtitle" id="ls-warning"></p>
|
||||
<div class="modal-actions">
|
||||
<button class="button" id="ls-cancel"></button>
|
||||
<button class="button primary" id="ls-start"></button>
|
||||
</div>
|
||||
`)
|
||||
modal.querySelector('#ls-modal-title').textContent = t('lansync.syncTitle', device.name)
|
||||
modal.querySelector('#ls-code-label').textContent = t('lansync.enterCode', device.name)
|
||||
const code = modal.querySelector('#ls-code')
|
||||
code.value = device.code || ''
|
||||
const error = modal.querySelector('#ls-code-error')
|
||||
if (errorKey) {
|
||||
error.textContent = t(errorKey)
|
||||
error.style.display = ''
|
||||
code.focus()
|
||||
}
|
||||
modal.querySelector('#ls-what-label').textContent = t('lansync.what')
|
||||
const box = modal.querySelector('#ls-categories')
|
||||
for (const category of LAN_CATEGORIES) {
|
||||
const label = document.createElement('label')
|
||||
label.className = 'check-label'
|
||||
label.innerHTML = '<input type="checkbox" checked><span></span>'
|
||||
label.querySelector('input').id = 'ls-cat-' + category.id
|
||||
label.querySelector('span').textContent = t(category.label)
|
||||
box.appendChild(label)
|
||||
}
|
||||
modal.querySelector('#ls-force-label').textContent = t('lansync.force')
|
||||
modal.querySelector('#ls-warning').textContent = t('lansync.mirrorWarning')
|
||||
modal.querySelector('#ls-cancel').textContent = t('dialog.cancel')
|
||||
modal.querySelector('#ls-cancel').onclick = closeModal
|
||||
const start = modal.querySelector('#ls-start')
|
||||
start.textContent = t('lansync.start')
|
||||
start.onclick = async () => {
|
||||
const value = code.value.trim().toUpperCase()
|
||||
if (value.length !== 8) {
|
||||
error.textContent = t('lansync.badCode')
|
||||
error.style.display = ''
|
||||
code.focus()
|
||||
return
|
||||
}
|
||||
const categories = LAN_CATEGORIES.map(c => c.id).filter(id => modal.querySelector('#ls-cat-' + id).checked)
|
||||
if (!categories.length) {
|
||||
toast(t('lansync.noneSelected'), 'error')
|
||||
return
|
||||
}
|
||||
await runLanSync(device, value, categories, modal.querySelector('#ls-force').checked)
|
||||
}
|
||||
}
|
||||
|
||||
async function runLanSync(device, code, categories, force) {
|
||||
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>
|
||||
`)
|
||||
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')
|
||||
let active = 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 = ''
|
||||
}
|
||||
})
|
||||
const res = await window.plwl.lansyncSync({ deviceId: device.id, code, categories, force })
|
||||
active = 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')
|
||||
await renderLanSync()
|
||||
return
|
||||
}
|
||||
const err = (res && res.error) || ''
|
||||
if (err === 'bad-code') {
|
||||
await window.plwl.lansyncForget(device.id)
|
||||
toast(t('lansync.badCode'), 'error')
|
||||
openLanSyncModal({ ...device, code: '' }, 'lansync.badCode')
|
||||
return
|
||||
}
|
||||
const message = err === 'busy' ? t('lansync.busy') : err === 'offline' ? t('lansync.offline') : (err || t('lansync.unknownError'))
|
||||
toast(t('lansync.failed', message), 'error')
|
||||
}
|
||||
|
||||
// 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() {
|
||||
const [wines, prefixes, games] = await Promise.all([
|
||||
window.plwl.winesList(),
|
||||
window.plwl.prefixesList(),
|
||||
window.plwl.gamesList()
|
||||
])
|
||||
state.wines = wines
|
||||
state.prefixes = prefixes
|
||||
state.games = games
|
||||
if (state.currentView === 'library') renderLibrary()
|
||||
else if (state.currentView === 'wines') renderWines()
|
||||
else if (state.currentView === 'prefixes') renderPrefixes()
|
||||
}
|
||||
|
||||
function wireLanSync() {
|
||||
window.plwl.onLanSyncDevices(devices => {
|
||||
if (state.lansync) state.lansync.devices = devices
|
||||
if (state.currentView === 'lansync') renderLanDevices(devices)
|
||||
})
|
||||
window.plwl.onLanSyncApplied(async info => {
|
||||
toast(t('lansync.applied', info.from || '', info.files || 0), 'success')
|
||||
await reloadLocalData()
|
||||
})
|
||||
qs('#ls-reroll-id').addEventListener('click', async () => {
|
||||
const res = await window.plwl.lansyncRerollDeviceId()
|
||||
qs('#ls-device-id').textContent = res.deviceId
|
||||
if (state.lansync) state.lansync.deviceId = res.deviceId
|
||||
toast(t('lansync.idRerolled'), 'success')
|
||||
})
|
||||
qs('#ls-reroll-code').addEventListener('click', async () => {
|
||||
const res = await window.plwl.lansyncRerollSyncCode()
|
||||
qs('#ls-sync-code').textContent = res.syncCode
|
||||
if (state.lansync) state.lansync.syncCode = res.syncCode
|
||||
toast(t('lansync.codeRerolled'), 'success')
|
||||
})
|
||||
}
|
||||
|
||||
function wireEvents() {
|
||||
qsa('[data-nav]').forEach(btn => {
|
||||
btn.addEventListener('click', () => showView(btn.dataset.nav))
|
||||
|
|
@ -1233,6 +1436,7 @@ async function boot() {
|
|||
wireEvents()
|
||||
wireRealtime()
|
||||
wireOpenWith()
|
||||
wireLanSync()
|
||||
qs('#loading').remove()
|
||||
qs('#main').style.display = 'flex'
|
||||
showView('library')
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@
|
|||
<indicator></indicator>
|
||||
<button class="icon-button" data-nav="settings"></button>
|
||||
</sidebarelement>
|
||||
<sidebarelement id="nav-lansync">
|
||||
<indicator></indicator>
|
||||
<button class="icon-button" data-nav="lansync"></button>
|
||||
</sidebarelement>
|
||||
</div>
|
||||
</sidebar>
|
||||
|
||||
|
|
@ -101,6 +105,34 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section id="view-lansync" class="view">
|
||||
<div class="view-header">
|
||||
<div>
|
||||
<h1 data-t="lansync.title"></h1>
|
||||
<p class="subtitle" data-t="lansync.subtitle"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-card">
|
||||
<h2 data-t="lansync.thisDevice"></h2>
|
||||
<label class="field-label" data-t="lansync.deviceId"></label>
|
||||
<div class="code-row">
|
||||
<code id="ls-device-id"></code>
|
||||
<button class="button small" id="ls-reroll-id" data-t="lansync.reroll"></button>
|
||||
</div>
|
||||
<label class="field-label" data-t="lansync.syncCode"></label>
|
||||
<div class="code-row">
|
||||
<code id="ls-sync-code"></code>
|
||||
<button class="button small" id="ls-reroll-code" data-t="lansync.reroll"></button>
|
||||
</div>
|
||||
<p class="subtitle" data-t="lansync.codeHint"></p>
|
||||
</div>
|
||||
<div class="form-card">
|
||||
<h2 data-t="lansync.devices"></h2>
|
||||
<p class="subtitle" data-t="lansync.devicesHint"></p>
|
||||
<div id="ls-device-list" class="list"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="view-gameconfig" class="view">
|
||||
<div class="view-header">
|
||||
<div>
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.3 KiB |
|
|
@ -445,6 +445,29 @@ select {
|
|||
color: var(--darker-text-color);
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
/* LAN syncing: generated id/code, shown as read-only machine values */
|
||||
.code-row { display: flex; gap: 0.6rem; align-items: center; margin: var(--button-margin) 0; }
|
||||
.code-row code {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: var(--button-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0.8rem;
|
||||
border-radius: 0.8rem;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border: var(--border-width) solid var(--light-border-color);
|
||||
font-family: ui-monospace, 'DejaVu Sans Mono', monospace;
|
||||
letter-spacing: 0.08em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
user-select: text;
|
||||
}
|
||||
.code-row #ls-sync-code { font-size: 1.25rem; font-weight: 700; letter-spacing: 0.3em; }
|
||||
.check-group { display: flex; flex-direction: column; }
|
||||
.check-group .check-label { margin-top: 0.2rem; margin-bottom: 0.2rem; }
|
||||
|
||||
.path-row { display: flex; gap: 0.6rem; align-items: center; }
|
||||
.path-row input { flex: 1; min-width: 0; }
|
||||
.path-row .custom-select { flex: 1; margin: var(--button-margin) 0; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue