mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 21:50:28 +02:00
feat: Enhance file upload experience with drag-and-drop support and improved UI for upload dropzone
This commit is contained in:
@@ -7,6 +7,9 @@ const initDashboard = () => {
|
||||
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 parseJSONScript = (id) => {
|
||||
const element = document.getElementById(id);
|
||||
@@ -32,6 +35,71 @@ const initDashboard = () => {
|
||||
const previewAudio = scope.querySelector('[data-role="voice-preview-audio"]');
|
||||
const sampleVoiceTexts = parseJSONScript('voice-sample-texts') || {};
|
||||
|
||||
const setDropzoneStatus = (message, state = "") => {
|
||||
if (!dropzoneFilename) return;
|
||||
if (!message) {
|
||||
dropzoneFilename.hidden = true;
|
||||
dropzoneFilename.textContent = "";
|
||||
dropzoneFilename.removeAttribute("data-state");
|
||||
return;
|
||||
}
|
||||
dropzoneFilename.hidden = false;
|
||||
dropzoneFilename.textContent = message;
|
||||
if (state) {
|
||||
dropzoneFilename.dataset.state = state;
|
||||
} else {
|
||||
dropzoneFilename.removeAttribute("data-state");
|
||||
}
|
||||
};
|
||||
|
||||
const updateDropzoneFilename = () => {
|
||||
if (!sourceFileInput) {
|
||||
setDropzoneStatus("");
|
||||
return;
|
||||
}
|
||||
const file = sourceFileInput.files && sourceFileInput.files[0];
|
||||
if (file) {
|
||||
setDropzoneStatus(`Selected: ${file.name}`);
|
||||
} else {
|
||||
setDropzoneStatus("");
|
||||
}
|
||||
};
|
||||
|
||||
const assignDroppedFile = (file) => {
|
||||
if (!sourceFileInput || !file) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (typeof DataTransfer === "undefined") {
|
||||
throw new Error("DataTransfer API unavailable");
|
||||
}
|
||||
const transfer = new DataTransfer();
|
||||
transfer.items.add(file);
|
||||
sourceFileInput.files = transfer.files;
|
||||
sourceFileInput.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
try {
|
||||
sourceFileInput.focus({ preventScroll: true });
|
||||
} catch (error) {
|
||||
// Ignore focus errors
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn("Unable to assign dropped file to input", error);
|
||||
setDropzoneStatus("Drag & drop isn't supported here. Click to choose a file instead.", "error");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const setDropzoneActive = (isActive) => {
|
||||
if (!dropzone) return;
|
||||
dropzone.classList.toggle("is-dragging", isActive);
|
||||
if (isActive) {
|
||||
dropzone.dataset.state = "drag";
|
||||
} else {
|
||||
delete dropzone.dataset.state;
|
||||
}
|
||||
};
|
||||
|
||||
let lastTrigger = null;
|
||||
let readerTrigger = null;
|
||||
let previewAbortController = null;
|
||||
@@ -151,6 +219,13 @@ const initDashboard = () => {
|
||||
}
|
||||
});
|
||||
|
||||
if (sourceFileInput) {
|
||||
sourceFileInput.addEventListener("change", updateDropzoneFilename);
|
||||
updateDropzoneFilename();
|
||||
} else {
|
||||
setDropzoneStatus("");
|
||||
}
|
||||
|
||||
const resolveSampleText = (language) => {
|
||||
const fallback = typeof sampleVoiceTexts === "object" && sampleVoiceTexts?.a
|
||||
? sampleVoiceTexts.a
|
||||
@@ -352,6 +427,68 @@ const initDashboard = () => {
|
||||
});
|
||||
}
|
||||
|
||||
if (dropzone) {
|
||||
let dragDepth = 0;
|
||||
|
||||
dropzone.addEventListener("dragenter", (event) => {
|
||||
event.preventDefault();
|
||||
dragDepth += 1;
|
||||
setDropzoneActive(true);
|
||||
});
|
||||
|
||||
dropzone.addEventListener("dragover", (event) => {
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
}
|
||||
});
|
||||
|
||||
const handleDragLeave = (event) => {
|
||||
if (event && dropzone.contains(event.relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
dragDepth = Math.max(0, dragDepth - 1);
|
||||
if (dragDepth === 0) {
|
||||
setDropzoneActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
dropzone.addEventListener("dragleave", (event) => {
|
||||
handleDragLeave(event);
|
||||
});
|
||||
|
||||
dropzone.addEventListener("dragend", () => {
|
||||
dragDepth = 0;
|
||||
setDropzoneActive(false);
|
||||
});
|
||||
|
||||
dropzone.addEventListener("drop", (event) => {
|
||||
event.preventDefault();
|
||||
dragDepth = 0;
|
||||
setDropzoneActive(false);
|
||||
const files = event.dataTransfer && event.dataTransfer.files;
|
||||
if (!files || !files.length) {
|
||||
return;
|
||||
}
|
||||
openUploadModal(dropzone);
|
||||
assignDroppedFile(files[0]);
|
||||
});
|
||||
|
||||
dropzone.addEventListener("click", (event) => {
|
||||
if (event.target.closest('[data-role="open-upload-modal"]')) {
|
||||
return;
|
||||
}
|
||||
openUploadModal(dropzone);
|
||||
});
|
||||
|
||||
dropzone.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
openUploadModal(dropzone);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[voiceSelect, profileSelect, formulaInput, languageSelect, speedInput].forEach((input) => {
|
||||
if (!input) return;
|
||||
const eventName = input === formulaInput ? "input" : "change";
|
||||
|
||||
@@ -130,12 +130,90 @@ body {
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.upload-card__dropzone {
|
||||
border: 1.5px dashed rgba(148, 163, 184, 0.3);
|
||||
border-radius: 24px;
|
||||
background: rgba(15, 23, 42, 0.28);
|
||||
padding: 2.75rem 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.upload-card__dropzone:hover,
|
||||
.upload-card__dropzone:focus-visible {
|
||||
border-color: rgba(56, 189, 248, 0.6);
|
||||
box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.12);
|
||||
}
|
||||
|
||||
.upload-card__dropzone.is-dragging {
|
||||
border-color: rgba(56, 189, 248, 0.85);
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.18);
|
||||
}
|
||||
|
||||
.upload-card__dropzone-content {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
justify-items: center;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.upload-card__icon {
|
||||
font-size: 2rem;
|
||||
line-height: 1;
|
||||
padding: 0.55rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(56, 189, 248, 0.12);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.upload-card__headline {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.upload-card__hint {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.upload-card__filename {
|
||||
margin: 0;
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.upload-card__filename[data-state="error"] {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.upload-card__dropzone .button {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.card__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.card__actions .button {
|
||||
padding: 0.7rem 1.1rem;
|
||||
font-size: 0.95rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 22px rgba(56, 189, 248, 0.18);
|
||||
}
|
||||
|
||||
.card__actions .button:hover {
|
||||
box-shadow: 0 12px 26px rgba(14, 165, 233, 0.24);
|
||||
}
|
||||
|
||||
.card--modal {
|
||||
padding: 0;
|
||||
border-radius: 28px;
|
||||
@@ -213,6 +291,7 @@ body {
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.modal__footer {
|
||||
|
||||
@@ -4,25 +4,16 @@
|
||||
|
||||
{% block content %}
|
||||
<section class="card card--workflow">
|
||||
<div class="step-indicator" aria-label="Audiobook workflow">
|
||||
<span class="step-indicator__item is-active">
|
||||
<span class="step-indicator__index">1</span>
|
||||
<span class="step-indicator__label">Upload & settings</span>
|
||||
</span>
|
||||
<span class="step-indicator__item">
|
||||
<span class="step-indicator__index">2</span>
|
||||
<span class="step-indicator__label">Chapters</span>
|
||||
</span>
|
||||
<span class="step-indicator__item">
|
||||
<span class="step-indicator__index">3</span>
|
||||
<span class="step-indicator__label">Speakers</span>
|
||||
</span>
|
||||
</div>
|
||||
<h1 class="card__title">Create a New Audiobook</h1>
|
||||
<p class="card__subtitle">Kick off a fresh conversion with your manuscript or pasted text. You can fine-tune chapters and speakers in the next steps.</p>
|
||||
<div class="card__actions">
|
||||
<button type="button" class="button" data-role="open-upload-modal">Open upload & settings</button>
|
||||
<a class="button button--ghost" href="{{ url_for('web.queue_page') }}">View queue</a>
|
||||
<div class="upload-card__dropzone" data-role="upload-dropzone" tabindex="0" role="button" aria-label="Open upload settings or drop a manuscript file">
|
||||
<div class="upload-card__dropzone-content">
|
||||
<span class="upload-card__icon" aria-hidden="true">↑</span>
|
||||
<p class="upload-card__headline">Drop your manuscript to begin</p>
|
||||
<p class="upload-card__hint">Drag & drop a supported file here, or click to choose one in the next step.</p>
|
||||
<p class="upload-card__filename" data-role="upload-dropzone-filename" hidden></p>
|
||||
<button type="button" class="button" data-role="open-upload-modal">Open upload & settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user