gallery/harnesses/gtauri/frontend/dist/index.html
gmp e019a950b1 gtauri: Tauri v2 gallery harness WORKING on emulator
Chromium owns scroll/compositor/lazy-load/image-cache; Rust owns
scan/SQLite/thumbnail-decode behind a gallery:// custom protocol.

Hard-won gotchas (see in-code comments):
- Android: no native custom-scheme support → wry rewrites the URL to
  http://gallery.localhost/... (custom_protocol_workaround). The JS
  MUST request that form; the handler receives the reverted
  gallery://localhost/... and must strip the authority.
- tauri_build::build() in build.rs must run UNCONDITIONALLY (build
  scripts run on the host: a cfg(target_os) gate silently disables it,
  which skips the rustc-cfg=mobile emission → no mobile_entry_point →
  .so with no JNI symbols → UnsatisfiedLinkError).
- build.rs must list frontend files for rerun-if-changed (a dir-level
  directive only watches the dir mtime, not content edits) — the .so
  bakes the frontend at compile time and cargo otherwise ships stale
  UI with a 'successful' build.
- logcat drops ~75% of lines under load: use the file trace.
- fetch() is CORS-restricted (page origin http://tauri.localhost);
  <img> is not — add ACAO:* anyway.

Emulator (5853 real files): grid renders, lazy thumbs via
gallery:// with on-disk cache (87 files 35-72KB), FR log clean at
60Hz, viewer opens full-res with compositor-only pan/zoom. The single
'broken' cell is a 17-byte fake test JPEG (correct behavior).
2026-10-31 18:59:51 -04:00

230 lines
6.7 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Gallery</title>
<style>
/* Chromium owns: layout, compositor-driven scroll, lazy fetch+decode,
image cache/eviction, viewer compositing. Rust owns: scan, SQLite,
on-disk thumbnail cache behind gallery://. */
html, body {
margin: 0; padding: 0;
background: #0a0a0c;
overscroll-behavior: none;
-webkit-user-select: none; user-select: none;
}
body { overflow-y: scroll; padding-top: 26px; }
#status {
position: fixed; top: 0; left: 0; right: 0; height: 26px;
display: flex; align-items: center; padding: 0 12px;
background: rgba(10,10,12,0.87);
color: #cfd2da; font: 14px/26px system-ui, sans-serif;
z-index: 10;
}
#grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
padding: 4px;
}
.cell {
aspect-ratio: 1 / 1;
overflow: hidden;
background: #1c1c22;
border-radius: 2px;
}
/* explicit 1:1 box via aspect-ratio → zero layout shift on arrival */
.cell img {
width: 100%; height: 100%;
object-fit: cover;
display: block;
}
/* ---- viewer ---- */
#viewer {
position: fixed; inset: 0;
background: #000;
z-index: 100;
display: none;
touch-action: none;
}
#viewer.open { display: block; }
#vimg {
position: absolute; left: 50%; top: 50%;
max-width: none;
transform-origin: 0 0;
will-change: transform;
/* compositor-only animation: pan/zoom never re-rasterizes */
}
#vclose {
position: fixed; top: 8px; right: 8px;
width: 44px; height: 44px;
border-radius: 22px;
background: rgba(20,20,24,0.7);
color: #fff; font: 26px/40px system-ui;
text-align: center;
z-index: 101;
touch-action: none;
}
#vload {
position: fixed; left: 0; right: 0; top: 50%;
color: #888; text-align: center;
font: 14px system-ui;
z-index: 99;
}
</style>
</head>
<body>
<div id="status"><span id="stext">starting…</span></div>
<div id="grid"></div>
<div id="viewer">
<div id="vload"></div>
<img id="vimg" alt="">
<div id="vclose">×</div>
</div>
<script>
"use strict";
const grid = document.getElementById("grid");
const stext = document.getElementById("stext");
const viewer = document.getElementById("viewer");
const vimg = document.getElementById("vimg");
const vload = document.getElementById("vload");
let total = 0, loaded = 0, lastSig = "", scanning = false;
function statusLine() {
stext.textContent = total + " photos" +
(scanning ? " • scanning…" : "") +
(loaded < total ? " (" + loaded + ")" : "");
}
function rebuild(ids, isScanning, found) {
const sig = ids.length + ":" + (ids[0] ?? 0) + ":" + (ids[ids.length - 1] ?? 0);
if (sig === lastSig && scanning === isScanning) return;
lastSig = sig;
scanning = isScanning;
total = ids.length; loaded = 0;
grid.replaceChildren();
const frag = document.createDocumentFragment();
for (const id of ids) {
const c = document.createElement("div");
c.className = "cell";
const im = new Image();
im.loading = "lazy";
im.decoding = "async";
im.src = "http://gallery.localhost/thumb/" + id;
im.addEventListener("load", () => { loaded++; statusLine(); });
im.addEventListener("click", () => openViewer(id));
c.appendChild(im);
frag.appendChild(c);
}
grid.appendChild(frag);
statusLine();
}
try {
window.__TAURI__.event.listen("gallery-files", (e) => {
const p = e.payload || {};
rebuild(p.ids || [], !!p.scanning, p.found || 0);
});
} catch (err) { stext.textContent = "no tauri bridge: " + err; }
/* ---------------- frame-rate logger (FR lines for tools/fr_capture.py) ---- */
let frLast = performance.now(), frLines = 0, frBatch = [];
function frFlush() {
if (frBatch.length === 0) return;
frBatch.push("IMG t=" + frLast.toFixed(1) + " loaded=" + loaded + "/" + total +
" scrollY=" + Math.round(window.scrollY));
const payload = frBatch.join("\n") + "\n";
frBatch = [];
try { window.__TAURI__.event.emit("gtauri-fr", payload); } catch (e) {}
}
function frTick(now) {
const work = performance.now() - now;
frBatch.push("FR " + now.toFixed(2).padStart(11) +
" gap=" + (now - frLast).toFixed(2).padStart(7) +
" cost=" + work.toFixed(2).padStart(6) + " drew=1");
frLast = now;
if (++frLines % 120 === 0) frFlush();
requestAnimationFrame(frTick);
}
requestAnimationFrame(frTick);
/* ---------------- viewer: compositor-only pan/zoom ---------------- */
let vScale = 1, vTx = 0, vTy = 0;
const base = { w: 0, h: 0 };
function vApply() {
vimg.style.transform =
"translate(" + (vTx - base.w * vScale / 2) + "px," + (vTy - base.h * vScale / 2) + "px) scale(" + vScale + ")";
}
function openViewer(id) {
vload.textContent = "loading…";
vimg.style.visibility = "hidden";
vScale = 1; vTx = 0; vTy = 0;
vimg.onload = () => {
base.w = vimg.naturalWidth; base.h = vimg.naturalHeight;
const s = Math.min(innerWidth / base.w, innerHeight / base.h);
vScale = s;
vimg.style.width = base.w + "px";
vimg.style.height = base.h + "px";
vimg.style.visibility = "visible";
vload.textContent = "";
vApply();
};
vimg.src = "http://gallery.localhost/full/" + id;
viewer.classList.add("open");
}
function closeViewer() {
viewer.classList.remove("open");
vimg.src = "";
}
document.getElementById("vclose").addEventListener("click", closeViewer);
addEventListener("keydown", (e) => { if (e.key === "Escape") closeViewer(); });
/* pointer-based pan + pinch (touch-action:none on #viewer keeps the browser
from stealing the gesture) */
const pts = new Map();
let pinchD0 = 0, pinchS0 = 1;
viewer.addEventListener("pointerdown", (e) => {
viewer.setPointerCapture(e.pointerId);
pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
if (pts.size === 2) {
const [a, b] = [...pts.values()];
pinchD0 = Math.hypot(a.x - b.x, a.y - b.y);
pinchS0 = vScale;
}
});
viewer.addEventListener("pointermove", (e) => {
const p = pts.get(e.pointerId);
if (!p) return;
if (pts.size === 1) {
vTx += e.clientX - p.x;
vTy += e.clientY - p.y;
p.x = e.clientX; p.y = e.clientY;
vApply();
} else if (pts.size === 2) {
p.x = e.clientX; p.y = e.clientY;
const [a, b] = [...pts.values()];
const d = Math.hypot(a.x - b.x, a.y - b.y);
if (pinchD0 > 0) vScale = Math.max(0.05, pinchS0 * d / pinchD0);
vApply();
}
});
const endPt = (e) => { pts.delete(e.pointerId); };
viewer.addEventListener("pointerup", endPt);
viewer.addEventListener("pointercancel", endPt);
viewer.addEventListener("dblclick", () => {
vScale = Math.min(innerWidth / base.w, innerHeight / base.h);
vTx = 0; vTy = 0; vApply();
});
</script>
</body>
</html>