From f260e2ff764a12a6b9f2fecbf2d030181144ca82 Mon Sep 17 00:00:00 2001 From: Mischa Date: Sun, 21 Jun 2026 21:10:31 +0200 Subject: [PATCH] feat: mobile swipe triage UI + tag visualization - HammerJS swipe cards (right=journal, left=skip, up=story) with tilt/color feedback - Three tap buttons as swipe alternative (J/S/X) - Undo stack (max 10) with Back button - Progress bar + header counter sync - Thumbnail strip (all photos, colored dots, tap to jump) - Desktop: J/S/X badges on all tagged photos including skip Co-Authored-By: Claude Sonnet 4.6 --- .../travel-memories/app/templates/phase2.html | 101 ++++++++++++++++-- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/services/travel-memories/app/templates/phase2.html b/services/travel-memories/app/templates/phase2.html index 819e30b..867938d 100644 --- a/services/travel-memories/app/templates/phase2.html +++ b/services/travel-memories/app/templates/phase2.html @@ -47,11 +47,12 @@
{{ photo.local_datetime[11:16] }}
- {% if photo.tag != 'untagged' and photo.tag != 'skip' %} -
- {{ photo.tag[0] | upper }} -
+ {% if photo.tag == 'journal' %} +
J
+ {% elif photo.tag == 'story' %} +
S
+ {% elif photo.tag == 'skip' %} +
X
{% endif %} {% endfor %} @@ -100,6 +101,11 @@ class="btn btn-circle btn-lg btn-ghost border-2 border-info text-2xl" onclick="mobileApp && mobileApp.doTag('story')">S + + {# Thumbnail strip — all photos, colored dot per tag, tap to jump #} +
@@ -274,6 +280,7 @@ function mobileTriageApp(albumId, photos) { if (queue.length === 0) { showCompletion(); updateProgress(); + updateThumbStrip(); return; } @@ -382,6 +389,7 @@ function mobileTriageApp(albumId, photos) { updateProgress(); showCard(); + updateThumbStrip(); } // ── Undo ───────────────────────────────────────────────────────────── @@ -406,10 +414,90 @@ function mobileTriageApp(albumId, photos) { updateProgress(); showCard(); + updateThumbStrip(); + } + + // ── Thumbnail strip ────────────────────────────────────────────────── + + const thumbStrip = document.getElementById('m-thumb-strip'); + + function buildThumbStrip() { + thumbStrip.innerHTML = ''; + photos.forEach(photo => { + const wrap = document.createElement('div'); + wrap.className = 'relative flex-none cursor-pointer'; + wrap.style.cssText = 'width:44px;height:44px;'; + wrap.dataset.thumbId = photo.id; + wrap.addEventListener('click', () => jumpToPhoto(photo)); + + const img = document.createElement('img'); + img.src = `/proxy/thumb/${photo.id}`; + img.style.cssText = 'width:100%;height:100%;object-fit:cover;border-radius:4px;border:2px solid transparent;transition:border-color 0.15s;display:block;'; + img.draggable = false; + wrap.appendChild(img); + + const dot = document.createElement('div'); + dot.style.cssText = 'position:absolute;bottom:2px;left:2px;width:7px;height:7px;border-radius:50%;display:none;border:1px solid rgba(0,0,0,0.3);'; + wrap.appendChild(dot); + + thumbStrip.appendChild(wrap); + }); + updateThumbStrip(); + } + + function updateThumbStrip() { + const currentId = queue.length > 0 ? queue[0].id : null; + photos.forEach(photo => { + const wrap = thumbStrip.querySelector(`[data-thumb-id="${photo.id}"]`); + if (!wrap) return; + const img = wrap.querySelector('img'); + const dot = wrap.querySelector('div'); + + img.style.borderColor = photo.id === currentId ? '#fff' : 'transparent'; + img.style.boxShadow = photo.id === currentId ? '0 0 0 1px rgba(0,0,0,0.4)' : 'none'; + + if (photo.tag === 'journal') { + dot.style.display = ''; + dot.style.background = '#4ade80'; + } else if (photo.tag === 'story') { + dot.style.display = ''; + dot.style.background = '#38bdf8'; + } else if (photo.tag === 'skip') { + dot.style.display = ''; + dot.style.background = '#64748b'; + } else { + dot.style.display = 'none'; + } + }); + + if (currentId) { + const currentWrap = thumbStrip.querySelector(`[data-thumb-id="${currentId}"]`); + if (currentWrap) currentWrap.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); + } + } + + async function jumpToPhoto(photo) { + const queueIdx = queue.findIndex(p => p.id === photo.id); + if (queueIdx !== -1) queue.splice(queueIdx, 1); + + if (photo.tag !== 'untagged') taggedCount--; + const prevTag = photo.tag; + photo.tag = 'untagged'; + queue.unshift(photo); + + await fetch('/triage/tag', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ album_id: albumId, asset_id: photo.id, tag: 'untagged' }), + }); + + updateProgress(); + updateThumbStrip(); + showCard(); } // ── Public API ─────────────────────────────────────────────────────── - return { doTag, undo, showCard }; + return { doTag, undo, showCard, buildThumbStrip, updateThumbStrip }; } // ── View switching on load ─────────────────────────────────────────────────── @@ -427,6 +515,7 @@ document.addEventListener('DOMContentLoaded', () => { document.getElementById('m-progress-text').textContent = `${taggedCount} / ${photos.length} tagged`; document.getElementById('m-progress-bar').style.width = photos.length > 0 ? `${(taggedCount / photos.length) * 100}%` : '0%'; + mobileApp.buildThumbStrip(); mobileApp.showCard(); } // Desktop: nothing extra needed — Alpine handles it