chore(i18n): finish translating code comments and scripts to English
CI / Format and clippy (push) Successful in 1m57s
CI / Dependency audit (push) Successful in 13s
CI / Check desktop app (Linux) (push) Successful in 3m54s
CI / Tests (Linux) (push) Failing after 7m25s
Release / Create release (push) Successful in 6s
Release / CLI (linux-aarch64) (push) Successful in 1m48s
Release / CLI (linux-x86_64) (push) Successful in 2m39s
Release / Desktop app (linux-aarch64) (push) Successful in 10m9s
Release / Desktop app (linux-x86_64) (push) Successful in 12m20s
Release / Desktop app (windows) (push) Successful in 7m32s
Release / Build latest.json (push) Successful in 5s
CI / Format and clippy (push) Successful in 1m57s
CI / Dependency audit (push) Successful in 13s
CI / Check desktop app (Linux) (push) Successful in 3m54s
CI / Tests (Linux) (push) Failing after 7m25s
Release / Create release (push) Successful in 6s
Release / CLI (linux-aarch64) (push) Successful in 1m48s
Release / CLI (linux-x86_64) (push) Successful in 2m39s
Release / Desktop app (linux-aarch64) (push) Successful in 10m9s
Release / Desktop app (linux-x86_64) (push) Successful in 12m20s
Release / Desktop app (windows) (push) Successful in 7m32s
Release / Build latest.json (push) Successful in 5s
Completes the English-only standardization for code, comments, scripts and CI templates (UI/CLI-facing strings stay bilingual pt-BR/en, untouched). Covers the remaining crates/, apps/gui/ui/ and apps/gui/src-tauri/ comments, .gitea/ workflow and template comments, .githooks/ hooks, and the scripts/ source comments for the files already renamed in a prior commit (check.sh, generate-logo.py, install-hooks.sh). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
This commit is contained in:
1 parent
da212e6926
commit
6328220c60
129 files changed
+1961
-1834
No files matched your search
+12
-12
@@ -129,7 +129,7 @@ h1, h2, h3 { margin: 0; letter-spacing: -0.02em; }
|
||||
.page { max-width: 1120px; margin: 0 auto; padding: 36px 40px 120px; animation: rise .35s cubic-bezier(.2, .8, .2, 1); }
|
||||
@keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
||||
|
||||
/* ------------------------------------------------------------ componentes */
|
||||
/* ------------------------------------------------------------ components */
|
||||
.hero { padding: 56px 0 28px; max-width: 640px; }
|
||||
.hero h1 { font-size: clamp(32px, 5vw, 48px); line-height: 1.05; font-weight: 700; }
|
||||
.hero p { color: var(--text-2); font-size: 18px; margin: 14px 0 0; }
|
||||
@@ -194,7 +194,7 @@ details.adv[open] summary { margin-bottom: 10px; }
|
||||
.check { display: flex; gap: 10px; align-items: flex-start; padding: 6px 0; font-size: 14px; }
|
||||
.check input { margin-top: 3px; accent-color: var(--accent); width: 16px; height: 16px; }
|
||||
|
||||
/* progresso */
|
||||
/* progress */
|
||||
.progress-card { background: var(--surface); border-radius: 22px; box-shadow: var(--shadow); padding: 26px; display: grid; gap: 14px; }
|
||||
.bar { height: 10px; border-radius: 99px; background: color-mix(in srgb, var(--text) 9%, transparent); overflow: hidden; }
|
||||
.bar > i { display: block; height: 100%; border-radius: 99px; background: linear-gradient(90deg, var(--accent), var(--accent-2)); transition: width .3s ease; }
|
||||
@@ -204,7 +204,7 @@ details.adv[open] summary { margin-bottom: 10px; }
|
||||
.stat b { display: block; font-size: 28px; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
|
||||
.stat span { color: var(--text-2); font-size: 13px; }
|
||||
|
||||
/* resultados */
|
||||
/* results */
|
||||
.toolbar { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin: 22px 0 12px; }
|
||||
.chips { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 2px; scrollbar-width: none; }
|
||||
.chips::-webkit-scrollbar { display: none; }
|
||||
@@ -212,7 +212,7 @@ details.adv[open] summary { margin-bottom: 10px; }
|
||||
.chip[aria-pressed="true"] { background: var(--text); color: var(--bg); }
|
||||
.chip small { opacity: .6; margin-left: 4px; font-variant-numeric: tabular-nums; }
|
||||
.input, .select { border: 0; border-radius: 12px; padding: 9px 14px; background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); font: inherit; min-width: 0; }
|
||||
/* O fundo do select é translúcido; a lista aberta não herda isso e vinha branca com texto claro no tema escuro. */
|
||||
/* The select background is translucent; the open dropdown does not inherit it and rendered white with light text in dark mode. */
|
||||
.select option, .select optgroup { background-color: var(--surface); color: var(--text); }
|
||||
.input { flex: 1 1 200px; }
|
||||
.input::placeholder { color: var(--text-3); }
|
||||
@@ -255,7 +255,7 @@ code, .mono { overflow-wrap: anywhere; font-family: var(--mono); font-size: .92e
|
||||
.scrim { display: none; }
|
||||
.back { color: var(--accent); background: none; border: 0; cursor: pointer; font-weight: 500; padding: 0; margin-bottom: 8px; }
|
||||
|
||||
/* ------------------------------------------------------------ responsivo */
|
||||
/* ------------------------------------------------------------ responsive */
|
||||
@media (max-width: 1100px) {
|
||||
.split { grid-template-columns: 1fr; }
|
||||
.preview { position: static; order: -1; grid-template-columns: 120px 1fr; align-items: start; }
|
||||
@@ -285,12 +285,12 @@ code, .mono { overflow-wrap: anywhere; font-family: var(--mono); font-size: .92e
|
||||
* { animation: none !important; transition: none !important; }
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ tema */
|
||||
/* ------------------------------------------------------------ theme */
|
||||
.theme-switch { display: inline-flex; padding: 2px; border-radius: 999px; background: color-mix(in srgb, var(--text) 8%, transparent); }
|
||||
.theme-switch button { border: 0; background: transparent; cursor: pointer; padding: 4px 10px; border-radius: 999px; font-size: 12px; color: var(--text-2); }
|
||||
.theme-switch button[aria-pressed="true"] { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0, 0, 0, .18); font-weight: 600; }
|
||||
|
||||
/* ------------------------------------------------------------ pré-visualização */
|
||||
/* ------------------------------------------------------------ preview */
|
||||
.viewer { position: relative; min-height: 160px; max-height: 52vh; overflow: auto; border-radius: 12px; background: var(--surface-2); box-shadow: inset 0 0 0 1px var(--line); display: flex; flex-direction: column; align-items: center; }
|
||||
.viewer.large { max-height: none; height: 100%; min-height: 0; }
|
||||
.viewer > * { max-width: 100%; }
|
||||
@@ -335,7 +335,7 @@ dialog.lightbox[open] { display: grid; }
|
||||
.hero-logo { width: min(340px, 70%); height: auto; margin: 0 0 14px -6px; }
|
||||
.pill.accent { background: var(--accent-soft); color: var(--accent); }
|
||||
|
||||
/* ------------------------------------------------------------ explorador de arquivos */
|
||||
/* ------------------------------------------------------------ file explorer */
|
||||
.main.fixed { overflow: hidden; }
|
||||
.page.wide { max-width: none; padding: 14px 18px 0; }
|
||||
.page.fill { height: 100%; display: flex; flex-direction: column; }
|
||||
@@ -467,7 +467,7 @@ dialog.lightbox[open] { display: grid; }
|
||||
.cand .dev[aria-current="true"] { outline: 2px solid var(--accent); }
|
||||
.cand .s { white-space: normal; }
|
||||
|
||||
/* ------------------------------------------------------------ visualizador de bancos de dados */
|
||||
/* ------------------------------------------------------------ database viewer */
|
||||
.db-sum { width: 100%; padding: 16px; display: grid; gap: 8px; text-align: left; }
|
||||
.db-sum ul { list-style: none; margin: 4px 0; padding: 0; display: grid; gap: 2px; font-size: 13px; }
|
||||
.db-sum li { display: flex; justify-content: space-between; gap: 10px; } .db-sum li small { color: var(--text-3); }
|
||||
@@ -497,7 +497,7 @@ dialog.lightbox[open] { display: grid; }
|
||||
.xp-tiles img { width: 100%; height: 100%; display: block; }
|
||||
.xp-pin { position: absolute; width: 14px; height: 14px; margin: -7px 0 0 -7px; border-radius: 50%; background: #0f8f8f; border: 3px solid #fff; box-shadow: 0 1px 4px rgba(0,0,0,.4); }
|
||||
|
||||
/* saúde do disco (SMART) */
|
||||
/* disk health (SMART) */
|
||||
.h-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 10px; }
|
||||
.h-tile { background: var(--surface); border-radius: 14px; box-shadow: var(--shadow); padding: 12px 14px; display: flex; flex-direction: column; gap: 2px; }
|
||||
.h-tile small { color: var(--text-3); font-size: 12px; }
|
||||
@@ -516,10 +516,10 @@ dialog.lightbox[open] { display: grid; }
|
||||
.h-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--ok); }
|
||||
.h-dot.warning { background: var(--warn); } .h-dot.failing { background: var(--bad); }
|
||||
|
||||
/* logo: uma arte só (laranja sobre transparente) serve aos temas claro e escuro */
|
||||
/* logo: a single artwork (orange on transparent) works for both light and dark themes */
|
||||
.logo-img { display: block; flex: none; }
|
||||
|
||||
/* atualização do app */
|
||||
/* app update */
|
||||
.update-card { position: fixed; right: 18px; bottom: 18px; z-index: 60; width: min(400px, calc(100vw - 36px)); display: grid; gap: 10px; padding: 16px 18px; border-radius: 18px; background: var(--surface); box-shadow: var(--shadow); border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent); }
|
||||
.update-card .upd-head { display: grid; gap: 2px; }
|
||||
.update-card .upd-ver { font-size: 12.5px; color: var(--text-2); }
|
||||
|
||||
+30
-30
@@ -1,5 +1,5 @@
|
||||
// Guará: interface. Fala com o núcleo por `rpc(método, parâmetros)`:
|
||||
// • app Tauri: invoke('rpc') • modo `guara ui`: POST /rpc/<método> com token.
|
||||
// Guará: UI. Communicates with the core via `rpc(method, params)`:
|
||||
// • Tauri app: invoke('rpc') • `guara ui` mode: POST /rpc/<method> with token.
|
||||
|
||||
import { createExplorer } from './explorer.js';
|
||||
import { setLang, LOC, currentLang } from './i18n.js';
|
||||
@@ -10,7 +10,7 @@ let token = params.get('token');
|
||||
try {
|
||||
if (token) sessionStorage.setItem('guara-token', token);
|
||||
else token = sessionStorage.getItem('guara-token');
|
||||
} catch { /* armazenamento indisponível */ }
|
||||
} catch { /* storage unavailable */ }
|
||||
|
||||
async function rpc(method, p = {}) {
|
||||
if (tauri?.core?.invoke) return tauri.core.invoke('rpc', { method, params: p });
|
||||
@@ -29,7 +29,7 @@ const fileBase = tauri ? (isWin ? 'http://guara.localhost' : 'guara://localhost'
|
||||
const thumbUrl = (job, id, s = 256) => `${fileBase}/thumb/${job}/${id}?s=${s}` + (tauri ? '' : `&t=${encodeURIComponent(token || '')}`);
|
||||
const fileUrl = (job, id) => `${fileBase}/file/${job}/${id}` + (tauri ? '' : `?t=${encodeURIComponent(token || '')}`);
|
||||
|
||||
// Atualiza o HTML só quando mudou (evita piscar e perder o foco/hover).
|
||||
// Update HTML only when changed (prevents flickering and losing focus/hover).
|
||||
function setHTML(el, html) {
|
||||
if (!el || el._h === html) return false;
|
||||
el._h = html;
|
||||
@@ -37,7 +37,7 @@ function setHTML(el, html) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ tema
|
||||
// ------------------------------------------------------------------ theme
|
||||
function applyTheme(t) {
|
||||
const root = document.documentElement;
|
||||
if (t === 'light' || t === 'dark') root.dataset.theme = t; else delete root.dataset.theme;
|
||||
@@ -47,19 +47,19 @@ try { applyTheme(localStorage.getItem('guara-theme') || 'auto'); } catch { apply
|
||||
document.querySelectorAll('[data-theme-pick]').forEach((b) => (b.onclick = async () => {
|
||||
const t = b.dataset.themePick;
|
||||
applyTheme(t);
|
||||
try { localStorage.setItem('guara-theme', t); } catch { /* sem armazenamento */ }
|
||||
try { localStorage.setItem('guara-theme', t); } catch { /* no storage */ }
|
||||
saveSettings({ theme: t });
|
||||
}));
|
||||
|
||||
// ------------------------------------------------------------------ configurações
|
||||
// (declarado antes do uso: `S` é definido mais abaixo, por isso o objeto fica aqui)
|
||||
// ------------------------------------------------------------------ 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 };
|
||||
async function saveSettings(patch) {
|
||||
Object.assign(SETTINGS, patch);
|
||||
try { await rpc('settings_set', SETTINGS); } catch { /* segue só com o armazenamento local */ }
|
||||
try { await rpc('settings_set', SETTINGS); } catch { /* continue with local storage only */ }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ utilidades
|
||||
// ------------------------------------------------------------------ utilities
|
||||
const $ = (s, el = document) => el.querySelector(s);
|
||||
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
const human = (n) => {
|
||||
@@ -153,14 +153,14 @@ const S = {
|
||||
sessions: [], // {job, target, label}
|
||||
device: null, // {path,name,...}
|
||||
layout: null,
|
||||
job: null, // id da varredura atual
|
||||
job: null, // active scan job id
|
||||
status: null,
|
||||
filters: { category: null, minConfidence: null, query: '', sort: 'confidence' },
|
||||
items: [],
|
||||
total: 0,
|
||||
selected: new Set(),
|
||||
allFiltered: false,
|
||||
current: null, // arquivo em foco (pré-visualização)
|
||||
current: null, // focused file (preview)
|
||||
poll: null,
|
||||
recJob: null,
|
||||
};
|
||||
@@ -259,7 +259,7 @@ $('#loadSessionBtn').onclick = async () => {
|
||||
} catch (e) { info({ title: 'Não foi possível abrir a sessão', html: `<p>${esc(e.message)}</p>` }); }
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------ início
|
||||
// ------------------------------------------------------------------ home
|
||||
function renderHome() {
|
||||
main.classList.remove('fixed');
|
||||
stopPoll();
|
||||
@@ -278,7 +278,7 @@ function renderHome() {
|
||||
$('#elevateHome')?.addEventListener('click', () => tauri.core.invoke('relaunch_elevated').catch((e) => toast(String(e))));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ dispositivo
|
||||
// ------------------------------------------------------------------ device
|
||||
async function openDevice(d) {
|
||||
main.classList.remove('fixed');
|
||||
stopPoll();
|
||||
@@ -291,7 +291,7 @@ async function openDevice(d) {
|
||||
try { layout = await rpc('inspect', { path: d.path }); } catch (e) { err = e.message; }
|
||||
S.layout = layout;
|
||||
renderDevice(err);
|
||||
// saúde (SMART) em segundo plano: não atrasa a tela
|
||||
// Query SMART health in the background without blocking the screen.
|
||||
rpc('health', { path: d.path }).then((h) => { if (S.device === d) { S.health = h; paintHealth(); } }).catch(() => {});
|
||||
}
|
||||
|
||||
@@ -325,7 +325,7 @@ function paintHealth() {
|
||||
${attrs.map((a) => `<tr><td><span class="h-dot ${esc(a.status)}" title="${HEALTH_WORD[a.status] || ''}"></span></td><td class="mono">${esc(a.id)}</td><td class="mono">${esc(a.name)}</td><td>${dash(a.value)}</td><td>${dash(a.worst)}</td><td>${dash(a.thresh)}</td><td class="mono">${esc(a.raw)}</td></tr>`).join('')}
|
||||
</tbody></table></div></details>` : '';
|
||||
const scope = h.partition ? `<p class="sub">O SMART é do disco inteiro: os dados abaixo são de <span class="mono">${esc(h.disk)}</span>, que contém <span class="mono">${esc(h.partition)}</span>.</p>` : '';
|
||||
// o que motivou o alerta (os campos da ficha já aparecem abaixo)
|
||||
// Fields that triggered the alert (standard summary fields appear below).
|
||||
const known = new Set(['Modelo', 'Nº de série', 'Horas ligado', 'Temperatura', 'Autoavaliação SMART', 'Vida útil usada']);
|
||||
const why = (h.details || []).filter(([k]) => !known.has(k)).map(([k, v]) => `<dt>${esc(k)}</dt><dd>${esc(v)}</dd>`).join('');
|
||||
el.innerHTML = `${bad ? `<div class="notice ${h.status === 'failing' ? 'bad' : ''}">${warnSvg}<div><b>${esc(h.summary)}</b>
|
||||
@@ -338,7 +338,7 @@ function paintHealth() {
|
||||
$('#healthImg')?.addEventListener('click', startImage);
|
||||
}
|
||||
|
||||
/** Disco com sinais de defeito: oferece criar a imagem antes de varrer. */
|
||||
/** Disk showing signs of failure: offer to create a disk image before scanning. */
|
||||
function confirmRisky() {
|
||||
return new Promise((resolve) => {
|
||||
const h = S.health;
|
||||
@@ -532,8 +532,8 @@ function openSearch(job, terms) {
|
||||
(async () => { while (await tick2()) await new Promise((r) => setTimeout(r, 700)); })();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ varredura
|
||||
let EX = null; // explorador de arquivos (grade, navegação, detalhes)
|
||||
// ------------------------------------------------------------------ scan
|
||||
let EX = null; // file explorer (grid, navigation, details)
|
||||
|
||||
function openJob(s) {
|
||||
stopPoll();
|
||||
@@ -556,7 +556,7 @@ async function tick() {
|
||||
S.status = st;
|
||||
paintStatus();
|
||||
EX?.onStatus(st);
|
||||
// continua consultando enquanto a varredura roda ou as datas/EXIF ainda estão sendo lidas
|
||||
// Keep polling while the scan is running or dates/EXIF are still being enriched.
|
||||
if (st.status !== 'running' && !st.enriching) {
|
||||
stopPoll();
|
||||
loadRecent();
|
||||
@@ -615,7 +615,7 @@ function paintStatus() {
|
||||
|
||||
async function chooseSource() {
|
||||
let cands = [];
|
||||
try { cands = await rpc('session_candidates', { job: S.job }); } catch { /* segue só com o campo manual */ }
|
||||
try { cands = await rpc('session_candidates', { job: S.job }); } catch { /* fall back to manual path input */ }
|
||||
const st = S.status || {};
|
||||
const size = st.layout?.size;
|
||||
const d = $('#dlg');
|
||||
@@ -711,7 +711,7 @@ async function recover(sel) {
|
||||
} catch (e) { info({ title: 'Não foi possível recuperar', html: `<p>${esc(e.message)}</p>` }); }
|
||||
}
|
||||
|
||||
// Modal de progresso de tarefas auxiliares (recuperação / imagem).
|
||||
// Progress modal for auxiliary jobs (recovery / disk imaging).
|
||||
function watchAux(job, title, doneHtml) {
|
||||
const d = $('#dlg');
|
||||
d.onclose = null;
|
||||
@@ -740,7 +740,7 @@ function watchAux(job, title, doneHtml) {
|
||||
|
||||
async function openJobFromList(s) { openJob(s); }
|
||||
|
||||
// ------------------------------------------------------------------ configurações (janela)
|
||||
// ------------------------------------------------------------------ settings modal
|
||||
$('#settingsBtn')?.addEventListener('click', () => {
|
||||
const d = $('#dlg');
|
||||
d.innerHTML = `<form method="dialog"><h2>Configurações</h2>
|
||||
@@ -764,9 +764,9 @@ $('#settingsBtn')?.addEventListener('click', () => {
|
||||
d.showModal();
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------ atualização do app
|
||||
// O núcleo do app (Tauri) consulta o `latest.json` da release no Gitea, confere a assinatura do
|
||||
// pacote com a chave pública embutida, instala e reabre. No navegador (`guara ui`) não há nada a fazer.
|
||||
// ------------------------------------------------------------------ app updater
|
||||
// The desktop shell (Tauri) queries `latest.json` from the Gitea release, verifies the
|
||||
// package signature against the embedded public key, installs, and relaunches. No-op in browser mode (`guara ui`).
|
||||
let UPD = null;
|
||||
|
||||
async function checkUpdate(manual) {
|
||||
@@ -822,7 +822,7 @@ function updateProgress(p) {
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ fechar com varredura em andamento
|
||||
// ------------------------------------------------------------------ close while scan is running
|
||||
function askClose() {
|
||||
return new Promise((resolve) => {
|
||||
const d = $('#dlg');
|
||||
@@ -848,14 +848,14 @@ function watchClose() {
|
||||
try {
|
||||
await rpc('scan_pause', { job: S.job });
|
||||
for (let i = 0; i < 40; i++) { const st = await rpc('status', { job: S.job }); if (st.status !== 'running') break; await new Promise((r) => setTimeout(r, 250)); }
|
||||
await new Promise((r) => setTimeout(r, 600)); // dá tempo do salvamento automático
|
||||
} catch { /* sai mesmo assim */ }
|
||||
await new Promise((r) => setTimeout(r, 600)); // allow auto-save to flush
|
||||
} catch { /* exit anyway */ }
|
||||
}
|
||||
win.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ início do app
|
||||
// ------------------------------------------------------------------ app startup
|
||||
(async function init() {
|
||||
try {
|
||||
const v = await rpc('version');
|
||||
|
||||
+24
-24
@@ -1,7 +1,7 @@
|
||||
// Explorador de arquivos recuperados: grade de miniaturas (virtualizada, aguenta
|
||||
// dezenas de milhares de itens), navegação por tipo/data/pasta/câmera, seleção
|
||||
// estilo gerenciador de arquivos (Ctrl/Shift/teclado), painel de detalhes e
|
||||
// visualização rápida em tela cheia.
|
||||
// Recovered file explorer: thumbnail grid (virtualized, handles
|
||||
// tens of thousands of items), navigation by type/date/folder/camera, file-manager
|
||||
// style selection (Ctrl/Shift/keyboard), inspector panel, and
|
||||
// fullscreen quick look.
|
||||
|
||||
import { renderPreview, kindOf, KIND_LABEL } from './preview.js';
|
||||
import { LOC } from './i18n.js';
|
||||
@@ -66,7 +66,7 @@ export function createExplorer(deps) {
|
||||
try {
|
||||
E.view = localStorage.getItem('guara-view') || 'grid';
|
||||
E.tile = +localStorage.getItem('guara-tile') || 150;
|
||||
} catch { /* sem armazenamento */ }
|
||||
} catch { /* no storage */ }
|
||||
|
||||
host.innerHTML = `<div class="xp" data-view="${E.view}">
|
||||
<div class="xp-bar">
|
||||
@@ -91,7 +91,7 @@ export function createExplorer(deps) {
|
||||
$('#xpSort').value = E.sort;
|
||||
$('#xpSize').value = E.tile;
|
||||
|
||||
// ------------------------------------------------------------ dados
|
||||
// ------------------------------------------------------------ data
|
||||
const toQuery = () => {
|
||||
const f = E.filters;
|
||||
const q = {};
|
||||
@@ -139,8 +139,8 @@ export function createExplorer(deps) {
|
||||
|
||||
const reloadAll = () => { reload(); loadFacets(); };
|
||||
|
||||
// ------------------------------------------------------------ layout virtual
|
||||
// Agrupa por dia; se houver poucos itens por dia, agrupa por mês.
|
||||
// ------------------------------------------------------------ virtual layout
|
||||
// Group by day; if there are too few items per day, group by month.
|
||||
let groupBy = 'day';
|
||||
const dayKey = (t) => (t == null ? 'Sem data' : new Date(t * 1000).toISOString().slice(0, groupBy === 'day' ? 10 : 7));
|
||||
const dayLabel = (k) => {
|
||||
@@ -150,7 +150,7 @@ export function createExplorer(deps) {
|
||||
: new Date(k + '-01T00:00:00Z').toLocaleDateString(LOC(), { timeZone: 'UTC', month: 'long', year: 'numeric' });
|
||||
};
|
||||
const fmtDate = (t) => (t == null ? 'sem data' : new Date(t * 1000).toLocaleString(LOC(), { timeZone: 'UTC', dateStyle: 'medium', timeStyle: 'short' }));
|
||||
// Metadados do núcleo trazem datas em ISO 8601; o idioma decide a ordem.
|
||||
// Core metadata provides ISO 8601 dates; the active locale determines the order.
|
||||
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?$/;
|
||||
const fmtMeta = (v) => ISO_DATE.test(v) ? new Date(v.endsWith('Z') ? v : v + 'Z').toLocaleString(LOC(), { timeZone: 'UTC', dateStyle: 'medium', timeStyle: 'medium' }) : v;
|
||||
const fmtDay = (t) => (t == null ? '' : new Date(t * 1000).toLocaleDateString(LOC(), { timeZone: 'UTC', day: '2-digit', month: 'short', year: 'numeric' }));
|
||||
@@ -195,7 +195,7 @@ export function createExplorer(deps) {
|
||||
});
|
||||
flush();
|
||||
if (grouped) {
|
||||
// contagem por grupo
|
||||
// count per group
|
||||
for (let r = 0; r < rows.length; r++) {
|
||||
if (rows[r].t !== 'head') continue;
|
||||
let n = 0;
|
||||
@@ -245,7 +245,7 @@ export function createExplorer(deps) {
|
||||
sizer.appendChild(frag);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ itens
|
||||
// ------------------------------------------------------------ items
|
||||
const isSel = (id) => E.allFiltered || E.sel.has(id);
|
||||
|
||||
function headHtml(row) {
|
||||
@@ -288,7 +288,7 @@ export function createExplorer(deps) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// imagens que falham viram um aviso (arquivo possivelmente danificado)
|
||||
// failed images become a warning (file possibly damaged)
|
||||
scroll.addEventListener('error', (e) => {
|
||||
const img = e.target;
|
||||
if (img.tagName !== 'IMG' || !img.dataset.th) return;
|
||||
@@ -300,7 +300,7 @@ export function createExplorer(deps) {
|
||||
: `<div class="xp-ico c-bad" title="Não foi possível gerar a miniatura (arquivo possivelmente danificado)"><span>${I.warn}</span><b>?</b></div>`);
|
||||
}, true);
|
||||
|
||||
// ------------------------------------------------------------ seleção
|
||||
// ------------------------------------------------------------ selection
|
||||
const selCount = () => (E.allFiltered ? E.total : E.sel.size);
|
||||
const idxOf = (id) => E.items.findIndex((x) => x.id === id);
|
||||
|
||||
@@ -387,7 +387,7 @@ export function createExplorer(deps) {
|
||||
else if (e.key === 'Escape') { E.sel = new Set(); E.allFiltered = false; paintSel(); }
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------ barra de ações
|
||||
// ------------------------------------------------------------ action bar
|
||||
function paintActions() {
|
||||
const n = selCount();
|
||||
const html = n
|
||||
@@ -407,7 +407,7 @@ export function createExplorer(deps) {
|
||||
else if (a === 'recall') onRecover({ filter: toQuery() });
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------ navegação lateral
|
||||
// ------------------------------------------------------------ sidebar navigation
|
||||
function paintNav() {
|
||||
const f = E.facets;
|
||||
if (!f) return;
|
||||
@@ -424,7 +424,7 @@ export function createExplorer(deps) {
|
||||
+ (open ? y.months.map((m) => node(MONTHS[m.month - 1], m.count, `data-f="month" data-y="${y.year}" data-v="${m.month}"`, F.year === y.year && F.month === m.month, 1)).join('') : '');
|
||||
}).join('') + (f.undated ? node('Sem data', f.undated, 'data-f="undated" data-v="1"', F.undated) : '');
|
||||
const folders = f.folders.filter((d) => {
|
||||
// só mostra se todos os ancestrais estiverem expandidos
|
||||
// only show if all ancestors are expanded
|
||||
const parts = d.path.split('/');
|
||||
for (let k = 1; k < parts.length; k++) if (!E.expanded.has('d' + parts.slice(0, k).join('/')) && !(F.folder || '').startsWith(parts.slice(0, k).join('/'))) return false;
|
||||
return true;
|
||||
@@ -528,7 +528,7 @@ export function createExplorer(deps) {
|
||||
changed();
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------ barra superior
|
||||
// ------------------------------------------------------------ top bar
|
||||
let qt;
|
||||
$('#xpQ').addEventListener('input', (e) => { clearTimeout(qt); qt = setTimeout(() => { E.filters.query = e.target.value.trim(); changed(); }, 250); });
|
||||
$('#xpSort').addEventListener('change', (e) => { E.sort = e.target.value; reload(); });
|
||||
@@ -550,7 +550,7 @@ export function createExplorer(deps) {
|
||||
$('#xpNavBtn').onclick = () => root.classList.toggle('show-nav');
|
||||
$('#xpVeil').onclick = () => root.classList.remove('show-nav', 'show-insp');
|
||||
|
||||
// ------------------------------------------------------------ painel de detalhes
|
||||
// ------------------------------------------------------------ inspector panel
|
||||
let inspT, stopPv = null, metaToken = 0;
|
||||
const schedInspector = () => { clearTimeout(inspT); inspT = setTimeout(paintInspector, 90); };
|
||||
|
||||
@@ -584,7 +584,7 @@ export function createExplorer(deps) {
|
||||
<div class="xp-meta" id="xpMeta"></div>
|
||||
<div class="row"><button class="btn sm" data-i="full">Tela cheia</button><button class="btn primary sm" data-i="rec1">Recuperar este arquivo</button></div>`;
|
||||
stopPv = renderPreview($('#xpPv'), it, { url, thumb: kindOf(it) === 'image', inlineMedia, db: dbApi(it), onOpen: () => openLightbox(idxOf(it.id)), onExternal: () => openExternal(it) });
|
||||
// clicar na miniatura abre o modal grande
|
||||
// clicking the thumbnail opens the large modal
|
||||
$('#xpPv').addEventListener('click', (ev) => {
|
||||
if (ev.target.closest('button, a, .pv-pdfbar')) return;
|
||||
openLightbox(idxOf(it.id));
|
||||
@@ -599,7 +599,7 @@ export function createExplorer(deps) {
|
||||
$('#xpOsm')?.addEventListener('click', () => rpc('open_url', { url: `https://www.openstreetmap.org/?mlat=${m.gps[0]}&mlon=${m.gps[1]}#map=15/${m.gps[0]}/${m.gps[1]}` }).catch(() => {}));
|
||||
}).catch(() => {});
|
||||
}
|
||||
/** Coordenadas e, se o usuário permitiu, um mapa (blocos do OpenStreetMap, 3x3 ao redor do ponto). */
|
||||
/** Coordinates and, if enabled by the user, a map (3x3 OpenStreetMap tiles around the point). */
|
||||
function gpsHtml([lat, lon]) {
|
||||
let map = '';
|
||||
if (deps.getSettings?.().maps) {
|
||||
@@ -625,7 +625,7 @@ export function createExplorer(deps) {
|
||||
else if (a === 'close') root.classList.remove('show-insp');
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------ tela cheia
|
||||
// ------------------------------------------------------------ fullscreen
|
||||
let stopLb = null;
|
||||
function openLightbox(i) {
|
||||
const it = E.items[i];
|
||||
@@ -649,14 +649,14 @@ export function createExplorer(deps) {
|
||||
if (!d.open) d.showModal();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ paint geral
|
||||
// ------------------------------------------------------------ main paint
|
||||
function paintAll() { paintCrumb(); paintChips(); paintActions(); renderWindow(); }
|
||||
|
||||
paintView(); paintCrumb(); paintActions(); paintInspector();
|
||||
reloadAll();
|
||||
|
||||
return {
|
||||
/** Chamado a cada atualização de status da varredura. */
|
||||
/** Called on every scan status update. */
|
||||
onStatus(st) {
|
||||
E.status = st;
|
||||
const enriching = !!st.enriching;
|
||||
@@ -672,7 +672,7 @@ export function createExplorer(deps) {
|
||||
},
|
||||
query: () => toQuery(),
|
||||
destroy() { E.reloadToken++; stopPv?.(); stopLb?.(); host.innerHTML = ''; },
|
||||
/** Idioma trocado em Configurações: refaz o que foi montado em JS (datas, meses, rótulos). */
|
||||
/** Language changed in Settings: rebuild JS-rendered elements (dates, months, labels). */
|
||||
relang() { paintCrumb(); paintChips(); paintNav(); paintActions(); paintInspector(); relayout(); },
|
||||
focus() { scroll.focus({ preventScroll: true }); },
|
||||
};
|
||||
|
||||
+25
-25
@@ -1,6 +1,6 @@
|
||||
// Dicionário português -> inglês da interface. {n} marca um valor variável.
|
||||
// Portuguese -> English UI dictionary. {n} marks a variable placeholder.
|
||||
export const EN = {
|
||||
// saúde do disco (SMART)
|
||||
// disk health (SMART)
|
||||
'O SMART não pôde ser lido. Instale o pacote smartmontools (comando smartctl) e rode como administrador para ver a saúde do disco.': 'SMART could not be read. Install the smartmontools package (smartctl command) and run as administrator to see the disk health.',
|
||||
'Setores realocados': 'Reallocated sectors',
|
||||
'Setores pendentes de realocação': 'Pending sectors',
|
||||
@@ -38,7 +38,7 @@ export const EN = {
|
||||
'O SMART é do disco inteiro: os dados abaixo são de': 'SMART belongs to the whole disk: the data below is for',
|
||||
', que contém': ', which contains',
|
||||
'Imagem de disco: não há SMART para avaliar.': 'Disk image: there is no SMART to check.',
|
||||
// barra lateral e estrutura
|
||||
// sidebar and layout
|
||||
'Recuperação de arquivos': 'File recovery',
|
||||
'Dispositivos': 'Devices',
|
||||
'Dispositivos e sessões': 'Devices and sessions',
|
||||
@@ -64,7 +64,7 @@ export const EN = {
|
||||
'· requer admin': '· needs admin',
|
||||
'requer admin': 'needs admin',
|
||||
'{n} · requer admin': '{n} · needs admin',
|
||||
// início
|
||||
// home
|
||||
'Encontre o que você achou que tinha perdido.': 'Find what you thought was lost.',
|
||||
'Escolha um disco, SSD ou pendrive ao lado. O Guará lê tudo: arquivos apagados, partições perdidas e espaço não particionado, sem escrever nada no disco de origem.': 'Pick a disk, SSD or USB drive on the left. Guará reads everything: deleted files, lost partitions and unpartitioned space, without writing anything to the source disk.',
|
||||
'Uma varredura, tudo junto': 'One scan, everything together',
|
||||
@@ -74,7 +74,7 @@ export const EN = {
|
||||
'Origem protegida': 'Protected source',
|
||||
'Somente leitura. A recuperação sempre vai para outra pasta, com SHA-256 de tudo.': 'Read-only. Recovery always goes to another folder, with SHA-256 of everything.',
|
||||
'Nenhum disco apareceu.': 'No disks showed up.',
|
||||
// dispositivo
|
||||
// device
|
||||
'Mapa do disco': 'Disk map',
|
||||
'Como procurar': 'How to search',
|
||||
'Recomendado': 'Recommended',
|
||||
@@ -108,7 +108,7 @@ export const EN = {
|
||||
'encontrada por varredura': 'found by scanning',
|
||||
'Desconhecido': 'Unknown',
|
||||
'Este SSD usa TRIM: o sistema avisa o disco quais blocos foram apagados e o próprio SSD pode zerá-los. Arquivos apagados há bastante tempo tendem a não ser recuperáveis.': 'This SSD uses TRIM: the system tells the disk which blocks were deleted and the SSD itself may zero them. Files deleted a long time ago tend to be unrecoverable.',
|
||||
// saúde do disco
|
||||
// disk health
|
||||
'Saúde do disco': 'Disk health',
|
||||
'Este disco mostra sinais sérios de defeito.': 'This disk shows serious signs of failure.',
|
||||
'Este disco mostra sinais de desgaste ou erros.': 'This disk shows signs of wear or errors.',
|
||||
@@ -119,7 +119,7 @@ export const EN = {
|
||||
'Varrer mesmo assim': 'Scan anyway',
|
||||
'Cancelar': 'Cancel',
|
||||
'Este disco parece estar com defeito': 'This disk seems to be failing',
|
||||
// varredura
|
||||
// scan
|
||||
'‹ Dispositivos': '‹ Devices',
|
||||
'Procurando…': 'Searching…',
|
||||
'Varredura concluída': 'Scan complete',
|
||||
@@ -152,7 +152,7 @@ export const EN = {
|
||||
'Lendo tabela de partições': 'Reading partition table',
|
||||
'Lendo metadados {n} (partição {n})': 'Reading {n} metadata (partition {n})',
|
||||
'Varredura profunda por assinatura': 'Deep signature scan',
|
||||
// explorador
|
||||
// explorer
|
||||
'Navegação': 'Navigation',
|
||||
'Filtros': 'Filters',
|
||||
'Todos os arquivos': 'All files',
|
||||
@@ -246,7 +246,7 @@ export const EN = {
|
||||
'Tem localização GPS': 'Has GPS location',
|
||||
'Confiança {n}': 'Confidence {n}',
|
||||
'Nada por aqui': 'Nothing here',
|
||||
// pré-visualização
|
||||
// preview
|
||||
'Imagem': 'Image',
|
||||
'Vídeo': 'Video',
|
||||
'PDF': 'PDF',
|
||||
@@ -277,7 +277,7 @@ export const EN = {
|
||||
'Neste app o vídeo abre no seu player: o navegador embutido do Linux não traz os codecs e travaria a janela.': 'In this app, video opens in your player: the embedded Linux browser does not ship the codecs and would freeze the window.',
|
||||
'Neste app o áudio abre no seu player: o navegador embutido do Linux não traz os codecs e travaria a janela.': 'In this app, audio opens in your player: the embedded Linux browser does not ship the codecs and would freeze the window.',
|
||||
'pasta': 'folder',
|
||||
// banco de dados
|
||||
// database
|
||||
'Abrir o visualizador': 'Open the viewer',
|
||||
'{n} tabela(s) · {n} linhas · páginas de {n}': '{n} table(s) · {n} rows · {n} pages',
|
||||
'{n} linhas · {n} colunas': '{n} rows · {n} columns',
|
||||
@@ -297,7 +297,7 @@ export const EN = {
|
||||
'{n} a {n} de {n}': '{n} to {n} of {n}',
|
||||
'‹ Anterior': '‹ Previous',
|
||||
'Próximas ›': 'Next ›',
|
||||
// recuperar
|
||||
// recover
|
||||
'Recuperar para…': 'Recover to…',
|
||||
'Recuperar': 'Recover',
|
||||
'Pasta de destino': 'Destination folder',
|
||||
@@ -336,7 +336,7 @@ export const EN = {
|
||||
'{n} arquivo(s) não puderam ser gravados.': '{n} file(s) could not be written.',
|
||||
'Se a recuperação for interrompida, escolha a mesma pasta de novo: o Guará pula o que já foi conferido.': 'If the recovery is interrupted, pick the same folder again: Guará skips what was already verified.',
|
||||
'{n} já estavam recuperados e conferidos (pulados).': '{n} were already recovered and verified (skipped).',
|
||||
// imagem, hash, busca
|
||||
// image, hash, search
|
||||
'Criando imagem do disco': 'Creating disk image',
|
||||
'Nenhum setor ruim encontrado.': 'No bad sectors found.',
|
||||
'Calculando hashes da origem': 'Computing source hashes',
|
||||
@@ -354,7 +354,7 @@ export const EN = {
|
||||
'{n} ocorrência{n}': '{n} occurrence{n}',
|
||||
'Termos: {n}': 'Terms: {n}',
|
||||
'{n}% · {n} até agora': '{n}% · {n} so far',
|
||||
// sessões e origem
|
||||
// sessions and source
|
||||
'Abrir arquivo de imagem': 'Open disk image file',
|
||||
'Caminho completo de uma imagem de disco (.img, .dd, .iso…).': 'Full path of a disk image (.img, .dd, .iso…).',
|
||||
'Caminho do arquivo de sessão (.json) criado pelo Guará.': 'Path of the session file (.json) created by Guará.',
|
||||
@@ -374,7 +374,7 @@ export const EN = {
|
||||
'mesmo disco': 'same disk',
|
||||
'tamanho igual, conteúdo diferente': 'same size, different content',
|
||||
'mesmo tamanho (precisa de administrador para conferir)': 'same size (needs administrator to verify)',
|
||||
// configurações e fechamento
|
||||
// settings and shutdown
|
||||
'Idioma': 'Language',
|
||||
'Notificações': 'Notifications',
|
||||
'Avisar quando uma tarefa longa terminar': 'Notify when a long task finishes',
|
||||
@@ -389,14 +389,14 @@ export const EN = {
|
||||
'Sair sem pausar': 'Quit without pausing',
|
||||
'Continuar varrendo': 'Keep scanning',
|
||||
'A sessão é salva automaticamente. Pausando agora, você retoma exatamente do ponto em que parou.': 'The session is saved automatically. If you pause now, you resume exactly where you stopped.',
|
||||
// erros comuns vindos do núcleo
|
||||
// common errors from the core
|
||||
'Não foi possível falar com o núcleo do Guará: {n}. Se abriu esta página manualmente, use o endereço completo exibido pelo comando': 'Could not talk to the Guará core: {n}. If you opened this page manually, use the full address shown by the command',
|
||||
'Não foi possível reiniciar como administrador': 'Could not restart as administrator',
|
||||
'Autorização cancelada.': 'Authorization canceled.',
|
||||
'Origem desconectada': 'Source disconnected',
|
||||
'Este disco está em uso (montado em': 'This disk is in use (mounted at',
|
||||
'). O Guará só lê, mas evite gravar nele enquanto recupera, porque qualquer escrita pode sobrescrever o que você quer salvar.': '). Guará only reads, but avoid writing to it while recovering, because any write can overwrite what you want to save.',
|
||||
// formulário de opções de varredura
|
||||
// scan options form
|
||||
'Volumes cifrados, RAID e vídeos': 'Encrypted volumes, RAID and videos',
|
||||
'Senha do volume (BitLocker, LUKS, FileVault 2)': 'Volume password (BitLocker, LUKS, FileVault 2)',
|
||||
'Chave de recuperação do BitLocker': 'BitLocker recovery key',
|
||||
@@ -407,7 +407,7 @@ export const EN = {
|
||||
'Vídeo íntegro da mesma câmera/drone (refaz o índice de MP4 sem moov)': 'Intact video from the same camera/drone (rebuilds the index of an MP4 without moov)',
|
||||
'Lembrar as chaves dos volumes abertos na sessão': 'Remember the keys of opened volumes in the session',
|
||||
'A senha nunca é gravada; só a chave derivada.': 'The password is never saved; only the derived key.',
|
||||
// criar imagem do disco
|
||||
// disk image creation
|
||||
'Arquivo de destino': 'Destination file',
|
||||
'Número do caso (E01/AFF4)': 'Case number (E01/AFF4)',
|
||||
'Formato da imagem': 'Image format',
|
||||
@@ -416,18 +416,18 @@ export const EN = {
|
||||
'Examinador (E01/AFF4)': 'Examiner (E01/AFF4)',
|
||||
'Imagem salva em': 'Image saved to',
|
||||
'Hashes do conteúdo inteiro de': 'Hashes of the entire content of',
|
||||
// resultados e partições
|
||||
// results and partitions
|
||||
'arquivo(s) salvos em': 'file(s) saved to',
|
||||
'arquivos em {n}': 'files in {n}',
|
||||
'Espaço não particionado: {n}': 'Unpartitioned space: {n}',
|
||||
'Partição {n}': 'Partition {n}',
|
||||
'erro {n}': 'error {n}',
|
||||
// saúde do disco (campos vindos do núcleo)
|
||||
// disk health (fields from the core)
|
||||
'Autoavaliação SMART': 'SMART self-assessment',
|
||||
'Vida útil usada': 'Life used',
|
||||
'aprovado': 'passed',
|
||||
'REPROVADO: o próprio disco prevê falha': 'FAILED: the disk itself predicts failure',
|
||||
// pré-visualização
|
||||
// preview
|
||||
'Pré-visualização': 'Preview',
|
||||
'Pré-visualização de {n}': 'Preview of {n}',
|
||||
'vídeo': 'video',
|
||||
@@ -438,7 +438,7 @@ export const EN = {
|
||||
'diretório do ZIP não encontrado (arquivo possivelmente incompleto)': 'ZIP directory not found (file possibly incomplete)',
|
||||
'visualizador indisponível': 'viewer unavailable',
|
||||
'© colaboradores do OpenStreetMap': '© OpenStreetMap contributors',
|
||||
// explorador: textos que a interface divide em nós separados
|
||||
// explorer: strings split across separate DOM nodes by the UI
|
||||
'selecionado': 'selected',
|
||||
'selecionados': 'selected',
|
||||
'selecionado (todos desta vista)': 'selected (all in this view)',
|
||||
@@ -459,7 +459,7 @@ export const EN = {
|
||||
'Conteúdo do arquivo compactado · {n}': 'Archive contents · {n}',
|
||||
'Banco de dados SQLite · {n}': 'SQLite database · {n}',
|
||||
'Dados brutos (hexadecimal) · {n}': 'Raw data (hexadecimal) · {n}',
|
||||
// tipos e campos de metadados vindos do núcleo
|
||||
// metadata types and fields from the core
|
||||
'Imagem JPEG': 'JPEG image',
|
||||
'Imagem PNG': 'PNG image',
|
||||
'Imagem GIF': 'GIF image',
|
||||
@@ -482,7 +482,7 @@ export const EN = {
|
||||
'Última modificação por': 'Last modified by',
|
||||
'Páginas': 'Pages',
|
||||
'Duração': 'Duration',
|
||||
// saúde do disco e confiança vindas do núcleo
|
||||
// disk health and confidence levels from the core
|
||||
'SSD (sem rotação)': 'SSD (no rotation)',
|
||||
'Cache de gravação': 'Write cache',
|
||||
'Segurança ATA': 'ATA security',
|
||||
@@ -490,13 +490,13 @@ export const EN = {
|
||||
'membro de RAID': 'RAID member',
|
||||
'sem rodapé/estrutura completa: pode estar truncado': 'no footer/complete structure: may be truncated',
|
||||
'vídeo sem índice (moov)': 'video without index (moov)',
|
||||
// visualizador de SQLite
|
||||
// SQLite viewer
|
||||
'Nenhuma tabela encontrada no esquema. A primeira página pode estar danificada.': 'No table found in the schema. The first page may be damaged.',
|
||||
'Tabela virtual: o conteúdo não fica armazenado como linhas comuns.': 'Virtual table: the content is not stored as ordinary rows.',
|
||||
'Tabela WITHOUT ROWID: as linhas ficam numa árvore de índice, ainda não exibida.': 'WITHOUT ROWID table: the rows live in an index tree, not shown yet.',
|
||||
'Arquivo truncado: o cabeçalho indica {n} páginas, mas só há {n}. Parte dos dados pode estar faltando.': 'Truncated file: the header says {n} pages, but there are only {n}. Part of the data may be missing.',
|
||||
|
||||
// mensagens de erro e avisos do núcleo (valores variáveis usam {n})
|
||||
// core error and warning messages (variable values use {n})
|
||||
'job {n} não existe': 'job {n} does not exist',
|
||||
'a varredura não está pausada': 'the scan is not paused',
|
||||
'A origem desta sessão não está disponível ({n}). Reconecte o disco ou escolha a origem para continuar.': 'The source of this session is unavailable ({n}). Reconnect the disk or choose the source to continue.',
|
||||
|
||||
+8
-8
@@ -1,7 +1,7 @@
|
||||
// Idioma da interface. O texto-fonte é em português; no modo inglês um tradutor
|
||||
// troca os textos exibidos (nós de texto e atributos) usando um dicionário, e
|
||||
// continua observando a página para traduzir o que for desenhado depois.
|
||||
// Textos com valores variáveis usam {n} no dicionário (ex.: "{n} arquivos" -> "{n} files").
|
||||
// UI language. Source strings are in Portuguese; in English mode a translator
|
||||
// replaces displayed strings (text nodes and attributes) using a dictionary, and
|
||||
// keeps observing the page to translate dynamically rendered elements.
|
||||
// Variable placeholders use {n} in the dictionary (e.g. "{n} files").
|
||||
|
||||
import { EN } from './i18n-en.js';
|
||||
|
||||
@@ -16,7 +16,7 @@ export function resolveLang(pref) {
|
||||
}
|
||||
|
||||
const ATTRS = ['placeholder', 'title', 'aria-label', 'alt', 'data-empty'];
|
||||
// Dados do usuário (nomes, caminhos, conteúdo) nunca são traduzidos.
|
||||
// User data (names, paths, content) is never translated.
|
||||
const SKIP = '.xp-cap, .xp-lnm, .db-table, .pv-zip, .hit, pre, code, .mono, .xp-ititle b, dd, textarea, input, script, style';
|
||||
|
||||
const exact = new Map(Object.entries(EN).filter(([k]) => !k.includes('{n}')));
|
||||
@@ -26,7 +26,7 @@ const patterns = Object.entries(EN)
|
||||
const parts = k.split('{n}').map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||
return { re: new RegExp('^' + parts.join('(.+?)') + '$', 's'), to: v };
|
||||
})
|
||||
// padrões mais específicos (mais texto fixo) primeiro
|
||||
// more specific patterns (more fixed text) first
|
||||
.sort((a, b) => b.re.source.length - a.re.source.length);
|
||||
|
||||
const cache = new Map();
|
||||
@@ -43,7 +43,7 @@ export function tr(text) {
|
||||
const m = core.match(p.re);
|
||||
if (m) {
|
||||
let i = 0;
|
||||
// os valores capturados também podem ter partes traduzíveis (ex.: nomes de tipos)
|
||||
// captured values may also contain translatable parts (e.g. type names)
|
||||
out = p.to.replace(/\{n\}/g, () => tr(m[++i] ?? ''));
|
||||
break;
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export function tr(text) {
|
||||
return lead + out + trail;
|
||||
}
|
||||
|
||||
const orig = new WeakMap(); // nó -> texto original em português
|
||||
const orig = new WeakMap(); // node -> original source text
|
||||
let observer = null;
|
||||
|
||||
function translateNode(n) {
|
||||
|
||||
+12
-12
@@ -1,8 +1,8 @@
|
||||
import { LOC } from './i18n.js';
|
||||
// Pré-visualização de arquivos recuperados.
|
||||
// O conteúdo é lido em streaming do núcleo (com Range) pela URL `url`; nada é
|
||||
// gravado em disco. Tipos: imagem, vídeo, áudio, PDF (pdf.js), texto, documentos
|
||||
// Office/OpenDocument (texto extraído), listagem de ZIP e visão hexadecimal.
|
||||
// Preview of recovered files.
|
||||
// Content is streamed from the core (with Range support) via `url`; nothing is
|
||||
// written to disk. Supported types: image, video, audio, PDF (pdf.js), text,
|
||||
// Office/OpenDocument documents (extracted text), ZIP listing, and hex view.
|
||||
|
||||
const IMG = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'avif', 'ico']);
|
||||
const VID = new Set(['mp4', 'm4v', 'mov', 'webm', 'mkv', 'ogv']);
|
||||
@@ -44,7 +44,7 @@ async function fetchRange(url, a, b) {
|
||||
return new Uint8Array(await r.arrayBuffer());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ ZIP mínimo
|
||||
// ------------------------------------------------------------------ minimal ZIP reader
|
||||
async function zipDir(url, size) {
|
||||
const tailLen = Math.min(size, 66000);
|
||||
const tail = await fetchRange(url, size - tailLen, size - 1);
|
||||
@@ -86,7 +86,7 @@ async function zipRead(url, e, cap = 8 << 20) {
|
||||
|
||||
const xml = (bytes) => new DOMParser().parseFromString(new TextDecoder().decode(bytes), 'application/xml');
|
||||
|
||||
// ------------------------------------------------------------------ renderizadores
|
||||
// ------------------------------------------------------------------ renderers
|
||||
function note(host, html, cls = '') {
|
||||
host.insertAdjacentHTML('beforeend', `<div class="pv-note ${cls}">${html}</div>`);
|
||||
}
|
||||
@@ -248,7 +248,7 @@ async function office(host, item, url, state) {
|
||||
const rows = [...d.getElementsByTagName('table:table-row')].slice(0, 80);
|
||||
html = `<div class="pv-sheet"><table><tbody>${rows.map((r) => `<tr>${[...r.getElementsByTagName('table:table-cell')].slice(0, 16).map((c) => `<td>${esc(c.textContent)}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`;
|
||||
} else {
|
||||
// pptx / odp: texto por slide
|
||||
// pptx / odp: per-slide text
|
||||
let slides;
|
||||
if (ext === 'pptx') {
|
||||
const files = ents.filter((e) => /^ppt\/slides\/slide\d+\.xml$/.test(e.name)).sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true })).slice(0, 60);
|
||||
@@ -275,9 +275,9 @@ async function zip(host, item, url, state) {
|
||||
note(host, `${ents.length} item(ns) no arquivo compactado.`);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ bancos de dados (SQLite)
|
||||
// ------------------------------------------------------------------ databases (SQLite)
|
||||
|
||||
/** Valores inteiros que parecem datas (Unix, Chrome/WebKit, macOS) viram uma dica legível. */
|
||||
/** Integer values that look like timestamps (Unix, Chrome/WebKit, macOS) become a readable hint. */
|
||||
function tsHint(v) {
|
||||
if (!Number.isInteger(v)) return null;
|
||||
let ms = null, kind = '';
|
||||
@@ -361,7 +361,7 @@ async function database(host, item, state, large, db, onOpen) {
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ API
|
||||
/** Desenha a pré-visualização de `item` dentro de `host`. Devolve uma função de limpeza. */
|
||||
/** Renders the preview of `item` inside `host`. Returns a cleanup function. */
|
||||
function mediaCard(host, item, kind, { large, inlineMedia, onOpen, onExternal }) {
|
||||
const ico = kind === 'video' ? '🎞' : '🎵';
|
||||
const canInline = inlineMedia && large;
|
||||
@@ -385,8 +385,8 @@ export function renderPreview(host, item, { url, large = false, thumb = false, i
|
||||
host.classList.remove('zoomable', 'actual');
|
||||
host.innerHTML = '<div class="pv-loading"><i></i></div>';
|
||||
const kind = kindOf(item);
|
||||
// Vídeo e áudio: nunca criamos <video>/<audio> no painel pequeno, e no Linux do app desktop
|
||||
// (WebKitGTK sem plugins de mídia) nem na tela grande, pois o processo da página morre.
|
||||
// Video and audio: never instantiate <video>/<audio> in the small panel, nor in the large view
|
||||
// on Linux desktop (WebKitGTK without media plugins), as the webview process would crash.
|
||||
if ((kind === 'video' || kind === 'audio') && (!large || !inlineMedia)) {
|
||||
mediaCard(host, item, kind, { large, inlineMedia, onOpen, onExternal });
|
||||
return () => { state.dead = true; };
|
||||
|
||||
Vendored
+2
-2
@@ -1,3 +1,3 @@
|
||||
# Bibliotecas de terceiros
|
||||
# Third-party libraries
|
||||
|
||||
- **pdf.js** 4.10.38 (build "legacy"), © Mozilla, licença Apache-2.0 (`PDFJS-LICENSE`). Usada só para pré-visualizar PDFs, localmente e sem internet. Origem: https://github.com/mozilla/pdf.js
|
||||
- **pdf.js** 4.10.38 ("legacy" build), © Mozilla, Apache-2.0 license (`PDFJS-LICENSE`). Used exclusively for local offline PDF preview. Source: https://github.com/mozilla/pdf.js
|
||||
Reference in new issue
Block a user