From e6211f13336b531181a0ce329c2043038146999f Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Thu, 10 Sep 2026 12:45:42 +0200 Subject: [PATCH] Release v1.26.9 --- CHANGELOG.md | 12 +++- VERSION | 2 +- core/magazine.py | 3 +- static/index.html | 16 ++--- static/js/magazine.js | 104 ++++++++++++++++++++++++++------ static/sections/s-magazine.html | 16 ++--- tests/magazine.spec.js | 5 +- 7 files changed, 120 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f600fe..8061133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,15 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.26.9] — 2026-09-10 + +### Fixed +- **PyMuPDF Ingestion Requirement.** Provided actionable installation guidance when `pymupdf` is missing in runtime. +- **Drag & Drop Upload Target & Feedback.** Made the magazine upload dropzone fully clickable, added file selection feedback showing filename and file size, and added active loading states with spinners and disabled controls during upload. +- **Optional Title & Auto-Fill.** Relabeled Title input to optional and auto-fills title from document filename if left blank. + +--- + ## [1.26.8] — 2026-09-09 ### Added @@ -3896,7 +3905,8 @@ Initial feature-complete release. --- -[Unreleased]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.26.1...HEAD +[Unreleased]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.26.9...HEAD +[1.26.9]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.26.8...v1.26.9 [1.26.1]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.26.0...v1.26.1 [1.26.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.25.3...v1.26.0 [1.22.3]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.22.2...v1.22.3 diff --git a/VERSION b/VERSION index 25691b4..3cd33cd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.26.8 +1.26.9 diff --git a/core/magazine.py b/core/magazine.py index 79001fe..6b378e4 100644 --- a/core/magazine.py +++ b/core/magazine.py @@ -728,7 +728,8 @@ def _require_fitz(): return fitz except ImportError as exc: # pragma: no cover - environment guard raise RuntimeError( - "PyMuPDF is required for magazine ingestion; install pymupdf" + "PyMuPDF is required for magazine ingestion; install pymupdf (pip install pymupdf) " + "or rebuild your Docker container." ) from exc diff --git a/static/index.html b/static/index.html index dfcd611..7464c9b 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,12 +27,12 @@ - - - - - - + + + + + + @@ -403,7 +403,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/static/js/magazine.js b/static/js/magazine.js index bed9924..382f8ab 100644 --- a/static/js/magazine.js +++ b/static/js/magazine.js @@ -175,9 +175,36 @@ async function magDelete(docId) { // ── 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 magFormatFilenameAsTitle(filename) { + if (!filename) return ''; + const base = String(filename).replace(/\.[^.]+$/, ''); + return base + .replace(/[_-]+/g, ' ') + .replace(/\s+/g, ' ') + .trim(); +} + function magShowDroppedFile(file) { const noteEl = $('mag-dropzone-note'); - if (noteEl) noteEl.textContent = file ? file.name : MAG_DROPZONE_DEFAULT_NOTE; + const titleEl = $('mag-dropzone-title'); + const iconEl = $('mag-dropzone-icon'); + const titleInput = $('mag-new-title'); + + if (file) { + const sizeMb = (file.size / (1024 * 1024)).toFixed(1); + if (noteEl) noteEl.textContent = `Selected: ${file.name} (${sizeMb} MB)`; + if (titleEl) titleEl.textContent = 'PDF file selected'; + if (iconEl) iconEl.className = 'mdi mdi-file-check-outline reader-dropzone-icon'; + + // Auto-fill title from filename if title input is empty + if (titleInput && !titleInput.value.trim()) { + titleInput.value = magFormatFilenameAsTitle(file.name); + } + } else { + if (noteEl) noteEl.textContent = MAG_DROPZONE_DEFAULT_NOTE; + if (titleEl) titleEl.textContent = 'Drop a PDF or click to browse'; + if (iconEl) iconEl.className = 'mdi mdi-cloud-upload-outline reader-dropzone-icon'; + } } let _magDropGuardInstalled = false; @@ -202,14 +229,16 @@ function setupMagDropzone() { if (zone.dataset.magDropzoneWired) return; zone.dataset.magDropzoneWired = '1'; + // Clicking anywhere in the dropzone triggers the hidden file picker + zone.addEventListener('click', (e) => { + if (!e.target.closest('input') && !e.target.closest('.reader-file-pick')) { + input.click(); + } + }); + // dragenter AND dragover both need preventDefault() -- some browsers only // arm the element as a valid drop target once dragenter itself has been - // cancelled, not just the dragover events that follow it. Not gated on - // dataTransfer.types: during a real OS file drag, Chromium does not - // always populate that list synchronously on every one of these events, - // and gating on it here (confirmed live) silently skipped preventDefault - // often enough that neither the hover highlight nor the drop interception - // ever fired -- the browser fell through to opening the file itself. + // cancelled, not just the dragover events that follow it. const armDropTarget = (e) => { e.preventDefault(); zone.classList.add('dragover'); }; zone.addEventListener('dragenter', armDropTarget); zone.addEventListener('dragover', armDropTarget); @@ -233,12 +262,36 @@ function setupMagDropzone() { } async function magCreate() { - const title = ($('mag-new-title')?.value || '').trim(); - const issue = ($('mag-new-issue')?.value || '').trim(); const file = $('mag-new-file')?.files?.[0]; - if (!title) return say('Give the magazine a title', 'err'); - if (!file) return say('Choose or drop the PDF', 'err'); + if (!file) return say('Choose or drop a PDF file first', 'err'); + + let title = ($('mag-new-title')?.value || '').trim(); + if (!title) { + title = magFormatFilenameAsTitle(file.name) || 'Untitled Magazine'; + if ($('mag-new-title')) $('mag-new-title').value = title; + } + const issue = ($('mag-new-issue')?.value || '').trim(); + + const addBtn = $('mag-add-btn'); + const dropzone = $('mag-dropzone'); + const titleInp = $('mag-new-title'); + const issueInp = $('mag-new-issue'); + const origBtnText = addBtn ? addBtn.innerHTML : ''; + + const setUploadingState = (uploading) => { + if (addBtn) { + addBtn.disabled = uploading; + addBtn.innerHTML = uploading + ? ' Uploading PDF...' + : origBtnText; + } + if (titleInp) titleInp.disabled = uploading; + if (issueInp) issueInp.disabled = uploading; + if (dropzone) dropzone.style.pointerEvents = uploading ? 'none' : 'auto'; + }; + try { + setUploadingState(true); const { id } = await api('/api/magazine/docs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -246,13 +299,17 @@ async function magCreate() { }); await api('/api/reader/docs/' + id + '/source?ext=pdf', { method: 'PUT', body: file }); say('Magazine added — starting ingestion', 'ok'); + if ($('mag-new-title')) $('mag-new-title').value = ''; if ($('mag-new-issue')) $('mag-new-issue').value = ''; if ($('mag-new-file')) $('mag-new-file').value = ''; magShowDroppedFile(null); + setUploadingState(false); + await magOpen(id); await magIngest(); } catch (e) { + setUploadingState(false); say('Could not add magazine: ' + e.message, 'err'); } } @@ -604,7 +661,7 @@ function renderReview() { '
' + '' + - 'Article p. ' + (page ?? '—') + '' + + 'Page ' + (page ?? '—') + '' + '
' + '
' + @@ -617,7 +674,7 @@ function renderReview() { '' + '' + + ' Draw image' + '' + '' + '
'; wrap.appendChild(panel); - const top = parseFloat(box.style.top), left = parseFloat(box.style.left); - panel.style.top = Math.min(85, top + parseFloat(box.style.height) + 1) + '%'; + const top = parseFloat(box.style.top) || 0, left = parseFloat(box.style.left) || 0, height = parseFloat(box.style.height) || 0; + panel.style.top = Math.min(85, top + height + 1) + '%'; panel.style.left = Math.min(60, left) + '%'; wrap.querySelectorAll('input[name="mag-draw-kind"]').forEach((r) => { r.addEventListener('change', () => { diff --git a/static/sections/s-magazine.html b/static/sections/s-magazine.html index 0245542..d8aaaef 100644 --- a/static/sections/s-magazine.html +++ b/static/sections/s-magazine.html @@ -11,24 +11,24 @@

Add a magazine or paper

-
-
- - Drop a PDF +
+ + Drop a PDF or click to browse Scanned issues work too — pages without selectable text are read with OCR. -
-
diff --git a/tests/magazine.spec.js b/tests/magazine.spec.js index 3f0da67..942f5de 100644 --- a/tests/magazine.spec.js +++ b/tests/magazine.spec.js @@ -198,7 +198,10 @@ test('drawing a chart links the crop and excludes enclosed narration on approval await page.locator('#mag-library-list .mag-card').first().click(); await page.getByRole('button', { name: 'Review', exact: true }).click(); await page.getByRole('button', { name: 'Draw image', exact: true }).click(); - await page.locator('#mag-review-page-wrap').evaluate(el => { el.style.width = '400px'; }); + await page.locator('#mag-review-page-wrap').evaluate(el => { + el.style.width = '400px'; + el.scrollIntoView(); + }); const wrap = await page.locator('#mag-review-page-wrap').boundingBox(); await page.mouse.move(wrap.x + wrap.width * 40 / 595, wrap.y + wrap.height * 110 / 842); await page.mouse.down();