This commit is contained in:
parent
9b1e29bb79
commit
e6211f1333
12
CHANGELOG.md
12
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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="theme-color" content="#2563EB">
|
||||
<meta name="app-version" content="1.26.8">
|
||||
<meta name="app-version" content="1.26.9">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/static/icon.svg">
|
||||
@ -27,12 +27,12 @@
|
||||
|
||||
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.26.8">
|
||||
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.26.8">
|
||||
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.26.8">
|
||||
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.26.8">
|
||||
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.26.8">
|
||||
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.26.8">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.26.9">
|
||||
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.26.9">
|
||||
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.26.9">
|
||||
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.26.9">
|
||||
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.26.9">
|
||||
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.26.9">
|
||||
|
||||
|
||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||
@ -403,7 +403,7 @@ window.toggleNavTree = function(treeId, chevronId) {
|
||||
</script>
|
||||
|
||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||
<script src="/static/loader.js?v=1.26.8"></script>
|
||||
<script src="/static/loader.js?v=1.26.9"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -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
|
||||
? '<span class="mdi mdi-loading mdi-spin"></span> 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() {
|
||||
'<div class="mag-review-page-controls" role="group" aria-label="Review pages">' +
|
||||
'<button type="button" class="btn-sm" aria-label="Previous page" ' + (prevPage ? '' : 'disabled') +
|
||||
' onclick="magReviewGoPage(' + prevPage + ')"><span class="mdi mdi-chevron-left" aria-hidden="true"></span></button>' +
|
||||
'<strong class="mag-review-page-number">Article p. ' + (page ?? '—') + '</strong>' +
|
||||
'<strong class="mag-review-page-number">Page ' + (page ?? '—') + '</strong>' +
|
||||
'<button type="button" class="btn-sm" aria-label="Next page" ' + (nextPage ? '' : 'disabled') +
|
||||
' onclick="magReviewGoPage(' + nextPage + ')"><span class="mdi mdi-chevron-right" aria-hidden="true"></span></button></div>' +
|
||||
'<div class="mag-review-page-controls mag-review-issue-controls" role="group" aria-label="Whole magazine pages">' +
|
||||
@ -617,7 +674,7 @@ function renderReview() {
|
||||
'<button type="button" class="btn-sm" onclick="magReviewToggleDraw()" id="mag-draw-toggle">' +
|
||||
'<span class="mdi mdi-vector-square-edit" aria-hidden="true"></span> Draw text</button>' +
|
||||
'<button type="button" class="btn-sm" id="mag-draw-image-toggle" onclick="magReviewToggleDraw(\'image\')">' +
|
||||
'<span class="mdi mdi-image-outline" aria-hidden="true"></span> Draw graphic</button>' +
|
||||
'<span class="mdi mdi-image-outline" aria-hidden="true"></span> Draw image</button>' +
|
||||
'<button type="button" class="btn-sm" onclick="magReviewToggleDraw(\'heading\')">' +
|
||||
'<span class="mdi mdi-format-header-1" aria-hidden="true"></span> Headline</button>' +
|
||||
'<button type="button" class="btn-sm" onclick="magReviewToggleDraw(\'page_number\')">' +
|
||||
@ -844,12 +901,23 @@ function setupMagDrawing() {
|
||||
// without needing to be re-attached.
|
||||
window.addEventListener('mouseup', (e) => {
|
||||
if (!_magDrawState) return;
|
||||
// Capture the release position even when the pointer left the page.
|
||||
$('mag-review-page-wrap')?.dispatchEvent(new MouseEvent('mousemove', { clientX: e.clientX, clientY: e.clientY }));
|
||||
const wrap = $('mag-review-page-wrap');
|
||||
const box = $('mag-draw-rect');
|
||||
if (wrap && box) {
|
||||
const rect = wrap.getBoundingClientRect();
|
||||
const xPct = Math.max(0, Math.min(100, (e.clientX - rect.left) / rect.width * 100));
|
||||
const yPct = Math.max(0, Math.min(100, (e.clientY - rect.top) / rect.height * 100));
|
||||
const left = Math.max(0, Math.min(_magDrawState.startXPct, xPct));
|
||||
const top = Math.max(0, Math.min(_magDrawState.startYPct, yPct));
|
||||
const w = Math.abs(xPct - _magDrawState.startXPct);
|
||||
const h = Math.abs(yPct - _magDrawState.startYPct);
|
||||
box.hidden = false;
|
||||
box.style.left = left + '%'; box.style.top = top + '%';
|
||||
box.style.width = w + '%'; box.style.height = h + '%';
|
||||
}
|
||||
_magDrawState = null;
|
||||
if (!box || box.hidden) return;
|
||||
const w = parseFloat(box.style.width), h = parseFloat(box.style.height);
|
||||
const w = parseFloat(box.style.width) || 0, h = parseFloat(box.style.height) || 0;
|
||||
if (w < 1 || h < 1) { box.hidden = true; return; } // too small to be intentional
|
||||
magReviewOpenDrawPanel(box);
|
||||
});
|
||||
@ -897,8 +965,8 @@ function magReviewOpenDrawPanel(box) {
|
||||
'<span class="mdi mdi-plus" aria-hidden="true"></span> Add region</button>' +
|
||||
'</div>';
|
||||
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', () => {
|
||||
|
||||
@ -11,24 +11,24 @@
|
||||
<div class="card mag-new">
|
||||
<h3>Add a magazine or paper</h3>
|
||||
<div class="mag-new-grid">
|
||||
<label for="mag-new-title">Title
|
||||
<input id="mag-new-title" type="text" placeholder="Scientific American" autocomplete="off">
|
||||
<label for="mag-new-title">Title <span class="muted">(optional)</span>
|
||||
<input id="mag-new-title" type="text" placeholder="Auto-filled from PDF filename if blank" autocomplete="off">
|
||||
</label>
|
||||
<label for="mag-new-issue">Issue
|
||||
<label for="mag-new-issue">Issue <span class="muted">(optional)</span>
|
||||
<input id="mag-new-issue" type="text" placeholder="September 2026" autocomplete="off">
|
||||
</label>
|
||||
</div>
|
||||
<div class="reader-dropzone" id="mag-dropzone">
|
||||
<span class="mdi mdi-cloud-upload-outline reader-dropzone-icon"></span>
|
||||
<strong>Drop a PDF</strong>
|
||||
<div class="reader-dropzone" id="mag-dropzone" style="cursor: pointer;">
|
||||
<span class="mdi mdi-cloud-upload-outline reader-dropzone-icon" id="mag-dropzone-icon"></span>
|
||||
<strong id="mag-dropzone-title">Drop a PDF or click to browse</strong>
|
||||
<span class="note" id="mag-dropzone-note">Scanned issues work too — pages without selectable text are read with OCR.</span>
|
||||
<label class="btn-secondary btn-sm reader-file-pick" title="Choose a PDF file">
|
||||
<label class="btn-secondary btn-sm reader-file-pick" title="Choose a PDF file" onclick="event.stopPropagation()">
|
||||
<span class="mdi mdi-file-upload-outline"></span> Choose file
|
||||
<input type="file" id="mag-new-file" accept="application/pdf" style="display:none">
|
||||
</label>
|
||||
</div>
|
||||
<div class="mag-new-actions">
|
||||
<button type="button" class="btn primary" onclick="magCreate()">
|
||||
<button type="button" class="btn primary" id="mag-add-btn" onclick="magCreate()">
|
||||
<span class="mdi mdi-plus"></span> Add magazine
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user