Commit Graph

13 Commits

Author SHA1 Message Date
695e2417cc gtauri viewer: mid-commit touch/key takes over the swipe (chained swipes)
A second swipe while the new image was still sliding in got dropped
(pointerdown bailed while the commit tween was in flight). Now a
touch — or arrow key — during a commit takes it over: the commit
finishes instantly (roles swap at the settle positions, same
dimension-adoption path as natural completion) and the input starts a
fresh gesture on the landed image, so rapid successive swipes chain
through multiple pictures. A plain tap during a commit simply settles
it early.

Verified on the emulator: two 100 ms flicks 120 ms apart (the second
lands mid-commit) end on the exact same image — pixel-identical — as
two deliberate slow swipes.
2026-11-03 13:47:39 -05:00
cd31b76889 gtauri viewer: velocity-scaled swipe commit (faster flicks)
Flicks now continue the finger's motion instead of taking a fixed
200 ms: commit duration = remaining distance / release velocity
(15 % snappier), clamped 80-240 ms, ease-out. A full-speed flick
releasing past half the screen commits in ~80 ms (was 200). Slow
releases past the 25 % threshold still settle gently (180 ms
smoothstep; 150 ms for keyboard nav).

Verified on the emulator: a 100 ms flick is fully settled before
the first post-release screencap (~150 ms), with only a 5 px
edge artifact between consecutive frames (no residual motion).
2026-11-03 13:28:43 -05:00
ba646fb233 gtauri viewer: adopt incoming dimensions at swipe commit (aspect fix)
Settling on a different-aspect neighbor (e.g. landscape → portrait)
stretched: vimg kept the OLD image's width/height styles while showing
the new thumbnail, so the new pixels rendered in the old box until
full-res landed and fitViewer() reset the dimensions — a visible
vertical stretch then snap-back.

At the commit swap, adopt the incoming image's dimensions from the
drag preview (nav already knows w/h/scale from its loaded thumb):
base/vScale/width/height/transform are all set consistently in the
same task as the src swap, so the first settled frame is correct.
Fallback: if the preview never loaded (keyboard nav / slow thumb),
use the plain openViewer path where the thumb round-trip sets the
dimensions before first paint.

Verified on the emulator with a 4:3 landscape + 3:4 portrait pair:
settled content box 1439x1921 (0.749 = exact 3:4) from the first
settled frame, unchanged after the full-res swap.
2026-11-03 13:15:55 -05:00
029780cc09 gtauri viewer: two-image swipe commit — outgoing stays until off-screen
Fix: on release the old image vanished instantly and only the new one
slid in. Now both images live on the page strip for the whole commit:
vimg (outgoing) slides off-screen while vimg2 (incoming, the drag
preview) slides home, over a 200 ms smoothstep; only then do the
elements swap roles (vimg.src = the already-decoded preview image,
vimg2 hidden after the swap paints — no blank frame).

Details:
- navigate() returns whether it committed; at the library edges it
  refuses and the drag snaps back (previously a past-the-edge drag
  could leave the image stuck mid-strip).
- commit tweens are locked: pointer/dblclick input is ignored for the
  ~200 ms so a second gesture can't fight the animation.
- openViewer gains a keepVisible (promoted) path that skips the
  hide+thumb round-trip; vFullSeq reset per open (safety-net counter
  could previously exhaust after ~11 opens).
- removed the now-dead slideFrom slide-in mechanism (its purpose is
  subsumed by the two-image commit).

Verified on the emulator: mid-commit frame shows both images
(outgoing sliding off left, incoming from right); snap-back slides
both back; edge drag right on the first image snaps home.
2026-11-03 13:04:53 -05:00
16cd868f22 gtauri: pure-push watching only; one emit per burst; live swipe preview
Watch de-duplication (per the plan review): FileObserver (inotify,
push) and MediaStore ContentObserver (URI push) are the ONLY change
detectors. Removed: the 100 ms recv_timeout tick, the fast_dir_sig
mtime-diff watcher, collect_watched_dirs, changed_dirs, dir_sig,
rescan_subtrees, and the 5-minute full-rescan timer thread.
watch_files now blocks on the push channel; on Android each wake
coalesces ~50 ms so a burst of N file events becomes one payload
batch, one SQLite connection, and ONE grid emit (verified on the
emulator: 20-file copy = 1-2 emits, 20-file delete = 1 emit,
single add still lands in ~75-95 ms).

FileObserver rewritten: recursive registration (Pictures/DCIM tree,
cap 5000 dirs) and self-extending — a CREATE/MOVED_TO on a directory
spawns a new observer for it, so new albums are watched too.
tif/tiff added to the image filter.

Dead code removed (the Tauri port never wired them): scan::walk,
walk_dir_nostats, walk_dir_into, mtime_ns, decode_jpeg (full-size),
images::thumb_png, db::set_thumb, db::list_entries, db::count,
db::delete_by_media_id; Thumb shrunk to {rgb,width,height} (the
grid is uniform squares; the viewer uses full-res). Tests now
compile (tempfile dev-dependency was missing — the test module
never built) and were fixed to match the current API.

Viewer: live swipe preview — while a horizontal drag is in flight at
fit scale, the picture being swiped in renders on the page strip
(second <img>, its grid thumb, a ~1 ms mem hit) and tracks the
finger, so there is no blank side. On commit the primary image takes
over from exactly where the preview was; on snap-back the preview
slides back with the tween. Verified on the emulator (two images
visible mid-drag, clean slide home).

Not re-verified: 120 Hz jank (per instruction — measure once when
settled). Desktop paths untouched (deferred).
2026-11-03 12:49:30 -05:00
bca6a9de4a gtauri viewer: horizontal swipe navigation (left/right to next/previous)
Standard gallery swipe: at fit scale a horizontal drag follows the
finger 1:1 (rubber-banded past the page edge); on release, past 25% of
the width or a fast flick (>0.35 px/ms), the neighbor in the list
opens with a 150 ms ease-out slide-in from the drag direction.
Short drags snap back; vertical drags at fit are inert; zoomed drags
still pan and pinch still zooms. Arrow keys navigate on desktop.

Verified on the emulator: left swipes advance, right swipe returns to
the previous image pixel-identical, 80px drag and vertical drag do not
navigate.
2026-11-03 11:23:41 -05:00
1bc1d6beb3 gtauri: fix camera-photo latency, delete blank boxes, truncation guard
- Camera-photo latency: root cause was READ_MEDIA_IMAGES never
  granted (granted=false on the phone), so the MediaStore fast-path
  query silently returned nothing and photos only appeared via the
  slow full FUSE walk. The app now requests the permission at
  runtime (once, with a single retry on denial). The camera writes
  through MediaStore at capture time, so the ContentObserver fires
  immediately and the photo appears in ~1 s.
- Delete blank boxes: two JS flaws — (1) no img onerror handler, so a
  cell that requested a thumb for an already-deleted file (404) sat
  as an empty dark box forever; (2) recycled cells were matched by
  slot index only, so after a deletion shifted the list a cell could
  keep a stale id. Now: onerror drops stale ids (idSet membership) or
  retries transient ones, and cells reassign on idx OR id change.
- Truncation guard (chicken-and-egg): the mass-delete suspicion
  threshold was 20% but observed FUSE truncations lose 15-17%, which
  sailed under it and poisoned the DB. Now 10%; a confirmed deletion
  (two-strike passed) lowers the new walked_max watermark, a refused
  one keeps it. Layer-2 re-walk compares against max(db, walked_max)
  so a poisoned (shrunk) DB cannot silence truncation detection.
  Emulator suite: healthy→truncated (all 3 layers, 0 loss),
  poisoned-DB→truncated (layer 2 now fires), healthy walk self-heals.
- Verified on the phone: DB matches disk exactly (8,398 Camera +
  27 screenshots + 11 messaging + 2 bluesky = 8,438); an earlier
  10,166 count was a transient user-system state, not truncation.
2026-11-02 08:01:13 -05:00
9e52dc0355 gtauri: MediaStore fast path + js-ready handshake + virtualized grid
- MediaStore fast path: Kotlin's ContentObserver queries MediaStore
  for images added in the last 120s and writes path|added|size lines
  to .ms_recent, then wakes Rust via the proven zero-arg
  nativeFsChange() JNI. Rust upserts those rows and emits
  immediately — a camera photo appears in ~1s without waiting for
  the ~10s FUSE re-read of a 10,000-file directory. (Direct
  Kotlin->Rust JNI string passing and Rust->Java calls crash under
  the emulator's x86_64->arm64 native bridge; file handoff plus
  zero-arg JNI is the only reliable channel.)
- js-ready handshake: the first gallery-files emit fires ~30 ms
  after process start, before the WebView's JS exists (Tauri events
  are fire-and-forget, so it was lost). JS now signals
  'gtauri-js-ready'; Rust re-emits the current state.
- Virtualized grid: the document is ONE spacer (full scroll height)
  plus ONE band of recycled cells pinned by transform — ~80 DOM
  nodes total for a 10,000-photo library. Launch cost no longer
  scales with library size; the first screen renders as soon as
  the WebView boots and the first thumbs arrive.
- 'Missing photos' report resolved: the full walk of the 9,961-file
  Camera dir enumerates exactly 8,438 images with no guard firing;
  the difference is videos, which a photo gallery correctly
  excludes.
2026-11-02 00:02:48 -05:00
bd27a7ccc1 gtauri: fix live watching (incremental subtree rescans)
Root cause of 'adds/removes not showing up': the watcher's file-COUNT
tree signature took 8-10 s per tick on the phone (FUSE readdir of the
8400-file Camera dir is ~1 ms/entry), so the 1 s poll was effectively
a 10 s walk, and the old 30 s forced FULL rescan (walk+stat of every
file, ~30 s) kept the watcher thread perpetually busy.

New design:
- 1 s tick stats DIRECTORY mtimes only (~1 ms). Verified on the phone:
  Android's FUSE DOES update dir mtime on file add/remove (stat
  before/after a copy showed it) — the earlier 'never fires' reading
  was wrong; no files had actually been added in that trace window.
- On a changed dir: incremental rescan of ONLY that subtree. Small
  dirs (<=2000 files) get a full walk with stat (adds, deletes AND
  in-place edits). The huge Camera dir gets a readdir-only walk: new
  files are stat'd, known files keep their DB mtime/size, deletions
  are detected by set difference. Everything outside the changed
  subtree keeps its DB metadata — cost is proportional to the change.
- Full rescan every 5 min as the safety net (in-place edits in the
  huge dir, anything missed).
- DB: busy_timeout(10s) so the startup scan and watcher can transact
  concurrently (WAL).

Measured on the phone: add to Pictures -> grid +1 in ~5 s; add to
Camera -> +1 in ~12 s; removals picked up on the next tick. Also
observed tracking a live 1700-photo user import (grid 8440 -> 10166
as the import progressed).

JS: event payload normalizer for thumb-ready/full-ready (object vs
JSON string across WebView builds), and js files/emit instrumentation
to compare send vs receive counts.
2026-11-01 18:55:31 -05:00
312d87f15d gtauri: pinch zoom about pointer midpoint + live filesystem watching
Pinch: the image now follows the hand (midpoint drift) and zooms
ABOUT the midpoint — the image point under the fingers stays put
while the scale changes. Per-frame delta math, clamped as before
(fit min, image-edge pan limits).

Live watching: new photos appear in the grid within ~2s of being
written. inotify does not work on Android's FUSE-backed external
storage, so the watcher polls a tree signature (dir mtimes only —
a few dozen stats/s) every second; on change (or a 30s safety-net
full pass) it debounces 600ms for the write burst, then runs the
incremental rescan (walk + DB diff + emit) off the main thread.

Supporting changes:
- Staged corruption: a decode failure no longer marks an id broken
  immediately — 3 consecutive failures are required, and a success
  clears the count. A photo still being written fails once or twice
  and then decodes; genuine garbage gives up and stays gray.
- Grid rebuild preserves the viewport across a live rescan (pins to
  the cell that was on top), so a new photo landing at the top does
  not jump the scroll position.
2026-11-01 18:02:09 -05:00
8f700e7f66 gtauri: clamp viewer pan/zoom to image edges
Pan is now limited to the image bounds (|offset| <= (rendered -
viewport)/2 per axis), so the image can never be dragged off-screen
leaving blank space around it; on a letterboxed axis panning is
locked to centered. Pinch can no longer zoom out past fit. Clamping
lives in vApply() so pan, pinch and reset all share it; still
compositor-only (transform only).
2026-11-01 17:44:14 -05:00
02d1f097ac gtauri: fix deep-scroll stall + instant viewer
Root causes found via per-event tracing (trace.log) + heartbeats:

1. Decode backlog: single worker at ~300ms/photo could not keep up with
   fast long scrolls — phone session ended with a 2400-deep queue
   draining at ~3 cells/s (13+ min to catch up). That was the
   'scroll far down and nothing loads no matter how long I wait' zone.
   - 4 parallel decode workers (~12 photos/s)
   - viewport-aware stale drops: prefetch jobs more than 60 rows behind
     the viewport are dropped, not decoded (fleeing leaves no backlog)
   - Q_CAP bounds the queue; JS 1.5s safety net re-requests
   - queue peak in repro test: 42 (was 2405); all 850 decodable
     emulator images now load (708 before)

2. Stale-anchor bug: sendAnchor was leading-edge throttled with no
   trailing call — if a scroll ended inside the 150ms window the Rust
   anchor stayed stale forever, and the worker then dropped fresh
   URGENT jobs (tap → full read) as 'off-screen'. Fix: trailing
   anchor send; and URGENT jobs are never stale-dropped (only
   prefetch work is droppable).

3. Viewer latency: full-res read ran on Chromium's main thread
   (wry shouldInterceptRequest is synchronous) — 30-50MB flash read
   froze the whole WebView. Now: thumb shown instantly (mem cache,
   ~1ms), full read queued urgent and done off-main; full-ready event
   swaps in the full-res (256MB full-res LRU makes re-opens instant).
   Measured click→full-res: 4ms on emulator.

4. Scan: skip OS junk (._* AppleDouble forks, dotfiles) — the phone's
   DCIM carried 72 of them, rendering as permanent gray cells.

5. Viewer for a corrupt file now shows 'image unavailable' instead of
   'loading…' forever; grid retry timers skip far-off-screen cells.
2026-11-01 09:11:11 -05:00
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