/* ============================================================ File: static/js/bookcard_controller.js Purpose: Single owner for updating book-card DOM from merged state (would_merge_to) ============================================================ */ console.log("[BOOKCARD] controller loaded"); /* ============================================================ ENTRY POINT (called by state_updater.js) ============================================================ */ function updateBookCardsFromState(stateList) { console.log("[BOOKCARD] updateBookCardsFromState called"); if (!Array.isArray(stateList)) { console.warn("[BOOKCARD] Invalid stateList", stateList); return; } const stateById = {}; stateList.forEach((entry) => { const merged = entry.would_merge_to; if (!merged || merged.book_idx == null) { console.warn("[BOOKCARD] entry without merged/book_idx", entry); return; } stateById[String(merged.book_idx)] = merged; }); document.querySelectorAll(".book-card").forEach((card) => { const bookIdx = card.dataset.bookIdx; const state = stateById[bookIdx]; if (!state) { console.debug("[BOOKCARD] No state for book_idx", bookIdx); return; } console.log("[BOOKCARD] Updating card", bookIdx, state.status); updateSingleBookCard(card, state); }); } /* ============================================================ SINGLE CARD UPDATE ============================================================ */ function updateSingleBookCard(card, state) { console.log("[BOOKCARD] updateSingleBookCard", state.book_idx); updateStatus(card, state); updateButtons(card, state); updateProgress(card, state); } /* ============================================================ STATUS ============================================================ */ function updateStatus(card, state) { console.log("[BOOKCARD][STATUS]", state.book_idx, "→", state.status); card.className = `book-card ${state.status || ""}`; } /* ============================================================ BUTTONS ============================================================ */ function updateButtons(card, state) { const startBtn = card.querySelector(".icon-start"); const abortBtn = card.querySelector(".icon-abort"); const busy = ["starting", "downloading", "parsing", "audio"]; console.log("[BOOKCARD][BUTTONS]", state.book_idx, "status:", state.status); if (startBtn) { // startBtn.disabled = busy.includes(state.status); } if (abortBtn) { abortBtn.disabled = !busy.includes(state.status); } } /* ============================================================ PROGRESS (DOWNLOAD + AUDIO) ============================================================ */ function updateProgress(card, s) { const total = Number(s.chapters_total || 0); // const downloadDone = // Number(s.chapters_download_done || 0) + // Number(s.chapters_download_skipped || 0); const downloadDone = Number(s.downloaded || 0); const audioDone = Number(s.audio_done || 0) + Number(s.audio_skipped || 0); const downloadPct = total > 0 ? Math.min((downloadDone / total) * 100, 100) : 0; const audioPct = total > 0 ? Math.min((audioDone / total) * 100, 100) : 0; console.log("[BOOKCARD][PROGRESS]", s.book_idx, { total, downloadDone, audioDone, downloadPct, audioPct, }); /* ---- DOWNLOAD ---- */ const dlBar = card.querySelector('[data-field="download_pct"]'); const dlText = card.querySelector('[data-field="download_text"]'); if (dlBar) dlBar.style.width = `${downloadPct}%`; if (dlText) dlText.textContent = `${downloadDone} / ${total}`; /* ---- AUDIO ---- */ const auBar = card.querySelector('[data-field="audio_pct"]'); const auText = card.querySelector('[data-field="audio_text"]'); if (auBar) auBar.style.width = `${audioPct}%`; if (auText) auText.textContent = `${audioDone} / ${total}`; }