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 <noreply@anthropic.com>
This commit is contained in:
@@ -47,11 +47,12 @@
|
|||||||
<div class="absolute bottom-0 left-0 right-0 text-[10px] text-white bg-black/40 px-1">
|
<div class="absolute bottom-0 left-0 right-0 text-[10px] text-white bg-black/40 px-1">
|
||||||
{{ photo.local_datetime[11:16] }}
|
{{ photo.local_datetime[11:16] }}
|
||||||
</div>
|
</div>
|
||||||
{% if photo.tag != 'untagged' and photo.tag != 'skip' %}
|
{% if photo.tag == 'journal' %}
|
||||||
<div class="absolute top-1 right-1 badge badge-xs
|
<div class="absolute top-1 right-1 badge badge-xs badge-success">J</div>
|
||||||
{% if photo.tag == 'journal' %}badge-success{% else %}badge-info{% endif %}">
|
{% elif photo.tag == 'story' %}
|
||||||
{{ photo.tag[0] | upper }}
|
<div class="absolute top-1 right-1 badge badge-xs badge-info">S</div>
|
||||||
</div>
|
{% elif photo.tag == 'skip' %}
|
||||||
|
<div class="absolute top-1 right-1 badge badge-xs badge-ghost opacity-60">X</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -100,6 +101,11 @@
|
|||||||
class="btn btn-circle btn-lg btn-ghost border-2 border-info text-2xl"
|
class="btn btn-circle btn-lg btn-ghost border-2 border-info text-2xl"
|
||||||
onclick="mobileApp && mobileApp.doTag('story')">S</button>
|
onclick="mobileApp && mobileApp.doTag('story')">S</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# Thumbnail strip — all photos, colored dot per tag, tap to jump #}
|
||||||
|
<div id="m-thumb-strip"
|
||||||
|
class="mt-3 flex gap-1.5 overflow-x-auto pb-2"
|
||||||
|
style="scrollbar-width:thin;-webkit-overflow-scrolling:touch"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -274,6 +280,7 @@ function mobileTriageApp(albumId, photos) {
|
|||||||
if (queue.length === 0) {
|
if (queue.length === 0) {
|
||||||
showCompletion();
|
showCompletion();
|
||||||
updateProgress();
|
updateProgress();
|
||||||
|
updateThumbStrip();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,6 +389,7 @@ function mobileTriageApp(albumId, photos) {
|
|||||||
|
|
||||||
updateProgress();
|
updateProgress();
|
||||||
showCard();
|
showCard();
|
||||||
|
updateThumbStrip();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Undo ─────────────────────────────────────────────────────────────
|
// ── Undo ─────────────────────────────────────────────────────────────
|
||||||
@@ -406,10 +414,90 @@ function mobileTriageApp(albumId, photos) {
|
|||||||
|
|
||||||
updateProgress();
|
updateProgress();
|
||||||
showCard();
|
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 ───────────────────────────────────────────────────────
|
// ── Public API ───────────────────────────────────────────────────────
|
||||||
return { doTag, undo, showCard };
|
return { doTag, undo, showCard, buildThumbStrip, updateThumbStrip };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── View switching on load ───────────────────────────────────────────────────
|
// ── 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-text').textContent = `${taggedCount} / ${photos.length} tagged`;
|
||||||
document.getElementById('m-progress-bar').style.width =
|
document.getElementById('m-progress-bar').style.width =
|
||||||
photos.length > 0 ? `${(taggedCount / photos.length) * 100}%` : '0%';
|
photos.length > 0 ? `${(taggedCount / photos.length) * 100}%` : '0%';
|
||||||
|
mobileApp.buildThumbStrip();
|
||||||
mobileApp.showCard();
|
mobileApp.showCard();
|
||||||
}
|
}
|
||||||
// Desktop: nothing extra needed — Alpine handles it
|
// Desktop: nothing extra needed — Alpine handles it
|
||||||
|
|||||||
Reference in New Issue
Block a user