Some checks are pending
Test and SonarQube / quality (push) Waiting to run
- Export the region-type picker handlers and magRunPageQuality to the page (they threw "not defined", so the picker never opened and Find missed text / Verify OCR did nothing) - Add a unit test that fails when an inline mag* handler is not exported - Quality audit: pixel-level clipping check on all four box edges, duplicate-article detection, detected full-page advertisements skipped and shown green (no manual button) - Driving mode text is karaoke-style (whole article, current sentence and word highlighted) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
322 lines
21 KiB
HTML
322 lines
21 KiB
HTML
<div class="section-head">
|
||
<span class="section-icon"><span class="mdi mdi-newspaper-variant-outline"></span></span>
|
||
<div class="section-title">
|
||
<h2>Magazines</h2>
|
||
<p>Listen to a magazine one article at a time — from its true beginning to its true end, without adverts, sidebars or a neighbouring piece spliced into the middle.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Library ──────────────────────────────────────────────────────── -->
|
||
<div id="mag-view-library">
|
||
<div class="card mag-new">
|
||
<h3>Add a magazine or paper</h3>
|
||
<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">
|
||
<span class="mdi mdi-file-upload-outline"></span> Import PDF
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h3>Your magazines</h3>
|
||
<div id="mag-library-list" class="mag-grid"></div>
|
||
</div>
|
||
|
||
<!-- ── Contents ─────────────────────────────────────────────────────── -->
|
||
<div id="mag-view-contents" hidden>
|
||
<div class="mag-bar">
|
||
<button type="button" class="btn-sm" onclick="magBackToLibrary()">
|
||
<span class="mdi mdi-arrow-left"></span> All magazines
|
||
</button>
|
||
<h3 id="mag-doc-title" contenteditable="true" spellcheck="false" title="Click to edit the magazine title"
|
||
onblur="magCommitDocField(this, 'title')"
|
||
onkeydown="if(event.key==='Enter'){event.preventDefault();this.blur();}">Magazine</h3>
|
||
<input id="mag-doc-issue" type="text" class="mag-doc-issue-input" placeholder="Issue, e.g. September 2026"
|
||
title="Issue date -- used for the year/month in exported MP3 filenames"
|
||
onblur="magCommitDocField(this, 'issue')"
|
||
onkeydown="if(event.key==='Enter'){this.blur();}">
|
||
<div class="mag-bar-actions">
|
||
<button type="button" class="btn-sm" id="mag-quality-run-btn" onclick="magQualityRun()"
|
||
title="Check every page: add text no box covers, re-read suspicious OCR, and list pages that still need a look">
|
||
<span class="mdi mdi-clipboard-check-outline"></span> <span class="mag-qrun-label">Quality check / run</span>
|
||
</button>
|
||
<button type="button" class="btn-sm" id="mag-contents-view-toggle" onclick="magToggleContentsView()" title="Switch between an article list and a page-by-page thumbnail grid">
|
||
<span class="mdi mdi-view-grid-outline"></span> Thumbnails
|
||
</button>
|
||
<span class="mag-cols-control" id="mag-cols-control" hidden>
|
||
<label for="mag-cols-range">Columns</label>
|
||
<input type="range" id="mag-cols-range" min="1" max="12" step="1" value="8" oninput="magSetPagesCols(this.value)">
|
||
<span id="mag-cols-value" class="muted">8</span>
|
||
</span>
|
||
<button type="button" class="btn-sm" id="mag-contents-select-toggle" aria-pressed="false" onclick="magContentsToggleSelect()" title="Select several articles to approve or reject at once">
|
||
<span class="mdi mdi-checkbox-multiple-marked-outline"></span> Select
|
||
</button>
|
||
<button type="button" class="btn-sm" id="mag-playlist-toggle" aria-pressed="false" onclick="magTogglePlaylist()"
|
||
title="Show every article with a generated MP3, in reading order, with a player">
|
||
<span class="mdi mdi-playlist-music-outline"></span> Playlist
|
||
</button>
|
||
<button type="button" class="btn-sm" id="mag-driving-open" onclick="magOpenDriving()"
|
||
title="Open the magazine playlist with large, distraction-free controls">
|
||
<span class="mdi mdi-car-outline"></span> Driving mode
|
||
</button>
|
||
<button type="button" class="btn-sm primary" onclick="magPlayAll()">
|
||
<span class="mdi mdi-playlist-play"></span> Play all articles
|
||
</button>
|
||
<button type="button" class="btn-sm" onclick="magIngest()">
|
||
<span class="mdi mdi-refresh"></span> Re-run detection
|
||
</button>
|
||
<button type="button" class="btn-sm" onclick="magReocrMagazine()" title="Send every page through the configured OCR engine again, then re-detect articles">
|
||
<span class="mdi mdi-text-recognition"></span> OCR again (whole magazine)
|
||
</button>
|
||
<button type="button" class="btn-sm" id="mag-refine-all-figures-btn" onclick="magRefineAllFigures()"
|
||
title="Batch-check figure boundaries on every page in this magazine">
|
||
<span class="mdi mdi-image-auto-adjust"></span> Fix all figure boundaries
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div id="mag-quality-report" class="mag-quality-report" role="region" aria-label="Quality check result" aria-live="polite" hidden></div>
|
||
<div id="mag-contents-bulkbar" class="mag-bulkbar" hidden></div>
|
||
<div class="mag-audio-export">
|
||
<label for="mag-audio-voice">Narrator for exported MP3s</label>
|
||
<select id="mag-audio-voice"></select>
|
||
<label for="mag-audio-interview-voice">Interview voice 2 (optional)</label>
|
||
<select id="mag-audio-interview-voice" title="When an article contains two recurring speaker labels, the second speaker uses this voice. Normal articles are unaffected.">
|
||
<option value="">— use narrator only —</option>
|
||
</select>
|
||
<button type="button" class="btn-sm" id="mag-audio-export-btn" onclick="magGenerateAllAudio()"
|
||
title="Render every approved article that doesn't already have an MP3 -- title, author, cover image and full text embedded as tags -- for a phone's own music/podcast player or a car head unit. Safe to click again after an aborted run: already-generated articles are skipped.">
|
||
<span class="mdi mdi-file-music-outline"></span> Generate all audio (MP3)
|
||
</button>
|
||
<button type="button" class="btn-sm" id="mag-audio-rebuild-btn" onclick="magGenerateAllAudio(true)"
|
||
title="Rebuild every approved MP3 using the latest OCR cleanup, pronunciation and interview-voice settings. Existing files are replaced.">
|
||
<span class="mdi mdi-refresh"></span> Regenerate all MP3s
|
||
</button>
|
||
<a class="btn-sm" id="mag-audio-zip-link" href="#" hidden download>
|
||
<span class="mdi mdi-folder-zip-outline"></span> Download all (ZIP)
|
||
</a>
|
||
<span class="mag-audio-export-progress-wrap" hidden id="mag-audio-export-progress-wrap">
|
||
<progress id="mag-audio-export-progress" max="1" value="0"></progress>
|
||
<span id="mag-audio-export-count" class="muted"></span>
|
||
</span>
|
||
<span id="mag-audio-export-status" class="muted" role="status" aria-live="polite"></span>
|
||
</div>
|
||
<div id="mag-playlist" class="mag-playlist" hidden>
|
||
<div class="mag-playlist-heading">
|
||
<div>
|
||
<strong>Magazine playlist</strong>
|
||
<span class="muted">Page · article · duration</span>
|
||
</div>
|
||
<label class="mag-karaoke-toggle" title="Highlight the sentence and word approximately as the MP3 plays">
|
||
<input type="checkbox" id="mag-playlist-karaoke" checked onchange="magPlaylistSync()">
|
||
Follow spoken text
|
||
</label>
|
||
</div>
|
||
<div class="mag-playlist-player">
|
||
<img id="mag-playlist-cover" class="mag-playlist-cover" alt="" hidden>
|
||
<div class="mag-playlist-nowplaying">
|
||
<strong id="mag-playlist-title">—</strong>
|
||
<span id="mag-playlist-sub" class="muted"></span>
|
||
<audio id="mag-playlist-audio" controls preload="none" style="width:100%"></audio>
|
||
</div>
|
||
<button type="button" class="btn-sm" onclick="magTogglePlaylist()" title="Close playlist" aria-label="Close playlist">
|
||
<span class="mdi mdi-close"></span>
|
||
</button>
|
||
</div>
|
||
<ol id="mag-playlist-tracks" class="mag-playlist-tracks"></ol>
|
||
<div id="mag-playlist-companion" class="mag-playlist-companion" hidden>
|
||
<figure id="mag-playlist-visual" class="mag-playlist-visual" hidden></figure>
|
||
<div>
|
||
<div id="mag-playlist-sync-note" class="mag-playlist-sync-note muted">Text timing is estimated from the MP3 position.</div>
|
||
<article id="mag-playlist-transcript" class="mag-playlist-transcript" aria-label="Article transcript"></article>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="mag-ingest-status" class="mag-status" hidden></div>
|
||
<!-- Decorative "watch it work" visualisation -- the essential
|
||
progress information (percentage, page count, done/failed) is
|
||
in #mag-ingest-status above, which is what a screen reader user
|
||
needs; this panel adds nothing beyond that but noisy churn. -->
|
||
<div id="mag-ingest-preview" class="mag-ingest-preview" hidden aria-hidden="true">
|
||
<div class="mag-ingest-preview-side">
|
||
<span class="mag-ingest-preview-label">Page <span id="mag-ingest-preview-stage"></span></span>
|
||
<div class="mag-ingest-preview-canvas">
|
||
<img id="mag-ingest-preview-img" alt="Page currently being read">
|
||
<div id="mag-ingest-preview-boxes" class="mag-ingest-preview-boxes"></div>
|
||
</div>
|
||
<div id="mag-ingest-preview-legend" class="mag-ingest-preview-legend"></div>
|
||
</div>
|
||
<div class="mag-ingest-preview-resize" id="mag-ingest-preview-resize" role="separator" aria-orientation="vertical" aria-label="Resize page and recognised text panes" title="Drag to resize page and recognised text panes"></div>
|
||
<div class="mag-ingest-preview-side">
|
||
<span class="mag-ingest-preview-label">Recognised text</span>
|
||
<div id="mag-ingest-preview-text" class="mag-ingest-preview-text"></div>
|
||
</div>
|
||
</div>
|
||
<div id="mag-contents"></div>
|
||
</div>
|
||
|
||
<!-- ── Review (Stage D) ─────────────────────────────────────────────── -->
|
||
<div id="mag-view-review" hidden>
|
||
<div class="mag-bar">
|
||
<button type="button" class="btn-sm" onclick="magBackToContents()">
|
||
<span class="mdi mdi-arrow-left"></span> Contents
|
||
</button>
|
||
<h3>Review article boundaries</h3>
|
||
</div>
|
||
<div id="mag-review"></div>
|
||
</div>
|
||
|
||
<!-- ── Reading ──────────────────────────────────────────────────────── -->
|
||
<div id="mag-view-read" hidden>
|
||
<div class="mag-bar">
|
||
<button type="button" class="btn-sm" onclick="magBackToContents()">
|
||
<span class="mdi mdi-arrow-left"></span> Contents
|
||
</button>
|
||
<div class="mag-read-id">
|
||
<h3 id="mag-read-title">Article</h3>
|
||
<span id="mag-read-by" class="muted"></span>
|
||
<span id="mag-read-pages" class="muted"></span>
|
||
</div>
|
||
<button type="button" class="btn-sm" onclick="magToggleDriving()" title="Large controls for listening without looking at the screen">
|
||
<span class="mdi mdi-car-outline"></span> Driving mode
|
||
</button>
|
||
</div>
|
||
|
||
<div class="mag-controls">
|
||
<div class="mag-voice-pick">
|
||
<label for="mag-voice">Narrator</label>
|
||
<select id="mag-voice"></select>
|
||
</div>
|
||
<button type="button" class="btn primary" id="mag-play" onclick="magPlay()"
|
||
aria-label="Play narration">
|
||
<span class="mdi mdi-play"></span> Listen
|
||
</button>
|
||
<button type="button" class="btn" id="mag-prepare" onclick="magPrepareAudio()"
|
||
title="Synthesise the whole article now, so it plays straight through without waiting sentence by sentence">
|
||
<span class="mdi mdi-waveform"></span> Prepare audio
|
||
</button>
|
||
<span id="mag-prep-status" class="muted" role="status" aria-live="polite"></span>
|
||
<span id="mag-read-progress" class="muted" aria-live="polite"></span>
|
||
<span id="mag-queue-progress" class="muted" aria-live="polite"></span>
|
||
<label class="mag-karaoke-toggle" title="Highlight each word approximately as it is spoken">
|
||
<input type="checkbox" id="mag-karaoke-cb" checked onchange="TTVC.magazine.karaoke = this.checked">
|
||
Word highlighting
|
||
</label>
|
||
<label class="mag-karaoke-toggle" title="Speak each figure's description where it appears, if one has been generated in review. Takes effect next time this article is opened.">
|
||
<input type="checkbox" id="mag-read-descriptions">
|
||
Read figure descriptions
|
||
</label>
|
||
<div class="mag-pane-tabs" role="group" aria-label="Reading view">
|
||
<button type="button" class="btn-sm active" data-mag-pane="reflow"
|
||
aria-pressed="true" onclick="magSetPane('reflow')">Article</button>
|
||
<button type="button" class="btn-sm" data-mag-pane="pdf"
|
||
aria-pressed="false" onclick="magSetPane('pdf')">Original page</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="mag-pane-reflow" class="mag-pane">
|
||
<div id="mag-read-image" class="mag-read-image" hidden></div>
|
||
<div id="mag-article" class="mag-article"></div>
|
||
</div>
|
||
|
||
<div id="mag-pane-pdf" class="mag-pane" hidden>
|
||
<div class="mag-page-wrap">
|
||
<img id="mag-page-img" alt="">
|
||
<div id="mag-page-box" class="mag-page-box" hidden></div>
|
||
</div>
|
||
<p id="mag-page-label" class="muted"></p>
|
||
</div>
|
||
|
||
<!-- Podcast mode -->
|
||
<details class="card mag-podcast">
|
||
<summary><span class="mdi mdi-podcast"></span> Podcast mode — discuss this article</summary>
|
||
<p class="muted">Two or three hosts discuss the article for someone who has not read it. The script is written by the configured LLM from the article text; it will not invent facts, but it is a generated discussion, not the author’s words.</p>
|
||
<div class="mag-hosts">
|
||
<div class="mag-voice-pick"><label for="mag-host-a">Host A</label><select id="mag-host-a"></select></div>
|
||
<div class="mag-voice-pick"><label for="mag-host-b">Host B</label><select id="mag-host-b"></select></div>
|
||
<div class="mag-voice-pick"><label for="mag-host-c">Host C <span class="muted">(optional)</span></label><select id="mag-host-c"></select></div>
|
||
</div>
|
||
<div class="mag-podcast-actions">
|
||
<button type="button" class="btn" id="mag-podcast-gen" onclick="magGeneratePodcast()">Generate script</button>
|
||
<button type="button" class="btn primary" id="mag-podcast-play" onclick="magPlayPodcast()" hidden>Play podcast</button>
|
||
</div>
|
||
<div id="mag-podcast-script"></div>
|
||
</details>
|
||
</div>
|
||
|
||
<!-- ── Driving mode overlay ─────────────────────────────────────────── -->
|
||
<!-- Large controls, minimal text, no interaction required beyond play/
|
||
pause/skip -- reuses whatever is already playing rather than its
|
||
own playback path. -->
|
||
<div id="mag-driving" class="mag-driving" hidden role="dialog" aria-label="Driving mode">
|
||
<button type="button" class="mag-driving-exit" onclick="magToggleDriving()" aria-label="Exit driving mode">
|
||
<span class="mdi mdi-close"></span>
|
||
</button>
|
||
<div class="mag-driving-body">
|
||
<h2 id="mag-driving-title">Article</h2>
|
||
<p id="mag-driving-by" class="muted"></p>
|
||
<div id="mag-driving-queue" class="mag-driving-queue" hidden>
|
||
<span id="mag-queue-progress-2" class="muted"></span>
|
||
</div>
|
||
<figure id="mag-driving-visual" class="mag-driving-visual" hidden></figure>
|
||
<div id="mag-driving-lyrics" class="mag-driving-lyrics" tabindex="0" aria-label="Article text, following the audio" hidden></div>
|
||
<div class="mag-driving-seek">
|
||
<input type="range" id="mag-driving-seek" min="0" max="1000" value="0" step="1"
|
||
aria-label="Position in article" oninput="magDrivingSeek(this.value)">
|
||
<div class="mag-driving-time" id="mag-driving-time" aria-live="off">0:00 / 0:00</div>
|
||
</div>
|
||
<div class="mag-driving-controls">
|
||
<button type="button" class="mag-driving-btn is-small" onclick="magDrivingPrevious()" aria-label="Previous article" title="Previous article">
|
||
<span class="mdi mdi-skip-previous"></span>
|
||
</button>
|
||
<button type="button" class="mag-driving-btn" onclick="magDrivingSkip(-15)" aria-label="Back 15 seconds" title="Back 15 seconds">
|
||
<span class="mdi mdi-rewind-15"></span>
|
||
</button>
|
||
<button type="button" class="mag-driving-btn mag-driving-play" id="mag-driving-play" onclick="magDrivingTogglePlayback()" aria-label="Play">
|
||
<span class="mdi mdi-play"></span>
|
||
</button>
|
||
<button type="button" class="mag-driving-btn" onclick="magDrivingSkip(15)" aria-label="Forward 15 seconds" title="Forward 15 seconds">
|
||
<span class="mdi mdi-fast-forward-15"></span>
|
||
</button>
|
||
<button type="button" class="mag-driving-btn is-small" onclick="magDrivingNext()" aria-label="Next article" title="Next article">
|
||
<span class="mdi mdi-skip-next"></span>
|
||
</button>
|
||
</div>
|
||
<div class="mag-driving-options" role="group" aria-label="Playback options">
|
||
<button type="button" class="mag-driving-opt" id="mag-driving-speed" onclick="magDrivingCycleSpeed()"
|
||
aria-label="Playback speed 1×" title="Playback speed">1×</button>
|
||
<button type="button" class="mag-driving-opt" id="mag-driving-text-toggle" aria-pressed="true" onclick="magDrivingToggleText()"
|
||
title="Show or hide the text being read"><span class="mdi mdi-text-long" aria-hidden="true"></span> Text</button>
|
||
<button type="button" class="mag-driving-opt" id="mag-driving-image-toggle" aria-pressed="true" onclick="magDrivingToggleImages()"
|
||
title="Show or hide the article's pictures"><span class="mdi mdi-image-outline" aria-hidden="true"></span> Images</button>
|
||
</div>
|
||
</div>
|
||
</div>
|