docs: refresh docs and screenshots, drop em dashes, add scripts/screenshots.sh

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-11-03 20:44:46 -03:00
1 parent 0b9a16e8a0
commit 13f380291f
24 files changed
+213 -26

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Regera as capturas de tela de docs/img a partir da interface real do Guará.
#
# Dados: uma imagem FAT32 sintética com 72 fotos JPEG apagadas (EXIF falso de câmera, data e
# GPS), gerada por scripts/make-photo-fixture.py e encontrada por uma varredura de verdade. A
# lista de dispositivos do computador é substituída por um nome genérico. Nenhum dado real.
#
# scripts/screenshots.sh # grava em docs/img
# OUT=/tmp/img scripts/screenshots.sh # grava em outro diretório
#
# Gera explorer-{dark,light}-{pt,en}, folders-light-{pt,en} e settings-language-{pt,en}.
# Variáveis: OUT, GUARA_BIN (padrão target/release/guara; sempre recompilado se não for dado,
# porque a interface vai embutida no binário), CHROME, PUPPETEER_CORE (padrões: Chrome do cache
# do puppeteer e puppeteer-core em ~/.claude/tools/node_modules).
# Requer: python3, ImageMagick (convert), mkfs.vfat e mtools (mmd, mcopy, mdel), node e Chrome.
# Usa /tmp/test-photos.img (a imagem aparece com esse caminho na barra lateral das capturas).
set -euo pipefail
root=$(cd "$(dirname "$0")/.." && pwd)
out=${OUT:-$root/docs/img}
image=/tmp/test-photos.img
if [ -z "${GUARA_BIN:-}" ]; then
(cd "$root" && cargo build --release -p guara-cli)
GUARA_BIN=$root/target/release/guara
fi
mkdir -p "$out"
python3 "$root/scripts/make-photo-fixture.py" "$image" 72
trap 'rm -f "$image"' EXIT
node "$root/scripts/screenshots/shots.mjs" "$GUARA_BIN" "$image" "$out"
echo "capturas gravadas em $out"
+129
View File
@@ -0,0 +1,129 @@
// Captura a interface do Guará (explorer, vista de pastas e configurações) nos temas
// claro/escuro e nos idiomas pt/en. Chamado por scripts/screenshots.sh:
// node shots.mjs <binário guara> <imagem de teste> <dir-saída>
import { execFileSync, spawn } from "node:child_process";
import { homedir } from "node:os";
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const [bin, image, out] = process.argv.slice(2);
const core = process.env.PUPPETEER_CORE || `${homedir()}/.claude/tools/node_modules/puppeteer-core/lib/esm/puppeteer/puppeteer-core.js`;
const { default: puppeteer } = await import(core);
const chrome = process.env.CHROME || execFileSync("bash", ["-c", `ls -1d "$HOME"/.cache/puppeteer/chrome/*/chrome-linux64/chrome | sort -V | tail -1`], { encoding: "utf8" }).trim();
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// Dispositivo fictício: a lista real do computador não deve aparecer nas capturas.
const FAKE_DEVICES = [{ path: "/dev/nvme0n1", name: "SSD interno", kind: "NVMe", size: 954 * 1024 ** 3, readable: false, mounted_at: [], removable: false }];
const LABEL = "Pendrive 64 GB";
const variants = [
{ theme: "dark", lang: "pt" }, { theme: "dark", lang: "en" },
{ theme: "light", lang: "pt" }, { theme: "light", lang: "en" },
];
// Sobe `guara ui` com config isolada (sessões e ajustes próprios) e devolve a URL com o token.
async function startServer(theme, lang) {
const cfg = mkdtempSync(join(tmpdir(), "guara-shots-"));
mkdirSync(cfg, { recursive: true });
writeFileSync(join(cfg, "settings.json"), JSON.stringify({ theme, lang, notify: true, maps: false, updates: false }));
const child = spawn(bin, ["ui", "--no-open", "--port", "0"], { env: { ...process.env, GUARA_CONFIG_DIR: cfg }, stdio: ["ignore", "ignore", "pipe"] });
const url = await new Promise((resolve, reject) => {
let buf = "";
child.stderr.on("data", (d) => { buf += d; const m = buf.match(/http:\/\/\S+/); if (m) resolve(m[0]); });
child.on("exit", () => reject(new Error("guara ui saiu antes de subir")));
setTimeout(() => reject(new Error("timeout subindo guara ui")), 20000);
});
return { url, stop: () => { child.kill(); rmSync(cfg, { recursive: true, force: true }); } };
}
async function newPage(browser, url, w = 1600, h = 940) {
const page = await browser.newPage();
await page.setViewport({ width: w, height: h, deviceScaleFactor: 1 });
await page.setRequestInterception(true);
page.on("request", (req) => {
if (req.url().includes("/rpc/devices")) {
req.respond({ status: 200, contentType: "application/json", body: JSON.stringify(FAKE_DEVICES) });
} else req.continue();
});
await page.goto(url, { waitUntil: "networkidle0" });
await sleep(1200);
return page;
}
// Abre a imagem de teste, faz a varredura profunda e espera terminar.
async function scanImage(page) {
await page.click("#openImageBtn");
await sleep(400);
await page.type("#dlg input[name=v]", image);
await page.click("#dlg button[value=ok]");
await sleep(1200);
await page.click("#goDeep");
await page.waitForFunction(() => document.querySelector(".xp-tile, .xp-lrow"), { timeout: 120000 });
await page.waitForFunction(() => !document.querySelector("#progressArea .progress-card"), { timeout: 120000 });
await sleep(2500); // miniaturas e autosave da sessão
}
const relabel = (page) => page.evaluate((t) => {
const sub = document.querySelector("#scanSub"); if (sub) sub.innerHTML = `<span class="mono">${t}</span>`;
}, LABEL);
const browser = await puppeteer.launch({ executablePath: chrome, args: ["--no-sandbox"] });
try {
for (const { theme, lang } of variants) {
const srv = await startServer(theme, lang);
try {
// 1ª rodada: só para deixar uma sessão salva em "Recentes" (a 2ª rodada cria a outra).
let page = await newPage(browser, srv.url);
await scanImage(page);
await page.close();
page = await newPage(browser, srv.url);
await page.evaluate(() => { try { localStorage.setItem("guara-view", "grid"); } catch {} });
await page.reload({ waitUntil: "networkidle0" });
await sleep(1000);
await scanImage(page);
await relabel(page);
// Explorador em grade, com a primeira foto marcada.
const chk = await page.$(".xp-tile .xp-chk");
if (chk) await chk.click(); else await (await page.$(".xp-tile")).click();
await sleep(800);
await page.screenshot({ path: `${out}/explorer-${theme}-${lang}.png` });
console.log("ok", `explorer-${theme}-${lang}`);
if (theme === "light") {
// Vista de pastas: pasta DCIM marcada e selecionada.
await page.click('button[data-view="folders"]');
await sleep(1000);
const dcim = await page.$('.xp-lrow.xp-dir[data-dir="0"]');
await (await dcim.$(".xp-chk")).click();
await sleep(400);
await dcim.click({ offset: { x: 200, y: 20 } });
await sleep(800);
await relabel(page);
await page.screenshot({ path: `${out}/folders-${theme}-${lang}.png` });
console.log("ok", `folders-${theme}-${lang}`);
await page.click('button[data-view="grid"]');
await sleep(500);
}
if (theme === "light") {
// Diálogo de configurações, recortado com a moldura borrada ao redor.
await page.click("#settingsBtn");
await sleep(900);
const box = await (await page.$("#dlg")).boundingBox();
const pad = 24;
await page.screenshot({
path: `${out}/settings-language-${lang}.png`,
clip: { x: Math.max(0, box.x - pad), y: Math.max(0, box.y - pad), width: box.width + 2 * pad, height: box.height + 2 * pad },
});
console.log("ok", `settings-language-${lang}`);
}
} finally {
srv.stop();
}
}
} finally {
await browser.close();
}