From 97fd8b85fc60eb5c70a49d1f1ba75f9b4f212b93 Mon Sep 17 00:00:00 2001 From: JB Date: Sat, 11 Oct 2025 07:13:59 -0700 Subject: [PATCH] feat: Refactor reader modal implementation and enhance dashboard event handling --- abogen/web/static/dashboard.js | 134 +---------- abogen/web/static/prepare.js | 14 +- abogen/web/static/queue.js | 11 + abogen/web/static/reader.js | 211 ++++++++++++++++++ abogen/web/templates/index.html | 20 +- .../web/templates/partials/reader_modal.html | 19 ++ abogen/web/templates/prepare_job.html | 9 +- abogen/web/templates/queue.html | 20 +- 8 files changed, 263 insertions(+), 175 deletions(-) create mode 100644 abogen/web/static/queue.js create mode 100644 abogen/web/static/reader.js create mode 100644 abogen/web/templates/partials/reader_modal.html diff --git a/abogen/web/static/dashboard.js b/abogen/web/static/dashboard.js index 2fd6c4b..5dd3c78 100644 --- a/abogen/web/static/dashboard.js +++ b/abogen/web/static/dashboard.js @@ -1,36 +1,13 @@ +import { initReaderUI } from "./reader.js"; + const initDashboard = () => { const uploadModal = document.querySelector('[data-role="upload-modal"]'); const openModalButtons = document.querySelectorAll('[data-role="open-upload-modal"]'); - const readerModal = document.querySelector('[data-role="reader-modal"]'); - const readerFrame = readerModal?.querySelector('[data-role="reader-frame"]') || null; - const readerHint = readerModal?.querySelector('[data-role="reader-modal-hint"]') || null; - const readerTitle = readerModal?.querySelector('#reader-modal-title') || null; - const defaultReaderHint = readerHint?.textContent || ""; const scope = uploadModal || document; const sourceFileInput = scope.querySelector('#source_file'); const dropzone = document.querySelector('[data-role="upload-dropzone"]'); const dropzoneFilename = document.querySelector('[data-role="upload-dropzone-filename"]'); - const readerButtonRegistry = new WeakSet(); - - const bindReaderButtons = (root) => { - const context = root instanceof Element ? root : document; - const buttons = context.querySelectorAll('[data-role="open-reader"]'); - buttons.forEach((button) => { - if (!(button instanceof HTMLElement)) { - return; - } - if (readerButtonRegistry.has(button)) { - return; - } - button.addEventListener("click", (event) => { - event.preventDefault(); - openReaderModal(button); - }); - readerButtonRegistry.add(button); - }); - }; - const parseJSONScript = (id) => { const element = document.getElementById(id); if (!element) return null; @@ -121,7 +98,6 @@ const initDashboard = () => { }; let lastTrigger = null; - let readerTrigger = null; let previewAbortController = null; let previewObjectUrl = null; let suppressPauseStatus = false; @@ -150,54 +126,6 @@ const initDashboard = () => { } }; - const openReaderModal = (trigger) => { - const url = trigger?.dataset.readerUrl || ""; - if (!url) return; - if (!readerModal || !readerFrame) { - window.open(url, "_blank", "noopener,noreferrer"); - return; - } - readerTrigger = trigger || null; - const bookTitle = trigger?.dataset.bookTitle || ""; - if (readerTitle) { - readerTitle.textContent = bookTitle ? `${bookTitle} · reader` : "Read & listen"; - } - if (readerHint) { - readerHint.textContent = bookTitle ? `Preview ${bookTitle} directly in your browser.` : defaultReaderHint; - } - closeUploadModal(); - readerModal.hidden = false; - readerModal.dataset.open = "true"; - document.body.classList.add("modal-open"); - readerFrame.src = url; - try { - readerFrame.focus({ preventScroll: true }); - } catch (error) { - // Ignore focus errors when the browser blocks iframe focus - } - }; - - const closeReaderModal = () => { - if (!readerModal) return; - if (readerModal.hidden) return; - readerModal.hidden = true; - delete readerModal.dataset.open; - document.body.classList.remove("modal-open"); - if (readerFrame) { - readerFrame.src = "about:blank"; - } - if (readerHint) { - readerHint.textContent = defaultReaderHint; - } - if (readerTitle) { - readerTitle.textContent = "Read & listen"; - } - if (readerTrigger && readerTrigger instanceof HTMLElement) { - readerTrigger.focus({ preventScroll: true }); - } - readerTrigger = null; - }; - openModalButtons.forEach((button) => { button.addEventListener("click", (event) => { event.preventDefault(); @@ -221,55 +149,10 @@ const initDashboard = () => { closeUploadModal(); return; } - if (readerModal && !readerModal.hidden) { - closeReaderModal(); - } } }); - const resolveEventMatch = (event, selector) => { - const target = event.target; - if (target instanceof Element) { - const match = target.closest(selector); - if (match) { - return match; - } - } - if (typeof event.composedPath === "function") { - const path = event.composedPath(); - for (const node of path) { - if (node instanceof Element) { - if (node.matches(selector)) { - return node; - } - const match = node.closest(selector); - if (match) { - return match; - } - } - } - } - const fallback = target?.parentElement; - if (fallback instanceof Element) { - return fallback.closest(selector); - } - return null; - }; - - document.addEventListener("click", (event) => { - const readerClose = resolveEventMatch(event, '[data-role="reader-modal-close"]'); - if (readerClose) { - event.preventDefault(); - closeReaderModal(); - return; - } - - const readerTriggerBtn = resolveEventMatch(event, '[data-role="open-reader"]'); - if (readerTriggerBtn instanceof HTMLElement) { - event.preventDefault(); - openReaderModal(readerTriggerBtn); - } - }); + initReaderUI({ onBeforeOpen: closeUploadModal }); if (sourceFileInput) { sourceFileInput.addEventListener("change", updateDropzoneFilename); @@ -655,17 +538,6 @@ const initDashboard = () => { cancelPreviewRequest(); stopPreviewAudio(); }); - - bindReaderButtons(); - - document.addEventListener("htmx:afterSwap", (event) => { - const fragment = event?.detail?.target; - if (fragment instanceof Element) { - bindReaderButtons(fragment); - } else { - bindReaderButtons(); - } - }); }; if (document.readyState === "loading") { diff --git a/abogen/web/static/prepare.js b/abogen/web/static/prepare.js index c45d874..25c90f4 100644 --- a/abogen/web/static/prepare.js +++ b/abogen/web/static/prepare.js @@ -355,22 +355,18 @@ document.addEventListener("DOMContentLoaded", () => { wizard.dataset.speakerStep = skipSpeakers ? "disabled" : "enabled"; } if (speakerIndicator) { + speakerIndicator.hidden = skipSpeakers; if (skipSpeakers) { - speakerIndicator.hidden = true; speakerIndicator.setAttribute("aria-hidden", "true"); - speakerIndicator.style.display = "none"; } else { - speakerIndicator.hidden = false; speakerIndicator.removeAttribute("aria-hidden"); - speakerIndicator.style.removeProperty("display"); } } + if (!skipSpeakers) { + unlockStep("speakers"); + } if (speakerPanel) { - if (skipSpeakers) { - speakerPanel.style.display = "none"; - } else { - speakerPanel.style.removeProperty("display"); - } + speakerPanel.dataset.speakerEnabled = skipSpeakers ? "false" : "true"; } if (continueButton) { if (skipSpeakers) { diff --git a/abogen/web/static/queue.js b/abogen/web/static/queue.js new file mode 100644 index 0000000..27ee10d --- /dev/null +++ b/abogen/web/static/queue.js @@ -0,0 +1,11 @@ +import { initReaderUI } from "./reader.js"; + +const initQueuePage = () => { + initReaderUI(); +}; + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", initQueuePage, { once: true }); +} else { + initQueuePage(); +} diff --git a/abogen/web/static/reader.js b/abogen/web/static/reader.js new file mode 100644 index 0000000..5f10c66 --- /dev/null +++ b/abogen/web/static/reader.js @@ -0,0 +1,211 @@ +const readerButtonRegistry = new WeakSet(); +let initialized = false; +let readerModal = null; +let readerFrame = null; +let readerHint = null; +let readerTitle = null; +let readerTrigger = null; +let defaultReaderHint = ""; + +const resolveEventMatch = (event, selector) => { + const target = event.target; + if (target instanceof Element) { + const match = target.closest(selector); + if (match) { + return match; + } + } + const path = typeof event.composedPath === "function" ? event.composedPath() : []; + for (const node of path) { + if (node instanceof Element) { + if (node.matches(selector)) { + return node; + } + const match = node.closest(selector); + if (match) { + return match; + } + } + } + return null; +}; + +const closeReaderModal = () => { + if (!readerModal) { + return; + } + if (readerModal.hidden) { + return; + } + readerModal.hidden = true; + readerModal.removeAttribute("data-open"); + document.body.classList.remove("modal-open"); + if (readerFrame) { + readerFrame.src = "about:blank"; + } + if (readerHint && defaultReaderHint) { + readerHint.textContent = defaultReaderHint; + } + if (readerTitle) { + readerTitle.textContent = "Read & listen"; + } + if (readerTrigger instanceof HTMLElement) { + try { + readerTrigger.focus({ preventScroll: true }); + } catch (error) { + // Ignore focus errors. + } + } + readerTrigger = null; +}; + +const createBindReaderButtons = (openReaderModal) => { + return (root) => { + const context = root instanceof Element ? root : document; + const buttons = context.querySelectorAll('[data-role="open-reader"]'); + buttons.forEach((button) => { + if (!(button instanceof HTMLElement)) { + return; + } + if (readerButtonRegistry.has(button)) { + return; + } + button.addEventListener("click", (event) => { + event.preventDefault(); + openReaderModal(button); + }); + readerButtonRegistry.add(button); + }); + }; +}; + +export const initReaderUI = (options = {}) => { + if (initialized) { + return { + bindReaderButtons: createBindReaderButtons((trigger) => { + if (typeof options.onBeforeOpen === "function") { + options.onBeforeOpen(); + } + openReader(trigger, options); + }), + closeReaderModal, + }; + } + + readerModal = document.querySelector('[data-role="reader-modal"]'); + readerFrame = readerModal?.querySelector('[data-role="reader-frame"]') || null; + readerHint = readerModal?.querySelector('[data-role="reader-modal-hint"]') || null; + readerTitle = readerModal?.querySelector('#reader-modal-title') || null; + defaultReaderHint = readerHint?.textContent || ""; + + const openReaderModal = (trigger) => { + if (typeof options.onBeforeOpen === "function") { + options.onBeforeOpen(); + } + if (!(trigger instanceof HTMLElement)) { + return; + } + const url = trigger.dataset.readerUrl || ""; + if (!url) { + return; + } + if (!readerModal || !readerFrame) { + window.open(url, "_blank", "noopener,noreferrer"); + return; + } + readerTrigger = trigger; + const bookTitle = trigger.dataset.bookTitle || ""; + if (readerTitle) { + readerTitle.textContent = bookTitle ? `${bookTitle} · reader` : "Read & listen"; + } + if (readerHint) { + readerHint.textContent = bookTitle + ? `Preview ${bookTitle} directly in your browser.` + : defaultReaderHint; + } + readerModal.hidden = false; + readerModal.dataset.open = "true"; + document.body.classList.add("modal-open"); + readerFrame.src = url; + try { + readerFrame.focus({ preventScroll: true }); + } catch (error) { + // Ignore focus errors. + } + }; + + const bindReaderButtons = createBindReaderButtons(openReaderModal); + bindReaderButtons(); + + document.addEventListener("click", (event) => { + const closeButton = resolveEventMatch(event, '[data-role="reader-modal-close"]'); + if (closeButton) { + event.preventDefault(); + closeReaderModal(); + return; + } + const trigger = resolveEventMatch(event, '[data-role="open-reader"]'); + if (trigger instanceof HTMLElement) { + event.preventDefault(); + openReaderModal(trigger); + } + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + closeReaderModal(); + } + }); + + document.addEventListener("htmx:afterSwap", (event) => { + const fragment = event?.detail?.target; + if (fragment instanceof Element) { + bindReaderButtons(fragment); + } else { + bindReaderButtons(); + } + }); + + initialized = true; + + return { + bindReaderButtons, + closeReaderModal, + }; +}; + +const openReader = (trigger, options) => { + if (typeof options.onBeforeOpen === "function") { + options.onBeforeOpen(); + } + if (!(trigger instanceof HTMLElement)) { + return; + } + const url = trigger.dataset.readerUrl || ""; + if (!url) { + return; + } + if (!readerModal || !readerFrame) { + window.open(url, "_blank", "noopener,noreferrer"); + return; + } + readerTrigger = trigger; + const bookTitle = trigger.dataset.bookTitle || ""; + if (readerTitle) { + readerTitle.textContent = bookTitle ? `${bookTitle} · reader` : "Read & listen"; + } + if (readerHint) { + readerHint.textContent = bookTitle + ? `Preview ${bookTitle} directly in your browser.` + : defaultReaderHint; + } + readerModal.hidden = false; + readerModal.dataset.open = "true"; + document.body.classList.add("modal-open"); + readerFrame.src = url; + try { + readerFrame.focus({ preventScroll: true }); + } catch (error) { + // Ignore focus errors. + } +}; diff --git a/abogen/web/templates/index.html b/abogen/web/templates/index.html index 9db1777..6142d46 100644 --- a/abogen/web/templates/index.html +++ b/abogen/web/templates/index.html @@ -175,25 +175,7 @@ - +{% include "partials/reader_modal.html" %} {% endblock %} diff --git a/abogen/web/templates/partials/reader_modal.html b/abogen/web/templates/partials/reader_modal.html new file mode 100644 index 0000000..9421bd9 --- /dev/null +++ b/abogen/web/templates/partials/reader_modal.html @@ -0,0 +1,19 @@ + diff --git a/abogen/web/templates/prepare_job.html b/abogen/web/templates/prepare_job.html index 5efc5bd..58acc9c 100644 --- a/abogen/web/templates/prepare_job.html +++ b/abogen/web/templates/prepare_job.html @@ -10,6 +10,7 @@ {% endif %} {% set is_chapters = wizard_step == 'chapters' %} {% set is_speakers = wizard_step == 'speakers' %} +{% set hide_speakers_panel = not is_speakers %}
@@ -20,7 +21,7 @@ 2 Chapters -
-