/*
* 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()
*/
/* exported 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}
${esc(book.title || '—')}
${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);
}
});