diff --git a/abogen/web/static/dashboard.js b/abogen/web/static/dashboard.js index 5dd3c78..c8339ae 100644 --- a/abogen/web/static/dashboard.js +++ b/abogen/web/static/dashboard.js @@ -102,6 +102,15 @@ const initDashboard = () => { let previewObjectUrl = null; let suppressPauseStatus = false; + const dispatchUploadModalEvent = (type, detail = {}) => { + const eventName = `upload-modal:${type}`; + if (uploadModal) { + uploadModal.dispatchEvent(new CustomEvent(eventName, { detail, bubbles: true })); + return; + } + document.dispatchEvent(new CustomEvent(eventName, { detail })); + }; + const openUploadModal = (trigger) => { if (!uploadModal) return; lastTrigger = trigger || null; @@ -112,6 +121,7 @@ const initDashboard = () => { if (focusTarget instanceof HTMLElement) { focusTarget.focus({ preventScroll: true }); } + dispatchUploadModalEvent("open", { trigger: lastTrigger }); }; const closeUploadModal = () => { @@ -124,6 +134,7 @@ const initDashboard = () => { if (lastTrigger && lastTrigger instanceof HTMLElement) { lastTrigger.focus({ preventScroll: true }); } + dispatchUploadModalEvent("close", { trigger: lastTrigger }); }; openModalButtons.forEach((button) => { diff --git a/abogen/web/static/prepare.js b/abogen/web/static/prepare.js index bd6cfd0..ba69004 100644 --- a/abogen/web/static/prepare.js +++ b/abogen/web/static/prepare.js @@ -2,6 +2,55 @@ document.addEventListener("DOMContentLoaded", () => { const form = document.querySelector(".prepare-form"); if (!form) return; + const wizardModal = document.querySelector('[data-role="wizard-modal"]'); + const uploadModal = document.querySelector('[data-role="upload-modal"]'); + const wizardPreviousButtons = Array.from(document.querySelectorAll('[data-role="wizard-previous"]')); + const openUploadTriggers = Array.from(document.querySelectorAll('[data-role="open-upload-modal"]')); + + const showWizardModal = () => { + if (!wizardModal) return; + wizardModal.hidden = false; + wizardModal.dataset.open = "true"; + wizardModal.removeAttribute("aria-hidden"); + document.body.classList.add("modal-open"); + }; + + const hideWizardModal = () => { + if (!wizardModal) return; + wizardModal.hidden = true; + delete wizardModal.dataset.open; + wizardModal.setAttribute("aria-hidden", "true"); + }; + + const triggerUploadModal = () => { + const existingTrigger = openUploadTriggers.find((button) => button !== null); + if (existingTrigger) { + existingTrigger.dispatchEvent(new MouseEvent("click", { bubbles: true })); + return; + } + if (!uploadModal) return; + uploadModal.hidden = false; + uploadModal.dataset.open = "true"; + document.body.classList.add("modal-open"); + const focusTarget = uploadModal.querySelector("#source_file") || uploadModal.querySelector("#source_text") || uploadModal; + if (focusTarget instanceof HTMLElement) { + focusTarget.focus({ preventScroll: true }); + } + }; + + showWizardModal(); + + document.addEventListener("upload-modal:open", hideWizardModal); + document.addEventListener("upload-modal:close", showWizardModal); + + wizardPreviousButtons.forEach((button) => { + button.addEventListener("click", (event) => { + event.preventDefault(); + hideWizardModal(); + triggerUploadModal(); + }); + }); + const parseJSONScript = (id) => { const el = document.getElementById(id); if (!el) return null; @@ -184,12 +233,39 @@ document.addEventListener("DOMContentLoaded", () => { const chapterRows = Array.from(form.querySelectorAll("[data-role=chapter-row]")); + const setRowExpansion = (row, expanded) => { + if (!row) return; + const details = row.querySelector('[data-role="chapter-details"]'); + const toggle = row.querySelector('[data-role="chapter-toggle"]'); + const isExpanded = Boolean(expanded); + row.dataset.expanded = isExpanded ? "true" : "false"; + if (details) { + details.hidden = !isExpanded; + details.setAttribute("aria-hidden", isExpanded ? "false" : "true"); + } + if (toggle) { + toggle.setAttribute("aria-expanded", isExpanded ? "true" : "false"); + toggle.setAttribute("aria-label", isExpanded ? "Hide chapter details" : "Show chapter details"); + } + }; + + const toggleRowExpansion = (row, force) => { + if (!row) return; + const current = row.dataset.expanded === "true"; + const next = typeof force === "boolean" ? force : !current; + setRowExpansion(row, next); + }; + + const isRowEnabled = (row) => { + const checkbox = row?.querySelector('[data-role="chapter-enabled"]'); + return checkbox ? checkbox.checked : true; + }; + const updateRowState = (row) => { const enabled = row.querySelector('[data-role=chapter-enabled]'); const inputs = Array.from(row.querySelectorAll("input[type=text], select, textarea")); const warning = row.querySelector('[data-role=chapter-warning]'); - const snippet = row.querySelector('[data-role="chapter-snippet"]'); - const details = row.querySelector('[data-role="chapter-details"]'); + const toggle = row.querySelector('[data-role="chapter-toggle"]'); const isChecked = enabled ? enabled.checked : true; row.dataset.disabled = isChecked ? "false" : "true"; @@ -214,14 +290,13 @@ document.addEventListener("DOMContentLoaded", () => { const select = row.querySelector("select[data-role=voice-select]"); toggleFormula(select); - if (snippet) { - snippet.hidden = !isChecked; - snippet.setAttribute("aria-hidden", isChecked ? "false" : "true"); + if (!isChecked) { + setRowExpansion(row, false); } - if (details) { - details.hidden = !isChecked; - details.setAttribute("aria-hidden", isChecked ? "false" : "true"); + if (toggle) { + toggle.disabled = !isChecked; + toggle.setAttribute("aria-disabled", isChecked ? "false" : "true"); } if (warning) { @@ -248,6 +323,7 @@ document.addEventListener("DOMContentLoaded", () => { }; chapterRows.forEach((row) => { + setRowExpansion(row, row.dataset.expanded === "true"); const enabled = row.querySelector('[data-role=chapter-enabled]'); if (enabled) { enabled.addEventListener("change", () => updateRowState(row)); @@ -258,6 +334,16 @@ document.addEventListener("DOMContentLoaded", () => { select.addEventListener("change", () => toggleFormula(select)); toggleFormula(select); } + const toggleButton = row.querySelector('[data-role="chapter-toggle"]'); + if (toggleButton) { + toggleButton.addEventListener("click", () => { + if (!isRowEnabled(row)) { + setRowExpansion(row, false); + return; + } + toggleRowExpansion(row); + }); + } }); const updatePreviewVoice = (select) => { diff --git a/abogen/web/static/styles.css b/abogen/web/static/styles.css index d8b7fa7..af47c9c 100644 --- a/abogen/web/static/styles.css +++ b/abogen/web/static/styles.css @@ -521,6 +521,11 @@ body { padding: 3rem 0 5rem; } + .wizard-page--modal { + padding: 0; + min-height: 100vh; + } + .wizard-card { width: min(1100px, calc(100% - 2.5rem)); margin: 0 auto; @@ -1789,13 +1794,70 @@ button.step-indicator__item:focus-visible { opacity: 0.5; } -.chapter-card__header { +.chapter-card__summary { display: flex; justify-content: space-between; align-items: flex-start; gap: 1.25rem; } +.chapter-card__summary:focus-within { + outline: none; +} + +.chapter-card__toggle { + border: none; + background: rgba(148, 163, 184, 0.1); + color: var(--muted); + width: 32px; + height: 32px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease; +} + +.chapter-card__toggle:hover, +.chapter-card__toggle:focus-visible { + background: rgba(148, 163, 184, 0.2); + color: #fff; +} + +.chapter-card__toggle[disabled] { + opacity: 0.45; + cursor: not-allowed; + pointer-events: none; +} + +.chapter-card__toggle:focus-visible { + outline: 2px solid rgba(56, 189, 248, 0.45); + outline-offset: 2px; +} + +.chapter-card__toggle-icon { + display: inline-flex; + font-size: 1rem; + transition: transform 0.2s ease; +} + +.chapter-card[data-expanded="true"] .chapter-card__toggle-icon { + transform: rotate(180deg); +} + +.chapter-card__details { + display: grid; + gap: 1rem; +} + +.chapter-card__snippet { + margin: 0; + font-size: 0.95rem; + line-height: 1.5; + color: var(--muted); +} + .chapter-card__title { font-weight: 600; display: flex; diff --git a/abogen/web/templates/partials/upload_modal.html b/abogen/web/templates/partials/upload_modal.html index 9cc2a6c..6241037 100644 --- a/abogen/web/templates/partials/upload_modal.html +++ b/abogen/web/templates/partials/upload_modal.html @@ -8,6 +8,7 @@ {% elif settings_dict %} {% set is_multi = settings_dict.get('speaker_mode', 'single') == 'multi' %} {% endif %} +{% set total_steps = 3 if is_multi else 2 %} {% set language_value = pending.language if pending else settings_dict.get('language', '') %} {% if not language_value %} {% set sorted_languages = options.languages|dictsort %} @@ -65,7 +66,7 @@ {% endif %} -
Step 1 of 3
+Step 1 of {{ total_steps }}
Choose your source file or paste text, then set the defaults used for chapter analysis and speaker casting.
diff --git a/abogen/web/templates/prepare_chapters.html b/abogen/web/templates/prepare_chapters.html index fbbd2d0..f3ef178 100644 --- a/abogen/web/templates/prepare_chapters.html +++ b/abogen/web/templates/prepare_chapters.html @@ -3,46 +3,50 @@ {% block title %}Prepare · {{ pending.original_filename }}{% endblock %} {% set is_multi_speaker = pending.speaker_mode == 'multi' %} +{% set total_steps = 3 if is_multi_speaker else 2 %} {% block content %} -Choose which chapters to convert. We'll analyse speakers automatically when you continue.
-{{ pending.original_filename }}
-