Photo-based book cataloger with AI identification. Room → Cabinet → Shelf → Book hierarchy; FastAPI + SQLite backend; vanilla JS SPA; OpenAI-compatible plugin system for boundary detection, text recognition, and archive search.
83 lines
3.7 KiB
JavaScript
83 lines
3.7 KiB
JavaScript
/*
|
|
* init.js
|
|
* Application bootstrap: full render, partial detail re-render, config and
|
|
* tree loading, batch-status polling, and the initial Promise.all boot call.
|
|
*
|
|
* render() is the single source of truth for full repaints — it replaces
|
|
* #app innerHTML, re-attaches editables, reinitialises Sortable instances,
|
|
* and (on desktop) schedules the boundary canvas setup.
|
|
*
|
|
* renderDetail() does a cheaper in-place update of the right panel only,
|
|
* used during plugin runs and field edits to avoid re-rendering the sidebar.
|
|
*
|
|
* Depends on: S, _plugins, _batchState, _batchPollTimer (state.js);
|
|
* req, toast (api.js / helpers.js); isDesktop (helpers.js);
|
|
* vApp, vDetailBody, mainTitle, mainHeaderBtns, vBatchBtn
|
|
* (tree-render.js / detail-render.js);
|
|
* attachEditables, initSortables (editing.js);
|
|
* setupDetailCanvas (canvas-boundary.js)
|
|
* Provides: render(), renderDetail(), loadConfig(), startBatchPolling(),
|
|
* loadTree()
|
|
*/
|
|
|
|
// ── Full re-render ────────────────────────────────────────────────────────────
|
|
function render() {
|
|
if (document.activeElement?.contentEditable === 'true') return;
|
|
const sy = window.scrollY;
|
|
document.getElementById('app').innerHTML = vApp();
|
|
window.scrollTo(0, sy);
|
|
attachEditables();
|
|
initSortables();
|
|
if (isDesktop()) requestAnimationFrame(setupDetailCanvas);
|
|
}
|
|
|
|
// ── Right-panel partial re-render ─────────────────────────────────────────────
|
|
// Used during plugin runs and field edits to avoid re-rendering the sidebar.
|
|
function renderDetail() {
|
|
const body = document.getElementById('main-body');
|
|
if (body) body.innerHTML = vDetailBody();
|
|
const t = document.getElementById('main-title');
|
|
if (t) t.innerHTML = mainTitle(); // innerHTML: mainTitle() returns an HTML span
|
|
const hb = document.getElementById('main-hdr-btns');
|
|
if (hb) hb.innerHTML = mainHeaderBtns();
|
|
const bb = document.getElementById('main-hdr-batch');
|
|
if (bb) bb.innerHTML = vBatchBtn();
|
|
attachEditables(); // pick up the new editable span in the header
|
|
requestAnimationFrame(setupDetailCanvas);
|
|
}
|
|
|
|
// ── Data loading ──────────────────────────────────────────────────────────────
|
|
async function loadConfig() {
|
|
try {
|
|
const cfg = await req('GET','/api/config');
|
|
window._grabPx = cfg.boundary_grab_px ?? 14;
|
|
window._confidenceThreshold = cfg.confidence_threshold ?? 0.8;
|
|
_plugins = cfg.plugins || [];
|
|
} catch { window._grabPx = 14; window._confidenceThreshold = 0.8; }
|
|
}
|
|
|
|
function startBatchPolling() {
|
|
if (_batchPollTimer) clearInterval(_batchPollTimer);
|
|
_batchPollTimer = setInterval(async () => {
|
|
try {
|
|
const st = await req('GET', '/api/batch/status');
|
|
_batchState = st;
|
|
const bb = document.getElementById('main-hdr-batch');
|
|
if (bb) bb.innerHTML = vBatchBtn();
|
|
if (!st.running) {
|
|
clearInterval(_batchPollTimer); _batchPollTimer = null;
|
|
toast(`Batch: ${st.done} done, ${st.errors} errors`);
|
|
await loadTree();
|
|
}
|
|
} catch { /* ignore poll errors */ }
|
|
}, 2000);
|
|
}
|
|
|
|
async function loadTree() {
|
|
S.tree = await req('GET','/api/tree');
|
|
render();
|
|
}
|
|
|
|
// ── Init ──────────────────────────────────────────────────────────────────────
|
|
Promise.all([loadConfig(), loadTree()]);
|