Compare commits
2 Commits
11142842c2
...
c1a2d6dc74
| Author | SHA1 | Date | |
|---|---|---|---|
| c1a2d6dc74 | |||
| 30e01a5cec |
20
CHANGELOG.md
20
CHANGELOG.md
@ -5,6 +5,26 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
|
||||
---
|
||||
|
||||
## [1.26.14] — 2026-09-10
|
||||
|
||||
### Added
|
||||
- **Studio-Style Side-by-Side Magazine Upload Layout.**
|
||||
- Re-designed the Magazine upload card to mirror Studio mode's side-by-side layout: left panel contains optional Title & Issue metadata inputs plus a prominent **Durchsuchen** button (`<span class="mdi mdi-folder-open-outline"></span> Durchsuchen`), and right panel contains the clean `.reader-dropzone`.
|
||||
- Removed the redundant `+ Add magazine` button for a clean, direct, one-action upload flow.
|
||||
|
||||
---
|
||||
|
||||
## [1.26.13] — 2026-09-10
|
||||
|
||||
### Fixed
|
||||
- **App-Wide Drag-and-Drop Safety Net & Dropzone Reliability.**
|
||||
- Installed an eager app-wide capture-phase `dragover` / `drop` safety net in `utils.js` that intercepts file drops on `window` and `document` to guarantee the browser never navigates away to open a dropped PDF in a new tab.
|
||||
- Improved `#mag-dropzone` drag event handling with nested element drag-depth tracking (`dragCounter`), preventing dragover class flickering when hovering over dropzone titles, icons, or buttons.
|
||||
- Direct file passing in `magCreate(file)`, removing programmatic `<input>` DataTransfer mutation to eliminate double invocation and race conditions during drag-and-drop.
|
||||
- Cleaned up duplicate variable scope declarations in `renderReview()` in `magazine.js`.
|
||||
|
||||
---
|
||||
|
||||
## [1.26.12] — 2026-09-10
|
||||
|
||||
### Fixed
|
||||
|
||||
@ -180,29 +180,17 @@
|
||||
color: var(--subtext);
|
||||
font-size: 12px;
|
||||
}
|
||||
#s-reader .reader-source-grid {
|
||||
#s-reader .reader-source-grid,
|
||||
#s-caststudio .reader-source-grid,
|
||||
#s-magazine .reader-source-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(260px, .8fr);
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
#s-caststudio .reader-source-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(260px, .8fr);
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
#s-reader .reader-paste-panel {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
}
|
||||
#s-caststudio .reader-paste-panel {
|
||||
#s-reader .reader-paste-panel,
|
||||
#s-caststudio .reader-paste-panel,
|
||||
#s-magazine .reader-paste-panel {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -409,8 +397,9 @@
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
#s-reader .reader-source-grid { grid-template-columns: 1fr; }
|
||||
#s-caststudio .reader-source-grid { grid-template-columns: 1fr; }
|
||||
#s-reader .reader-source-grid,
|
||||
#s-caststudio .reader-source-grid,
|
||||
#s-magazine .reader-source-grid { grid-template-columns: 1fr; }
|
||||
#s-reader .reader-tuning-row { grid-template-columns: 1fr; gap: 5px; }
|
||||
#s-caststudio .reader-tuning-row { grid-template-columns: 1fr; gap: 5px; }
|
||||
}
|
||||
|
||||
70
static/dist/main.min.js
vendored
70
static/dist/main.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,11 +1,11 @@
|
||||
// ── Init ──────────────────────────────────────────────────────────────────
|
||||
|
||||
initBenchmarkSampleControls();
|
||||
if (typeof initBenchmarkSampleControls === 'function') initBenchmarkSampleControls();
|
||||
// Sync visible preview-text input with hidden benchmark-sample-text
|
||||
(function syncPreviewInput() {
|
||||
const visible = $('vl-preview-sample');
|
||||
const hidden = $('benchmark-sample-text');
|
||||
if (!visible) return;
|
||||
if (!visible || typeof BENCHMARK_SAMPLE_STORAGE_KEY === 'undefined') return;
|
||||
const stored = localStorage.getItem(BENCHMARK_SAMPLE_STORAGE_KEY);
|
||||
visible.value = stored || DEFAULT_BENCHMARK_SAMPLE_TEXT;
|
||||
if (hidden) hidden.value = visible.value;
|
||||
@ -19,12 +19,14 @@ initBenchmarkSampleControls();
|
||||
(function syncPreviewLangBtn() {
|
||||
const ta = $('vl-preview-sample');
|
||||
const btn = $('vl-preview-lang-btn');
|
||||
if (!ta || !btn) return;
|
||||
if (!ta || !btn || typeof VL_SAMPLE_TEXTS === 'undefined') return;
|
||||
const cur = ta.value.trim();
|
||||
const matched = Object.keys(VL_SAMPLE_TEXTS).find(l => VL_SAMPLE_TEXTS[l] === cur) || 'EN';
|
||||
const cc = (_VL_LANG_FLAG[matched] || 'gb').toLowerCase();
|
||||
const flags = typeof _VL_LANG_FLAG !== 'undefined' ? _VL_LANG_FLAG : {};
|
||||
const labels = typeof LANGUAGE_LABELS !== 'undefined' ? LANGUAGE_LABELS : {};
|
||||
const cc = (flags[matched] || 'gb').toLowerCase();
|
||||
btn.innerHTML = `<span class="fi fi-${cc}"></span>`;
|
||||
btn.title = (LANGUAGE_LABELS[matched] || matched) + ' — click to change sample language';
|
||||
btn.title = (labels[matched] || matched) + ' — click to change sample language';
|
||||
btn.dataset.lang = matched;
|
||||
})();
|
||||
|
||||
|
||||
@ -205,7 +205,7 @@ function magShowDroppedFile(file) {
|
||||
} else {
|
||||
if (dropzone) dropzone.classList.remove('is-selected', 'is-uploading');
|
||||
if (noteEl) noteEl.textContent = MAG_DROPZONE_DEFAULT_NOTE;
|
||||
if (titleEl) titleEl.textContent = 'Drop a PDF or click to browse';
|
||||
if (titleEl) titleEl.textContent = 'Drop a PDF document';
|
||||
if (iconEl) iconEl.className = 'mdi mdi-cloud-upload-outline reader-dropzone-icon';
|
||||
}
|
||||
}
|
||||
@ -213,13 +213,15 @@ function magShowDroppedFile(file) {
|
||||
function installGlobalMagDropGuard() {
|
||||
if (window._magDropGuardInstalled) return;
|
||||
window._magDropGuardInstalled = true;
|
||||
window.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
|
||||
});
|
||||
window.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
const guard = (e) => {
|
||||
if (e.dataTransfer && Array.from(e.dataTransfer.types || []).includes('Files')) {
|
||||
e.preventDefault();
|
||||
if (e.type === 'dragover' && e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
};
|
||||
window.addEventListener('dragenter', guard, true);
|
||||
window.addEventListener('dragover', guard, true);
|
||||
window.addEventListener('drop', guard, true);
|
||||
}
|
||||
installGlobalMagDropGuard();
|
||||
|
||||
@ -239,50 +241,61 @@ function setupMagDropzone() {
|
||||
}
|
||||
});
|
||||
|
||||
// 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.
|
||||
const armDropTarget = (e) => {
|
||||
let dragCounter = 0;
|
||||
|
||||
zone.addEventListener('dragenter', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dragCounter++;
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
|
||||
zone.classList.add('dragover');
|
||||
});
|
||||
|
||||
zone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
|
||||
zone.classList.add('dragover');
|
||||
};
|
||||
zone.addEventListener('dragenter', armDropTarget);
|
||||
zone.addEventListener('dragover', armDropTarget);
|
||||
});
|
||||
|
||||
zone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
zone.classList.remove('dragover');
|
||||
dragCounter = Math.max(0, dragCounter - 1);
|
||||
if (dragCounter === 0) {
|
||||
zone.classList.remove('dragover');
|
||||
}
|
||||
});
|
||||
|
||||
zone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dragCounter = 0;
|
||||
zone.classList.remove('dragover');
|
||||
const file = e.dataTransfer?.files?.[0];
|
||||
|
||||
let file = e.dataTransfer?.files?.[0];
|
||||
if (!file && e.dataTransfer?.items?.[0]) {
|
||||
file = e.dataTransfer.items[0].getAsFile();
|
||||
}
|
||||
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);
|
||||
magCreate();
|
||||
magCreate(file);
|
||||
});
|
||||
|
||||
input.addEventListener('change', () => {
|
||||
const file = input.files?.[0];
|
||||
if (file) {
|
||||
magShowDroppedFile(file);
|
||||
magCreate();
|
||||
magCreate(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function magCreate() {
|
||||
const file = $('mag-new-file')?.files?.[0];
|
||||
async function magCreate(droppedFile = null) {
|
||||
const file = droppedFile || $('mag-new-file')?.files?.[0];
|
||||
if (!file) return say('Choose or drop a PDF file first', 'err');
|
||||
|
||||
let title = ($('mag-new-title')?.value || '').trim();
|
||||
@ -293,6 +306,7 @@ async function magCreate() {
|
||||
const issue = ($('mag-new-issue')?.value || '').trim();
|
||||
|
||||
const addBtn = $('mag-add-btn');
|
||||
const browseBtn = $('mag-browse-btn');
|
||||
const dropzone = $('mag-dropzone');
|
||||
const titleInp = $('mag-new-title');
|
||||
const issueInp = $('mag-new-issue');
|
||||
@ -312,6 +326,7 @@ async function magCreate() {
|
||||
}
|
||||
if (progress) progress.hidden = false;
|
||||
if (pickBtn) pickBtn.hidden = true;
|
||||
if (browseBtn) browseBtn.hidden = true;
|
||||
if (iconEl) iconEl.className = 'mdi mdi-loading mdi-spin reader-dropzone-icon';
|
||||
if (titleEl) titleEl.textContent = 'Uploading & Ingesting PDF...';
|
||||
if (noteEl) noteEl.textContent = `Processing "${file.name}" (${sizeMb} MB). Transferring source file and extracting layout...`;
|
||||
@ -328,6 +343,7 @@ async function magCreate() {
|
||||
}
|
||||
if (progress) progress.hidden = true;
|
||||
if (pickBtn) pickBtn.hidden = false;
|
||||
if (browseBtn) browseBtn.hidden = false;
|
||||
if (addBtn) {
|
||||
addBtn.disabled = false;
|
||||
addBtn.innerHTML = origBtnText;
|
||||
@ -675,48 +691,33 @@ function renderReview() {
|
||||
const included = _magReviewChecked || new Set(a.blocks || []);
|
||||
const conf = confidenceLabel(a.confidence);
|
||||
const page = state.reviewPage;
|
||||
const pageMeta = (state.layout.pages || []).find((p) => p.page === page);
|
||||
const pageBlocks = page == null ? [] : blocksForPages([page]);
|
||||
const pageImages = page == null ? [] : (state.layout.images || [])
|
||||
.filter((img) => img.page === page).sort((x, y) => (x.reading_order || 0) - (y.reading_order || 0));
|
||||
const pageMeta = page != null ? (state.layout?.pages || []).find((p) => p.page === page) : null;
|
||||
const ownImages = new Set(a.images || []);
|
||||
|
||||
const pageBlocks = page == null ? [] : blocksForPages([page]);
|
||||
const pageImages = page == null ? [] : (state.layout?.images || [])
|
||||
.filter((img) => img.page === page).sort((x, y) => (x.reading_order || 0) - (y.reading_order || 0));
|
||||
// One merged, reading-order-sorted list so numbering on both sides matches
|
||||
// regardless of whether an item is text or a figure.
|
||||
const visualBlockIds = new Set(pageImages.map(i => i.source_block).filter(Boolean));
|
||||
const regions = pageBlocks.filter(b => !visualBlockIds.has(b.id)).map((b) => ({ kind: 'block', id: b.id, ro: b.reading_order, data: b }))
|
||||
.concat(pageImages.map((img) => ({ kind: 'image', id: img.id, ro: img.reading_order || 0, data: img })))
|
||||
const regions = pageBlocks.filter(b => !visualBlockIds.has(b.id)).map((b) => ({ kind: 'block', id: b.id, ro: b.reading_order ?? 0, data: b }))
|
||||
.concat(pageImages.map((img) => ({ kind: 'image', id: img.id, ro: img.reading_order ?? 0, data: img })))
|
||||
.sort((x, y) => x.ro - y.ro);
|
||||
|
||||
const spanIdx = state.reviewSpan.indexOf(page);
|
||||
const spanIdx = (state.reviewSpan || []).indexOf(page);
|
||||
const prevPage = spanIdx > 0 ? state.reviewSpan[spanIdx - 1] : null;
|
||||
const nextPage = spanIdx >= 0 && spanIdx < state.reviewSpan.length - 1 ? state.reviewSpan[spanIdx + 1] : null;
|
||||
const issuePages = Number(state.layout?.page_count || 0);
|
||||
const previousIssuePage = page > 1 ? page - 1 : null;
|
||||
if (!host) return;
|
||||
const page = state.reviewPage;
|
||||
const pageMeta = page != null ? (state.layout.pages || []).find((p) => p.page === page) : null;
|
||||
const article = state.article;
|
||||
const nextPage = spanIdx >= 0 && spanIdx < (state.reviewSpan || []).length - 1 ? state.reviewSpan[spanIdx + 1] : null;
|
||||
const issuePages = (state.layout || {}).page_count || '—';
|
||||
const nextIssuePage = page != null && page < (state.layout || {}).page_count ? page + 1 : null;
|
||||
const prevIssuePage = page != null && page > 1 ? page - 1 : null;
|
||||
|
||||
const included = new Set(article ? article.blocks || [] : []);
|
||||
const ownImages = new Set(article ? article.images || [] : []);
|
||||
|
||||
const pageBlocks = (state.layout.blocks || []).filter((b) => b.page === page);
|
||||
const pageImages = (state.layout.images || []).filter((i) => i.page === page);
|
||||
const regions = [
|
||||
...pageBlocks.map((b) => ({ kind: 'block', id: b.id, data: b, ro: b.reading_order ?? 0 })),
|
||||
...pageImages.map((i) => ({ kind: 'image', id: i.id, data: i, ro: i.reading_order ?? 0 })),
|
||||
].sort((a, b) => a.ro - b.ro);
|
||||
|
||||
let html = '';
|
||||
html += '<div class="mag-review-pagenav">' +
|
||||
'<div class="mag-review-pagenav-group" role="group" aria-label="Review pages">' +
|
||||
'<button type="button" class="btn-sm" aria-label="Previous page" ' + (state.reviewPageIdx > 0 ? '' : 'disabled') +
|
||||
' onclick="magReviewStepPage(-1)"><span class="mdi mdi-chevron-left" aria-hidden="true"></span></button>' +
|
||||
'<strong class="mag-review-page-number">Page ' + (page ?? '—') + '</strong>' +
|
||||
'<button type="button" class="btn-sm" aria-label="Next page" ' + (state.reviewPageIdx < (article?.pages || []).length - 1 ? '' : 'disabled') +
|
||||
'<button type="button" class="btn-sm" aria-label="Next page" ' + (state.reviewPageIdx < (a?.pages || []).length - 1 ? '' : 'disabled') +
|
||||
' onclick="magReviewStepPage(1)"><span class="mdi mdi-chevron-right" aria-hidden="true"></span></button></div>' +
|
||||
'<div class="mag-review-pagenav-group" role="group" aria-label="Whole magazine pages">' +
|
||||
'<button type="button" class="btn-sm" aria-label="Previous magazine page" ' + (prevIssuePage ? '' : 'disabled') +
|
||||
@ -745,7 +746,6 @@ function renderReview() {
|
||||
'</div>';
|
||||
|
||||
html += '<div class="mag-review-fields">';
|
||||
const a = state.article || {};
|
||||
html += '<div class="mag-review-field-row">' +
|
||||
'<label>Title<span class="mag-review-field-control"><input id="mag-rev-title" type="text" value="' + esc(a.title) + '"><button type="button" class="btn-sm" onclick="magReviewToggleDraw(\'title\')"><span class="mdi mdi-vector-square-edit"></span> Pick from page</button></span></label>' +
|
||||
'<label>Author<span class="mag-review-field-control"><input id="mag-rev-author" type="text" value="' + esc(a.author) + '"><button type="button" class="btn-sm" onclick="magReviewToggleDraw(\'author\')"><span class="mdi mdi-vector-square-edit"></span> Pick from page</button></span></label>' +
|
||||
@ -753,7 +753,6 @@ function renderReview() {
|
||||
'<span id="mag-draw-hint" class="muted"></span>';
|
||||
html += '</div>';
|
||||
|
||||
const conf = confidenceLabel(article ? article.confidence : 1.0);
|
||||
html += '<span class="mag-review-conf ' + conf.cls + '">' + esc(conf.text) + '</span>';
|
||||
html += '<p class="muted">Untick anything that is not part of this article — a sidebar, an advert, or a neighbouring piece. Only ticked text is read aloud. Click a numbered region on the page, or its matching entry on the right, to line the two up. A region labelled with another article belongs to a different piece sharing this page. Draw a missing text or image region below.</p>';
|
||||
|
||||
|
||||
@ -1392,6 +1392,24 @@ if (typeof MutationObserver !== 'undefined') {
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => initLangSelects(document));
|
||||
else initLangSelects(document);
|
||||
|
||||
// App-Wide Drag and Drop Safety Net:
|
||||
// Prevents the browser from navigating away to a dropped file (e.g. opening PDF in a new tab)
|
||||
// if a drop occurs outside an active dropzone or on elements without custom drop handling.
|
||||
(function installGlobalAppDropGuard() {
|
||||
if (root._appDropGuardInstalled) return;
|
||||
root._appDropGuardInstalled = true;
|
||||
const preventDefaultDrop = (e) => {
|
||||
if (e.dataTransfer && Array.from(e.dataTransfer.types || []).includes('Files')) {
|
||||
e.preventDefault();
|
||||
if (e.type === 'dragover') {
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener('dragover', preventDefaultDrop, true);
|
||||
window.addEventListener('drop', preventDefaultDrop, true);
|
||||
})();
|
||||
|
||||
Object.assign(root, {
|
||||
$, avatarColor, toast, confirmDialog, status, ensureStatusBar, updateStatusBar,
|
||||
escHtml, debounce, logErr, ttvcIsLowMemoryDevice, fmtDuration, loadingMarkup,
|
||||
|
||||
@ -10,30 +10,39 @@
|
||||
<div id="mag-view-library">
|
||||
<div class="card mag-new">
|
||||
<h3>Add a magazine or paper</h3>
|
||||
<div class="mag-new-grid">
|
||||
<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 <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" 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>
|
||||
<div class="mag-dropzone-progress" id="mag-dropzone-progress" hidden>
|
||||
<div class="mag-dropzone-progress-bar"></div>
|
||||
<div class="reader-source-grid">
|
||||
<div class="reader-paste-panel">
|
||||
<div class="reader-source-method">
|
||||
<span class="mdi mdi-file-document-edit-outline"></span>
|
||||
<div><strong>Import & Metadata</strong><span>Select a PDF file or configure optional title and issue details.</span></div>
|
||||
</div>
|
||||
<div class="mag-new-grid" style="margin-top: 4px; margin-bottom: 4px;">
|
||||
<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 <span class="muted">(optional)</span>
|
||||
<input id="mag-new-issue" type="text" placeholder="e.g. September 2026" autocomplete="off">
|
||||
</label>
|
||||
</div>
|
||||
<div class="reader-paste-actions" style="margin-top: auto; padding-top: 6px;">
|
||||
<label class="btn-primary btn-sm reader-file-pick" id="mag-browse-btn" title="Choose a PDF file from your computer">
|
||||
<span class="mdi mdi-folder-open-outline"></span> Durchsuchen
|
||||
<input type="file" id="mag-new-file" accept="application/pdf" style="display:none">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 document</strong>
|
||||
<span class="note" id="mag-dropzone-note">Scanned issues work too — pages without selectable text are read with OCR.</span>
|
||||
<div class="mag-dropzone-progress" id="mag-dropzone-progress" hidden>
|
||||
<div class="mag-dropzone-progress-bar"></div>
|
||||
</div>
|
||||
<label class="btn-secondary btn-sm reader-file-pick" id="mag-dropzone-pick-btn" title="Choose a PDF file" onclick="event.stopPropagation()">
|
||||
<span class="mdi mdi-file-upload-outline"></span> Import PDF
|
||||
</label>
|
||||
</div>
|
||||
<label class="btn-secondary btn-sm reader-file-pick" id="mag-dropzone-pick-btn" 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" id="mag-add-btn" onclick="magCreate()">
|
||||
<span class="mdi mdi-plus"></span> Add magazine
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -218,3 +218,35 @@ test('drawing a chart links the crop and excludes enclosed narration on approval
|
||||
await page.getByRole('button', { name: 'Approve', exact: true }).click();
|
||||
expect((await request).postDataJSON()).toMatchObject({ images: ['manual-chart'], blocks: ['b1'] });
|
||||
});
|
||||
|
||||
test('dropping a PDF file onto mag-dropzone triggers creation without browser navigation', async ({ page }) => {
|
||||
await mockBackend(page);
|
||||
let uploadCalled = false;
|
||||
await page.route('**/api/magazine/docs', route => {
|
||||
uploadCalled = true;
|
||||
return route.fulfill({ json: { id: DOC_ID } });
|
||||
});
|
||||
await page.route(`**/api/reader/docs/${DOC_ID}/source*`, route => route.fulfill({ json: { ok: true } }));
|
||||
|
||||
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('#app-skeleton', { state: 'detached' });
|
||||
await navTo(page, 's-magazine');
|
||||
|
||||
const dropzone = page.locator('#mag-dropzone');
|
||||
await expect(dropzone).toBeVisible();
|
||||
|
||||
const dataTransfer = await page.evaluateHandle(() => {
|
||||
const dt = new DataTransfer();
|
||||
const file = new File(['%PDF-1.4 dummy pdf content'], 'ct_KI_Wissen_2026.pdf', { type: 'application/pdf' });
|
||||
dt.items.add(file);
|
||||
return dt;
|
||||
});
|
||||
|
||||
await dropzone.dispatchEvent('dragenter', { dataTransfer });
|
||||
await dropzone.dispatchEvent('dragover', { dataTransfer });
|
||||
await dropzone.dispatchEvent('drop', { dataTransfer });
|
||||
|
||||
await page.waitForTimeout(300);
|
||||
expect(uploadCalled).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user