Compare commits
No commits in common. '516bca6de56270974db79e9f12f4c4cd1f2c22f2' and '3a62dfae799964a2cc801a2eaea0af51fd4b924f' have entirely different histories.
516bca6de5
...
3a62dfae79
@ -1,129 +0,0 @@
|
|||||||
/* ============================================================
|
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
@ -1,101 +0,0 @@
|
|||||||
/* ============================================================
|
|
||||||
File: static/js/inspect_state.js
|
|
||||||
Purpose:
|
|
||||||
- Receive merged state via state_updater.js
|
|
||||||
- Update ONLY the right-side state tables
|
|
||||||
- NO polling, NO fetch
|
|
||||||
============================================================ */
|
|
||||||
|
|
||||||
console.log("[inspect_state] JS loaded (subscriber mode)");
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------
|
|
||||||
State subscription
|
|
||||||
------------------------------------------------------------ */
|
|
||||||
|
|
||||||
window.addEventListener("state:update", (e) => {
|
|
||||||
const entries = e.detail;
|
|
||||||
|
|
||||||
if (!Array.isArray(entries)) {
|
|
||||||
console.warn("[inspect_state] state:update payload is not array", entries);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("[inspect_state] state:update received entries:", entries.length);
|
|
||||||
updateInspectTables(entries);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------
|
|
||||||
Update tables
|
|
||||||
------------------------------------------------------------ */
|
|
||||||
|
|
||||||
function updateInspectTables(entries) {
|
|
||||||
console.log("[inspect_state] updating tables");
|
|
||||||
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
const bookIdx = entry.book_idx;
|
|
||||||
if (bookIdx == null) {
|
|
||||||
console.warn("[inspect_state] entry without book_idx", entry);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const block = document.querySelector(
|
|
||||||
`.state-block[data-book-idx="${bookIdx}"]`
|
|
||||||
);
|
|
||||||
if (!block) {
|
|
||||||
console.warn("[inspect_state] no state-block for book_idx", bookIdx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const table = block.querySelector(".state-table");
|
|
||||||
if (!table) {
|
|
||||||
console.warn("[inspect_state] no state-table for book_idx", bookIdx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("[inspect_state] updating table for book_idx", bookIdx);
|
|
||||||
|
|
||||||
const sql = entry.sqlite || {};
|
|
||||||
const redis = entry.redis || {};
|
|
||||||
const merged = entry.would_merge_to || {};
|
|
||||||
|
|
||||||
table.innerHTML = `
|
|
||||||
<tr>
|
|
||||||
<th>Field</th>
|
|
||||||
<th>SQLite</th>
|
|
||||||
<th>Redis</th>
|
|
||||||
<th>Merged</th>
|
|
||||||
</tr>
|
|
||||||
${row("status", sql, redis, merged)}
|
|
||||||
${row("chapters_total", sql, redis, merged)}
|
|
||||||
${row("downloaded", sql, redis, merged)}
|
|
||||||
${row("chapters_download_done", sql, redis, merged)}
|
|
||||||
${row("chapters_download_skipped", sql, redis, merged)}
|
|
||||||
${row("parsed", sql, redis, merged)}
|
|
||||||
${row("chapters_parsed_done", sql, redis, merged)}
|
|
||||||
${row("audio_done", sql, redis, merged)}
|
|
||||||
${row("audio_skipped", sql, redis, merged)}
|
|
||||||
${row("last_update", sql, redis, merged)}
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------
|
|
||||||
Row helper
|
|
||||||
------------------------------------------------------------ */
|
|
||||||
|
|
||||||
function row(field, sql, redis, merged) {
|
|
||||||
const s = sql[field] ?? "";
|
|
||||||
const r = redis[field] ?? "";
|
|
||||||
const m = merged[field] ?? "";
|
|
||||||
|
|
||||||
const cls = String(s) === String(r) ? "same" : "diff";
|
|
||||||
|
|
||||||
return `
|
|
||||||
<tr>
|
|
||||||
<th>${field}</th>
|
|
||||||
<td class="${cls}">${s}</td>
|
|
||||||
<td class="${cls}">${r}</td>
|
|
||||||
<td>${m}</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
/* =======================================================================
|
||||||
|
File: static/js/progress.js
|
||||||
|
Purpose:
|
||||||
|
Update progress bars dynamically for the current book.
|
||||||
|
Only updates the main progress box (book_detail page).
|
||||||
|
======================================================================= */
|
||||||
|
|
||||||
|
console.log(">>> progress.js LOADED");
|
||||||
|
|
||||||
|
function updateProgressBars(data) {
|
||||||
|
console.log(">>> progress.js updateProgressBars() CALLED with:", data);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
console.warn(">>> progress.js: NO DATA RECEIVED");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We always update inside the main progress box:
|
||||||
|
const container = document.querySelector("#progressSection");
|
||||||
|
if (!container) {
|
||||||
|
console.warn(">>> progress.js: #progressSection NOT FOUND");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select bars ONLY inside the correct section
|
||||||
|
const barDL = container.querySelector(
|
||||||
|
".progress-bar:not(.audio) .progress-bar-fill"
|
||||||
|
);
|
||||||
|
const barAU = container.querySelector(
|
||||||
|
".progress-bar.audio .progress-bar-fill"
|
||||||
|
);
|
||||||
|
|
||||||
|
const pctDL =
|
||||||
|
data.download_total > 0
|
||||||
|
? (100 * data.download_done) / data.download_total
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const pctAU =
|
||||||
|
data.audio_total > 0 ? (100 * data.audio_done) / data.audio_total : 0;
|
||||||
|
|
||||||
|
if (barDL) {
|
||||||
|
barDL.style.width = pctDL.toFixed(1) + "%";
|
||||||
|
console.log(">>> progress.js DL bar =", pctDL.toFixed(1) + "%");
|
||||||
|
} else {
|
||||||
|
console.warn(">>> progress.js: barDL NOT FOUND INSIDE #progressSection");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (barAU) {
|
||||||
|
barAU.style.width = pctAU.toFixed(1) + "%";
|
||||||
|
console.log(">>> progress.js AU bar =", pctAU.toFixed(1) + "%");
|
||||||
|
} else {
|
||||||
|
console.warn(">>> progress.js: barAU NOT FOUND INSIDE #progressSection");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Textual stats — only update inside progress box
|
||||||
|
const stats = container.querySelectorAll(".progress-stats span");
|
||||||
|
|
||||||
|
// Expected: [DL x/y, DL %, AU x/y, AU %]
|
||||||
|
if (stats.length >= 4) {
|
||||||
|
stats[0].innerText = `${data.download_done} / ${data.download_total}`;
|
||||||
|
stats[1].innerText = pctDL.toFixed(1) + "%";
|
||||||
|
stats[2].innerText = `${data.audio_done} / ${data.audio_total}`;
|
||||||
|
stats[3].innerText = pctAU.toFixed(1) + "%";
|
||||||
|
|
||||||
|
console.log(">>> progress.js stats updated");
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
">>> progress.js: not enough stats spans in the container, found",
|
||||||
|
stats.length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,98 +0,0 @@
|
|||||||
/* ========================================================
|
|
||||||
File: static/js/state_updater.js
|
|
||||||
Purpose:
|
|
||||||
- Poll /api/state/all
|
|
||||||
- Dispatch merged state to subscribers
|
|
||||||
(bookcard_controller, inspect_state, others)
|
|
||||||
- Pause polling when tab inactive
|
|
||||||
======================================================== */
|
|
||||||
|
|
||||||
console.log("[STATE-UPDATER] loaded");
|
|
||||||
|
|
||||||
const STATE_POLL_INTERVAL_MS = 2500;
|
|
||||||
const STATE_ENDPOINT = "/api/state/all";
|
|
||||||
|
|
||||||
let STATE_TIMER = null;
|
|
||||||
|
|
||||||
/* ========================================================
|
|
||||||
INIT
|
|
||||||
======================================================== */
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
initStateUpdater();
|
|
||||||
});
|
|
||||||
|
|
||||||
function initStateUpdater() {
|
|
||||||
const cards = document.querySelectorAll(".book-card");
|
|
||||||
|
|
||||||
if (cards.length === 0) {
|
|
||||||
console.log("[STATE-UPDATER] No bookcards found — skipping");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[STATE-UPDATER] Starting updater for ${cards.length} bookcards`);
|
|
||||||
|
|
||||||
startPolling(true);
|
|
||||||
|
|
||||||
document.addEventListener("visibilitychange", () => {
|
|
||||||
document.hidden ? stopPolling() : startPolling(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ========================================================
|
|
||||||
DISPATCH
|
|
||||||
======================================================== */
|
|
||||||
|
|
||||||
function dispatchState(entries) {
|
|
||||||
console.debug("[STATE] dispatch", entries.length);
|
|
||||||
|
|
||||||
// 1. Bookcards
|
|
||||||
if (typeof window.updateBookCardsFromState === "function") {
|
|
||||||
window.updateBookCardsFromState(entries);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Inspect state tables / other subscribers
|
|
||||||
window.dispatchEvent(new CustomEvent("state:update", { detail: entries }));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ========================================================
|
|
||||||
POLLING CONTROL
|
|
||||||
======================================================== */
|
|
||||||
|
|
||||||
function startPolling(immediate = false) {
|
|
||||||
if (STATE_TIMER) return;
|
|
||||||
|
|
||||||
console.log("[STATE-UPDATER] Start polling");
|
|
||||||
|
|
||||||
if (immediate) pollState();
|
|
||||||
|
|
||||||
STATE_TIMER = setInterval(pollState, STATE_POLL_INTERVAL_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopPolling() {
|
|
||||||
if (!STATE_TIMER) return;
|
|
||||||
|
|
||||||
console.log("[STATE-UPDATER] Stop polling (tab inactive)");
|
|
||||||
clearInterval(STATE_TIMER);
|
|
||||||
STATE_TIMER = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ========================================================
|
|
||||||
POLL API
|
|
||||||
======================================================== */
|
|
||||||
|
|
||||||
async function pollState() {
|
|
||||||
if (document.hidden) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await fetch(STATE_ENDPOINT, { cache: "no-store" });
|
|
||||||
if (!resp.ok) return;
|
|
||||||
|
|
||||||
const entries = await resp.json();
|
|
||||||
if (!Array.isArray(entries)) return;
|
|
||||||
|
|
||||||
dispatchState(entries);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[STATE-UPDATER] poll error", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,34 +1,62 @@
|
|||||||
<!-- =======================================================================
|
<!-- =======================================================================
|
||||||
File: templates/components/progress_box.html
|
File: templates/components/progress_box.html
|
||||||
Purpose:
|
Purpose: Reusable progress overview (download + audio) for any book.
|
||||||
Dumb progress UI for a book card.
|
Notes:
|
||||||
Initial values via Jinja, live updates via state_updater.js
|
- Expects the following variables from Flask:
|
||||||
======================================================================= -->
|
book_idx: str
|
||||||
|
title: str
|
||||||
|
download_total: int
|
||||||
|
download_done: int
|
||||||
|
audio_total: int
|
||||||
|
audio_done: int
|
||||||
|
- Pure HTML; JS for live updates will be added later.
|
||||||
|
======================================================================= -->
|
||||||
|
|
||||||
<div class="progress-box">
|
<div class="progress-box">
|
||||||
<!-- DOWNLOAD -->
|
<!-- Header -->
|
||||||
<div class="progress-row">
|
<div class="progress-header">
|
||||||
<div class="progress-label">Download</div>
|
<h2>Progress</h2>
|
||||||
<div class="progressbar">
|
{% if title %}
|
||||||
<div
|
<div class="progress-subtitle">{{ title }}</div>
|
||||||
class="progressbar-fill download"
|
{% endif %} {% if book_idx %}
|
||||||
data-field="download_pct"
|
<div class="progress-bookid">Book IDX: <span>{{ book_idx }}</span></div>
|
||||||
style="width: 0%"
|
{% endif %}
|
||||||
></div>
|
</div>
|
||||||
<div class="progressbar-text" data-field="download_text">0 / 0</div>
|
|
||||||
|
<!-- DOWNLOAD SECTION -->
|
||||||
|
<div class="progress-section">
|
||||||
|
<h3>Download Progress</h3>
|
||||||
|
|
||||||
|
<div class="progress-bar">
|
||||||
|
{% set pct = 0 %} {% if download_total > 0 %} {% set pct = (100 *
|
||||||
|
download_done / download_total) | round(1) %} {% endif %}
|
||||||
|
<div class="progress-bar-fill" style="width: {{ pct }}%;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-stats">
|
||||||
|
<span>{{ download_done }} / {{ download_total }}</span>
|
||||||
|
<span>{{ pct }}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- AUDIO -->
|
<!-- AUDIO SECTION -->
|
||||||
<div class="progress-row">
|
<div class="progress-section">
|
||||||
<div class="progress-label">Audio</div>
|
<h3>Audio Progress</h3>
|
||||||
<div class="progressbar">
|
|
||||||
|
<div class="progress-bar audio">
|
||||||
|
{% set pct2 = 0 %} {% if audio_total > 0 %} {% set pct2 = (100 *
|
||||||
|
audio_done / audio_total) | round(1) %} {% endif %}
|
||||||
<div
|
<div
|
||||||
class="progressbar-fill audio"
|
class="progress-bar-fill audio-fill"
|
||||||
data-field="audio_pct"
|
style="width: {{ pct2 }}%;"
|
||||||
style="width: 0%"
|
|
||||||
></div>
|
></div>
|
||||||
<div class="progressbar-text" data-field="audio_text">0 / 0</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-stats">
|
||||||
|
<span>{{ audio_done }} / {{ audio_total }}</span>
|
||||||
|
<span>{{ pct2 }}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="/static/js/progress.js"></script>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in new issue