feat: Implement upload modal event dispatching and enhance wizard modal navigation

This commit is contained in:
JB
2025-10-11 12:29:52 -07:00
parent 6266819670
commit 610091c0d9
6 changed files with 302 additions and 128 deletions
+11
View File
@@ -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) => {
+94 -8
View File
@@ -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) => {
+63 -1
View File
@@ -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;
@@ -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 @@
</span>
{% endif %}
</nav>
<p class="modal__eyebrow">Step 1 of 3</p>
<p class="modal__eyebrow">Step 1 of {{ total_steps }}</p>
<h2 class="modal__title" id="upload-modal-title">Upload &amp; settings</h2>
<p class="hint">Choose your source file or paste text, then set the defaults used for chapter analysis and speaker casting.</p>
</div>
+23 -13
View File
@@ -3,10 +3,13 @@
{% 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 %}
<section class="wizard-page" data-step="chapters">
<div class="wizard-card card card--modal">
<section class="wizard-page wizard-page--modal" data-step="chapters">
<div class="modal modal--wizard" data-role="wizard-modal" data-open="true">
<div class="modal__overlay" aria-hidden="true"></div>
<div class="modal__content card card--modal wizard-card" role="dialog" aria-modal="true" aria-labelledby="prepare-chapters-title">
<header class="modal__header wizard-card__header">
<div class="wizard-card__headline">
<nav class="step-indicator" aria-label="Audiobook workflow">
@@ -30,7 +33,8 @@
</a>
{% endif %}
</nav>
<h2 class="modal__title">Select chapters</h2>
<p class="modal__eyebrow">Step 2 of {{ total_steps }}</p>
<h2 class="modal__title" id="prepare-chapters-title">Select chapters</h2>
<p class="hint">Choose which chapters to convert. We'll analyse speakers automatically when you continue.</p>
</div>
<div class="wizard-card__aside">
@@ -83,8 +87,8 @@
<article class="chapter-card"
data-role="chapter-row"
data-disabled="{{ 'false' if is_enabled else 'true' }}"
data-expanded="{{ 'true' if is_enabled else 'false' }}">
<header class="chapter-card__summary">
data-expanded="false">
<header class="chapter-card__summary" data-role="chapter-summary">
<label class="chapter-card__checkbox">
<input type="checkbox"
name="chapter-{{ loop.index0 }}-enabled"
@@ -92,15 +96,20 @@
{% if is_enabled %}checked{% endif %}>
<span>Chapter {{ loop.index }} · {{ chapter.title }}</span>
</label>
<p class="chapter-card__snippet"
data-role="chapter-snippet"
{% if not is_enabled %}hidden aria-hidden="true"{% else %}aria-hidden="false"{% endif %}>
{{ excerpt }}{% if raw_excerpt|length > 240 %}…{% endif %}
</p>
<button type="button"
class="chapter-card__toggle"
data-role="chapter-toggle"
aria-expanded="false"
aria-label="Toggle chapter details">
<span class="chapter-card__toggle-icon" aria-hidden="true">&#9662;</span>
</button>
</header>
<div class="chapter-card__details"
data-role="chapter-details"
{% if not is_enabled %}hidden aria-hidden="true"{% else %}aria-hidden="false"{% endif %}>
data-role="chapter-details">
<p class="chapter-card__snippet"
data-role="chapter-snippet">
{{ excerpt }}{% if raw_excerpt|length > 240 %}…{% endif %}
</p>
<div class="chapter-card__field">
<label for="chapter-{{ loop.index0 }}-title">Title</label>
<input type="text" id="chapter-{{ loop.index0 }}-title" name="chapter-{{ loop.index0 }}-title" value="{{ chapter.title }}">
@@ -148,7 +157,7 @@
</div>
<footer class="modal__footer wizard-card__footer">
<div class="wizard-card__footer-actions">
<a class="button button--ghost" href="{{ url_for('web.index') }}">Previous</a>
<button type="button" class="button button--ghost" data-role="wizard-previous">Previous</button>
<button type="submit" class="button button--ghost" form="cancel-form">Cancel</button>
</div>
<div class="wizard-card__footer-actions">
@@ -170,6 +179,7 @@
</form>
<form method="post" action="{{ url_for('web.cancel_pending_job', pending_id=pending.id) }}" id="cancel-form"></form>
</div>
</div>
</section>
{% with pending=pending, readonly=True, active_step='chapters' %}
{% include "partials/upload_modal.html" %}
+7 -3
View File
@@ -7,10 +7,13 @@
{% set analysis_speakers = analysis.get('speakers', {}) %}
{% set speaker_configs = options.speaker_configs or [] %}
{% set recommended_template = options.speaker_pronunciation_sentence or "This is {{name}} speaking." %}
{% set total_steps = 3 if is_multi_speaker else 2 %}
{% block content %}
<section class="wizard-page" data-step="speakers">
<div class="wizard-card card card--modal">
<section class="wizard-page wizard-page--modal" data-step="speakers">
<div class="modal modal--wizard" data-role="wizard-modal" data-open="true">
<div class="modal__overlay" aria-hidden="true"></div>
<div class="modal__content card card--modal wizard-card" role="dialog" aria-modal="true" aria-labelledby="prepare-speakers-title">
<header class="modal__header wizard-card__header">
<div class="wizard-card__headline">
<nav class="step-indicator" aria-label="Audiobook workflow">
@@ -34,7 +37,8 @@
</a>
{% endif %}
</nav>
<h2 class="modal__title">Select speakers</h2>
<p class="modal__eyebrow">Step 3 of {{ total_steps }}</p>
<h2 class="modal__title" id="prepare-speakers-title">Select speakers</h2>
<p class="hint">Assign voices, audition samples, and finalise your roster before queueing the conversion.</p>
</div>
<div class="wizard-card__aside">