feat: add drag-and-drop dropzone to the magazine upload form

Replaces the plain <input type="file"> with the same reader-dropzone
component the Book/Source screen already uses (reused CSS, no new
styles needed beyond grid/layout adjustments) -- drag a PDF onto the
zone or click "Choose file", both paths route through the same hidden
input magCreate() already reads via a DataTransfer handoff, so no other
logic needed to change. Shows the chosen/dropped filename in place of
the default hint text, and resets it after a successful add.

Verified live: click-to-choose and a simulated real 'drop' DragEvent
both populate the file input correctly, dragover highlighting toggles,
no console errors from the app itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-09-07 00:03:01 +02:00
parent 813e06c914
commit 5b0c8e0bf5
3 changed files with 55 additions and 5 deletions

View File

@ -106,12 +106,51 @@ function renderLibrary() {
}).join(''); }).join('');
} }
// ── Add-magazine dropzone (mirrors reader.js's setupReaderDropzone) ─────────
const MAG_DROPZONE_DEFAULT_NOTE = 'Scanned issues work too — pages without selectable text are read with OCR.';
function magShowDroppedFile(file) {
const noteEl = $('mag-dropzone-note');
if (noteEl) noteEl.textContent = file ? file.name : MAG_DROPZONE_DEFAULT_NOTE;
}
function setupMagDropzone() {
const zone = $('mag-dropzone');
const input = $('mag-new-file');
if (!zone || !input || zone.dataset.magDropzoneWired) return;
zone.dataset.magDropzoneWired = '1';
zone.addEventListener('dragover', (e) => {
if (e.dataTransfer && Array.from(e.dataTransfer.types || []).includes('Files')) {
e.preventDefault();
zone.classList.add('dragover');
}
});
zone.addEventListener('dragleave', () => zone.classList.remove('dragover'));
zone.addEventListener('drop', (e) => {
if (!(e.dataTransfer && Array.from(e.dataTransfer.types || []).includes('Files'))) return;
e.preventDefault();
zone.classList.remove('dragover');
const file = e.dataTransfer.files?.[0];
if (!file) return;
if (file.type !== 'application/pdf' && !/\.pdf$/i.test(file.name)) {
return say('Only PDF files are supported', 'err');
}
// Route the dropped file through the same hidden <input> magCreate()
// already reads, so drag-and-drop and "Choose file" share one code path.
const dt = new DataTransfer();
dt.items.add(file);
input.files = dt.files;
magShowDroppedFile(file);
});
input.addEventListener('change', () => magShowDroppedFile(input.files?.[0]));
}
async function magCreate() { async function magCreate() {
const title = ($('mag-new-title')?.value || '').trim(); const title = ($('mag-new-title')?.value || '').trim();
const issue = ($('mag-new-issue')?.value || '').trim(); const issue = ($('mag-new-issue')?.value || '').trim();
const file = $('mag-new-file')?.files?.[0]; const file = $('mag-new-file')?.files?.[0];
if (!title) return say('Give the magazine a title', 'err'); if (!title) return say('Give the magazine a title', 'err');
if (!file) return say('Choose the PDF', 'err'); if (!file) return say('Choose or drop the PDF', 'err');
try { try {
const { id } = await api('/api/magazine/docs', { const { id } = await api('/api/magazine/docs', {
method: 'POST', method: 'POST',
@ -123,6 +162,7 @@ async function magCreate() {
if ($('mag-new-title')) $('mag-new-title').value = ''; if ($('mag-new-title')) $('mag-new-title').value = '';
if ($('mag-new-issue')) $('mag-new-issue').value = ''; if ($('mag-new-issue')) $('mag-new-issue').value = '';
if ($('mag-new-file')) $('mag-new-file').value = ''; if ($('mag-new-file')) $('mag-new-file').value = '';
magShowDroppedFile(null);
await magOpen(id); await magOpen(id);
await magIngest(); await magIngest();
} catch (e) { } catch (e) {
@ -912,6 +952,7 @@ function magInit() {
if (!$('mag-library-list')) return; // section not mounted if (!$('mag-library-list')) return; // section not mounted
magLoadLibrary(); magLoadLibrary();
magLoadVoices(); magLoadVoices();
setupMagDropzone();
magShow('library'); magShow('library');
} }

View File

@ -17,14 +17,21 @@
<label for="mag-new-issue">Issue <label for="mag-new-issue">Issue
<input id="mag-new-issue" type="text" placeholder="September 2026" autocomplete="off"> <input id="mag-new-issue" type="text" placeholder="September 2026" autocomplete="off">
</label> </label>
<label for="mag-new-file">PDF </div>
<input id="mag-new-file" type="file" accept="application/pdf"> <div class="reader-dropzone" id="mag-dropzone">
<span class="mdi mdi-cloud-upload-outline reader-dropzone-icon"></span>
<strong>Drop a PDF</strong>
<span class="note" id="mag-dropzone-note">Scanned issues work too &mdash; pages without selectable text are read with OCR.</span>
<label class="btn-secondary btn-sm reader-file-pick" title="Choose a PDF file">
<span class="mdi mdi-file-upload-outline"></span> Choose file
<input type="file" id="mag-new-file" accept="application/pdf" style="display:none">
</label> </label>
</div>
<div class="mag-new-actions">
<button type="button" class="btn primary" onclick="magCreate()"> <button type="button" class="btn primary" onclick="magCreate()">
<span class="mdi mdi-plus"></span> Add magazine <span class="mdi mdi-plus"></span> Add magazine
</button> </button>
</div> </div>
<p class="muted">Scanned issues work too &mdash; pages without selectable text are read with OCR.</p>
</div> </div>
<h3>Your magazines</h3> <h3>Your magazines</h3>

View File

@ -4747,9 +4747,11 @@ input[list="lang-codes"] {
Article-first magazine reader: contents tree, boundary review, reflowed Article-first magazine reader: contents tree, boundary review, reflowed
article with sentence highlighting, and the original PDF page beside it. */ article with sentence highlighting, and the original PDF page beside it. */
.mag-new { margin-bottom: 20px; } .mag-new { margin-bottom: 20px; }
.mag-new-grid { display: grid; grid-template-columns: 2fr 1fr 1.5fr auto; gap: 12px; align-items: end; } .mag-new-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 12px; }
.mag-new-grid label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--subtext); } .mag-new-grid label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--subtext); }
.mag-new-grid input { padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text); } .mag-new-grid input { padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text); }
.mag-new .reader-dropzone { max-width: none; }
.mag-new-actions { display: flex; justify-content: flex-end; margin-top: 14px; }
.mag-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; } .mag-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
.mag-card { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; text-align: left; .mag-card { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; text-align: left;