feat: responsive pause/stop, non-blocking update with progress bar, delete saved sessions (1.5.0)
CI / Format and clippy (push) Successful in 25s
Release / Create release (push) Successful in 13s
CI / Dependency audit (push) Successful in 11s
CI / Check desktop app (Linux) (push) Successful in 1m35s
Release / CLI (linux-x86_64) (push) Successful in 2m31s
Release / CLI (linux-aarch64) (push) Successful in 1m46s
CI / Tests (Linux) (push) Successful in 4m28s
Release / Desktop app (linux-x86_64) (push) Successful in 6m9s
Release / Desktop app (windows) (push) Successful in 4m13s
Release / Desktop app (linux-aarch64) (push) Successful in 9m51s
CI / Format and clippy (push) Successful in 25s
Release / Create release (push) Successful in 13s
CI / Dependency audit (push) Successful in 11s
CI / Check desktop app (Linux) (push) Successful in 1m35s
Release / CLI (linux-x86_64) (push) Successful in 2m31s
Release / CLI (linux-aarch64) (push) Successful in 1m46s
CI / Tests (Linux) (push) Successful in 4m28s
Release / Desktop app (linux-x86_64) (push) Successful in 6m9s
Release / Desktop app (windows) (push) Successful in 4m13s
Release / Desktop app (linux-aarch64) (push) Successful in 9m51s
- carve: check cancellation inside chunks and per hit; reassembly stage is cancellable - status exposes `stopping`; UI shows "Pausing…" immediately - Tauri update commands run off the UI thread; download percentage reported - delete autosaved sessions manually (one/all) and by retention setting - chore(release): 1.5.0 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
db5b972650
commit
0b9a16e8a0
20 files changed
+262
-35
No files matched your search
@@ -542,3 +542,8 @@ dialog.lightbox[open] { display: grid; }
|
||||
.update-card .upd-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.update-card .sub { margin: 0; }
|
||||
pre.upd-notes { max-height: 50vh; overflow: auto; white-space: pre-wrap; font-size: 13px; }
|
||||
.update-card .upd-bar { margin-top: 10px; }
|
||||
|
||||
.list li.has-del { position: relative; }
|
||||
.list li.has-del .del { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); opacity: 0; }
|
||||
.list li.has-del:hover .del, .list li.has-del .del:focus-visible { opacity: 1; }
|
||||
+50
-10
@@ -53,7 +53,7 @@ document.querySelectorAll('[data-theme-pick]').forEach((b) => (b.onclick = async
|
||||
|
||||
// ------------------------------------------------------------------ settings
|
||||
// (declared before use: `S` is defined further below, so the object lives here)
|
||||
const SETTINGS = { theme: 'auto', lang: 'auto', ui_lang: null, notify: true, maps: false, updates: true };
|
||||
const SETTINGS = { theme: 'auto', lang: 'auto', ui_lang: null, notify: true, maps: false, updates: true, session_retention_days: 0 };
|
||||
async function saveSettings(patch) {
|
||||
Object.assign(SETTINGS, patch);
|
||||
try { await rpc('settings_set', SETTINGS); } catch { /* continue with local storage only */ }
|
||||
@@ -201,11 +201,36 @@ function renderSide() {
|
||||
const rl = $('#recentList');
|
||||
rl.dataset.empty = 'Nada salvo ainda';
|
||||
rl.innerHTML = (S.recent || []).map((r, i) => `
|
||||
<li><button class="dev" data-r="${i}" title="${esc(r.path)}"><span class="ico">${icon('default')}</span>
|
||||
<span><span class="t">${esc((r.target || '').split(/[\\/]/).pop() || 'Sessão')}</span><span class="s">${r.found.toLocaleString(LOC())} arquivos · ${esc(r.status)} · ${new Date(r.saved_at * 1000).toLocaleString(LOC(), { dateStyle: 'short', timeStyle: 'short' })}</span></span></button></li>`).join('');
|
||||
<li class="has-del"><button class="dev" data-r="${i}" title="${esc(r.path)}"><span class="ico">${icon('default')}</span>
|
||||
<span><span class="t">${esc((r.target || '').split(/[\\/]/).pop() || 'Sessão')}</span><span class="s">${r.found.toLocaleString(LOC())} arquivos · ${esc(r.status)} · ${new Date(r.saved_at * 1000).toLocaleString(LOC(), { dateStyle: 'short', timeStyle: 'short' })}</span></span></button><button class="icon-btn small del" data-del="${i}" title="Apagar esta sessão" aria-label="Apagar esta sessão"><svg viewBox="0 0 24 24" width="14" height="14"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg></button></li>`).join('');
|
||||
rl.querySelectorAll('.dev').forEach((b) => (b.onclick = () => openRecent(S.recent[+b.dataset.r])));
|
||||
rl.querySelectorAll('.del').forEach((b) => (b.onclick = (e) => { e.stopPropagation(); deleteRecent(S.recent[+b.dataset.del]); }));
|
||||
}
|
||||
|
||||
function confirmDlg({ title, text, ok }) {
|
||||
return new Promise((resolve) => {
|
||||
const d = $('#dlg');
|
||||
d.innerHTML = `<form method="dialog"><h2>${esc(title)}</h2><p>${text}</p><menu><button class="btn" value="cancel">Cancelar</button><button class="btn danger" value="ok">${esc(ok)}</button></menu></form>`;
|
||||
d.onclose = () => resolve(d.returnValue === 'ok');
|
||||
d.showModal();
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteRecent(r) {
|
||||
if (!r) return;
|
||||
const name = (r.target || '').split(/[\\/]/).pop() || 'Sessão';
|
||||
if (!await confirmDlg({ title: 'Apagar sessão salva', text: `A sessão de <code>${esc(name)}</code> será apagada de vez. Os arquivos do disco não são afetados.`, ok: 'Apagar' })) return;
|
||||
try { await rpc('session_delete', { path: r.path }); toast('Sessão apagada'); } catch (e) { toast(e.message); }
|
||||
loadRecent();
|
||||
}
|
||||
|
||||
$('#clearRecentBtn')?.addEventListener('click', async () => {
|
||||
if (!(S.recent || []).length) { toast('Nada para apagar'); return; }
|
||||
if (!await confirmDlg({ title: 'Apagar todas as sessões salvas', text: 'Todas as sessões salvas sozinhas serão apagadas de vez. Sessões abertas agora são mantidas.', ok: 'Apagar tudo' })) return;
|
||||
try { const r = await rpc('sessions_prune', { all: true }); toast(`Sessões apagadas: ${r.removed}`); } catch (e) { toast(e.message); }
|
||||
loadRecent();
|
||||
});
|
||||
|
||||
async function loadRecent() {
|
||||
try { S.recent = await rpc('sessions_list'); } catch { S.recent = []; }
|
||||
renderSide();
|
||||
@@ -590,7 +615,9 @@ function paintStatus() {
|
||||
const rate = p.total && st.elapsed > 1 && p.done ? p.done / st.elapsed : 0;
|
||||
const eta = rate && running ? dur((p.total - p.done) / rate) : null;
|
||||
const enr = st.enriching ? `<span class="sub">Lendo datas e metadados das fotos… ${st.enriching.done.toLocaleString(LOC())}/${st.enriching.total.toLocaleString(LOC())}</span>` : '';
|
||||
const buttons = running ? '<button class="btn sm" data-act="pause">Pausar</button><button class="btn sm danger" data-act="stop">Parar</button>'
|
||||
const stopping = running && st.stopping;
|
||||
const buttons = stopping ? '<button class="btn sm" disabled>Pausando…</button>'
|
||||
: running ? '<button class="btn sm" data-act="pause">Pausar</button><button class="btn sm danger" data-act="stop">Parar</button>'
|
||||
: st.status === 'paused' ? `<button class="btn sm primary" data-act="${st.source === false ? 'reconnect' : 'resume'}">${st.source === false ? 'Reconectar o disco' : 'Continuar'}</button><button class="btn sm" data-act="relink">Escolher a origem…</button>` : '';
|
||||
const relink = st.source === false ? '<button class="btn sm primary" data-act="relink">Escolher a origem…</button>' : '';
|
||||
const tools = relink + (!running && st.found ? '<button class="btn sm" data-act="csv">Exportar CSV</button><button class="btn sm" data-act="report">Relatório</button><button class="btn sm" data-act="save">Salvar sessão…</button>' : '');
|
||||
@@ -651,8 +678,8 @@ async function chooseSource() {
|
||||
async function onProgressClick(e) {
|
||||
const act = e.target.closest('[data-act]')?.dataset.act;
|
||||
if (!act) return;
|
||||
if (act === 'pause') { await rpc('scan_pause', { job: S.job }); toast('Pausando…'); }
|
||||
else if (act === 'stop') await rpc('scan_stop', { job: S.job });
|
||||
if (act === 'pause') { await rpc('scan_pause', { job: S.job }); toast('Pausando…'); tick(); }
|
||||
else if (act === 'stop') { await rpc('scan_stop', { job: S.job }); tick(); }
|
||||
else if (act === 'resume') {
|
||||
try { await rpc('scan_resume', { job: S.job }); stopPoll(); S.poll = setInterval(tick, 500); tick(); }
|
||||
catch (err) { toast(err.message); chooseSource(); }
|
||||
@@ -753,9 +780,11 @@ $('#settingsBtn')?.addEventListener('click', () => {
|
||||
<label class="fld"><span>Idioma</span><select class="select" name="lang"><option value="auto">Auto</option><option value="pt">Português</option><option value="en">English</option></select></label>
|
||||
<label class="check"><input type="checkbox" name="notify" ${SETTINGS.notify ? 'checked' : ''}><span><b>Notificações</b><br><span class="sub">Avisar quando uma tarefa longa terminar</span></span></label>
|
||||
<label class="check"><input type="checkbox" name="maps" ${SETTINGS.maps ? 'checked' : ''}><span><b>Mapas online</b><br><span class="sub">Mostrar mapas das fotos com GPS (usa a internet: baixa blocos do OpenStreetMap)</span></span></label>
|
||||
<label class="fld"><span>Apagar sessões salvas sozinhas após</span><select class="select" name="retention"><option value="0">Nunca</option><option value="7">7 dias</option><option value="30">30 dias</option><option value="90">90 dias</option></select></label>
|
||||
${tauri?.core?.invoke ? `<label class="check"><input type="checkbox" name="updates" ${SETTINGS.updates ? 'checked' : ''}><span><b>Atualizações automáticas</b><br><span class="sub">Ao abrir, consulta o Gitea para ver se há versão nova (uma consulta, sem enviar dados seus)</span><br><button type="button" class="btn sm" id="updNow">Verificar agora</button></span></label>` : ''}
|
||||
<menu><button class="btn" value="cancel">Cancelar</button><button class="btn primary" value="ok">Salvar</button></menu></form>`;
|
||||
d.querySelector('[name=lang]').value = SETTINGS.lang;
|
||||
d.querySelector('[name=retention]').value = String([0, 7, 30, 90].includes(SETTINGS.session_retention_days) ? SETTINGS.session_retention_days : 0);
|
||||
d.querySelector('#updNow')?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (UPD) applyUpdate();
|
||||
@@ -763,11 +792,11 @@ $('#settingsBtn')?.addEventListener('click', () => {
|
||||
});
|
||||
d.onclose = () => {
|
||||
if (d.returnValue !== 'ok') return;
|
||||
const patch = { lang: d.querySelector('[name=lang]').value, notify: d.querySelector('[name=notify]').checked, maps: d.querySelector('[name=maps]').checked };
|
||||
const patch = { lang: d.querySelector('[name=lang]').value, notify: d.querySelector('[name=notify]').checked, maps: d.querySelector('[name=maps]').checked, session_retention_days: +d.querySelector('[name=retention]').value };
|
||||
if (d.querySelector('[name=updates]')) patch.updates = d.querySelector('[name=updates]').checked;
|
||||
setLang(patch.lang);
|
||||
patch.ui_lang = currentLang();
|
||||
saveSettings(patch);
|
||||
saveSettings(patch).then(() => rpc('sessions_prune', {}).then(loadRecent).catch(() => {}));
|
||||
EX?.relang?.();
|
||||
toast('Salvo');
|
||||
};
|
||||
@@ -785,7 +814,15 @@ async function applyUpdate() {
|
||||
updating = true;
|
||||
const updApply = $('#updApply');
|
||||
const updNow = $('#updNow');
|
||||
const setPhase = (txt) => {
|
||||
const setPhase = (txt, pct) => {
|
||||
const card = $('#updateCard');
|
||||
if (card) {
|
||||
let bar = card.querySelector('.upd-bar');
|
||||
if (!bar) { bar = document.createElement('div'); bar.className = 'bar upd-bar'; bar.innerHTML = '<i></i>'; card.appendChild(bar); }
|
||||
bar.classList.toggle('indet', pct == null);
|
||||
bar.firstChild.style.width = pct == null ? '35%' : `${pct}%`;
|
||||
}
|
||||
if (pct != null) txt = `${txt} ${pct}%`;
|
||||
if (updApply) { updApply.disabled = true; updApply.textContent = txt; }
|
||||
if (updNow) { updNow.disabled = true; updNow.textContent = txt; }
|
||||
};
|
||||
@@ -796,7 +833,8 @@ async function applyUpdate() {
|
||||
try {
|
||||
unlisten = await tauri.event.listen('update-progress', (event) => {
|
||||
const phase = event.payload?.phase;
|
||||
if (phase === 'downloading') setPhase('Baixando a atualização…');
|
||||
const pct = event.payload?.percent;
|
||||
if (phase === 'downloading') setPhase('Baixando a atualização…', pct);
|
||||
else if (phase === 'verifying') setPhase('Verificando integridade…');
|
||||
else if (phase === 'installing') setPhase('Instalando a atualização…');
|
||||
else if (phase === 'restarting') setPhase('Reiniciando…');
|
||||
@@ -809,6 +847,7 @@ async function applyUpdate() {
|
||||
setPhase('Atualização concluída');
|
||||
} catch (e) {
|
||||
updating = false;
|
||||
$('#updateCard .upd-bar')?.remove();
|
||||
if (updApply) { updApply.disabled = false; updApply.textContent = 'Atualizar agora'; }
|
||||
if (updNow) { updNow.disabled = false; updNow.textContent = UPD?.version ? `atualizar para ${UPD.version}` : 'Verificar agora'; }
|
||||
toast(`Falha na atualização: ${e}`);
|
||||
@@ -914,6 +953,7 @@ function watchClose() {
|
||||
return;
|
||||
}
|
||||
await loadDevices();
|
||||
try { await rpc('sessions_prune', {}); } catch { /* retention is best-effort */ }
|
||||
await loadRecent();
|
||||
renderHome();
|
||||
})();
|
||||
@@ -619,4 +619,20 @@ export const EN = {
|
||||
'{n} arquivo': '{n} file',
|
||||
'{n} arquivos': '{n} files',
|
||||
'{n} {n} · {n}': '{n} {n} · {n}',
|
||||
'Apagar esta sessão': 'Delete this session',
|
||||
'Apagar todas as sessões salvas sozinhas': 'Delete all autosaved sessions',
|
||||
'Apagar sessão salva': 'Delete saved session',
|
||||
'Apagar': 'Delete',
|
||||
'Apagar tudo': 'Delete all',
|
||||
'Apagar todas as sessões salvas': 'Delete all saved sessions',
|
||||
'Todas as sessões salvas sozinhas serão apagadas de vez. Sessões abertas agora são mantidas.': 'All autosaved sessions will be permanently deleted. Sessions currently open are kept.',
|
||||
'Sessão apagada': 'Session deleted',
|
||||
'Nada para apagar': 'Nothing to delete',
|
||||
'Apagar sessões salvas sozinhas após': 'Delete autosaved sessions after',
|
||||
'Nunca': 'Never',
|
||||
'7 dias': '7 days',
|
||||
'30 dias': '30 days',
|
||||
'90 dias': '90 days',
|
||||
'será apagada de vez. Os arquivos do disco não são afetados.': 'will be permanently deleted. Files on the disk are not affected.',
|
||||
'Sessões apagadas: {n}': 'Sessions deleted: {n}',
|
||||
};
|
||||
@@ -24,7 +24,7 @@
|
||||
<button class="side-action" id="openImageBtn"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M4 7a2 2 0 0 1 2-2h4l2 2h6a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" stroke="currentColor" stroke-width="1.8" fill="none"/></svg>Abrir arquivo de imagem…</button>
|
||||
<div class="side-title">Sessões</div>
|
||||
<ul id="sessionList" class="list"></ul>
|
||||
<div class="side-title">Recentes <small style="font-weight:400;text-transform:none;letter-spacing:0">(salvas sozinhas)</small></div>
|
||||
<div class="side-title">Recentes <small style="font-weight:400;text-transform:none;letter-spacing:0">(salvas sozinhas)</small> <button class="icon-btn small" id="clearRecentBtn" title="Apagar todas as sessões salvas sozinhas" aria-label="Apagar todas as sessões salvas sozinhas"><svg viewBox="0 0 24 24" width="16" height="16"><path d="M5 7h14M10 7V5h4v2m-6 0l1 12h6l1-12" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg></button></div>
|
||||
<ul id="recentList" class="list"></ul>
|
||||
<button class="side-action" id="loadSessionBtn"><svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 4v11m0 0l-4-4m4 4l4-4M5 20h14" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>Abrir sessão salva…</button>
|
||||
</nav>
|
||||
|
||||
Reference in new issue
Block a user