/* * photo.js * Photo upload for all entity types and the mobile Photo Queue feature. * * Photo upload: * triggerPhoto(type, id) — opens the hidden file input, sets _photoTarget. * The 'change' handler uploads via multipart POST, updates the tree node, * and on mobile automatically runs the full AI pipeline for books * (POST /api/books/{id}/process). * * Photo Queue (mobile-only UI): * collectQueueBooks(node, type) — collects all non-approved books in tree * order (top-to-bottom within each shelf, left-to-right across shelves). * renderPhotoQueue() — updates the #photo-queue-overlay DOM in-place. * Queue flow: show spine → tap camera → upload + process → auto-advance. * Queue is stored in _photoQueue (state.js) so events.js can control it. * * Depends on: S, _photoQueue (state.js); req, toast (api.js / helpers.js); * walkTree, findNode, esc (tree-render.js / helpers.js); * isDesktop, render (helpers.js / init.js) * Provides: collectQueueBooks(), renderPhotoQueue(), triggerPhoto() */ // ── Photo Queue ────────────────────────────────────────────────────────────── function collectQueueBooks(node, type) { const books = []; function collect(n, t) { if (t === 'book') { if (n.identification_status !== 'user_approved') books.push(n); return; } if (t === 'room') n.cabinets.forEach(c => collect(c, 'cabinet')); if (t === 'cabinet') n.shelves.forEach(s => collect(s, 'shelf')); if (t === 'shelf') n.books.forEach(b => collect(b, 'book')); } collect(node, type); return books; } function renderPhotoQueue() { const el = document.getElementById('photo-queue-overlay'); if (!el) return; if (!_photoQueue) { el.style.display = 'none'; el.innerHTML = ''; return; } const {books, index, processing} = _photoQueue; el.style.display = 'flex'; if (index >= books.length) { el.innerHTML = `
Photo Queue
All done!
All ${books.length} book${books.length !== 1 ? 's' : ''} photographed
`; return; } const book = books[index]; el.innerHTML = `
${index + 1} / ${books.length}
Spine
${esc(book.title || '—')}
${processing ? '
Processing…
' : ''}`; } // ── Photo upload ───────────────────────────────────────────────────────────── const gphoto = document.getElementById('gphoto'); function triggerPhoto(type, id) { S._photoTarget = {type, id}; if (/Android|iPhone|iPad/i.test(navigator.userAgent)) gphoto.setAttribute('capture','environment'); else gphoto.removeAttribute('capture'); gphoto.value = ''; gphoto.click(); } gphoto.addEventListener('change', async () => { const file = gphoto.files[0]; if (!file || !S._photoTarget) return; const {type, id} = S._photoTarget; S._photoTarget = null; const fd = new FormData(); fd.append('image', file, file.name); // HD — no client-side compression const urls = { room: `/api/rooms/${id}/photo`, cabinet: `/api/cabinets/${id}/photo`, shelf: `/api/shelves/${id}/photo`, book: `/api/books/${id}/photo`, }; try { const res = await req('POST', urls[type], fd, true); const key = type==='book' ? 'image_filename' : 'photo_filename'; walkTree(n=>{ if(n.id===id) n[key]=res[key]; }); // Photo queue mode: process and advance without full re-render if (_photoQueue && type === 'book') { _photoQueue.processing = true; renderPhotoQueue(); const book = findNode(id); if (book && book.identification_status !== 'user_approved') { try { const br = await req('POST', `/api/books/${id}/process`); walkTree(n => { if (n.id === id) Object.assign(n, br); }); } catch { /* continue queue on process error */ } } _photoQueue.processing = false; _photoQueue.index++; renderPhotoQueue(); return; } render(); // Mobile: auto-queue AI after photo upload (books only) if (!isDesktop()) { if (type === 'book') { const book = findNode(id); if (book && book.identification_status !== 'user_approved') { try { const br = await req('POST', `/api/books/${id}/process`); walkTree(n => { if(n.id===id) Object.assign(n, br); }); toast(`Photo saved · Identified (${br.identification_status})`); render(); } catch { toast('Photo saved'); } } else { toast('Photo saved'); } } else { toast('Photo saved'); } } else { toast('Photo saved'); } } catch(err) { toast('Upload failed: '+err.message); } });