Release v1.52.0
Some checks are pending
Test and SonarQube / quality (push) Waiting to run

This commit is contained in:
mARTin-B78 2026-09-22 23:33:44 +02:00
parent c453295ab0
commit 2ba4daa742
6 changed files with 72 additions and 52 deletions

View File

@ -9,6 +9,14 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
--- ---
## [1.52.0] — 2026-09-22
### Changed
- **Magazine: the article-thumbnail grid is gone; "Thumbnails" now means the page-by-page view.** An article is a poor thumbnail unit — it can span many pages or share one with others — so the toggle is back to two states: List (articles, for the title/author/duration/status detail that only makes sense per article) and the page-complete thumbnail grid added earlier. "All pages" is also now the default view on first open.
- **Magazine: the page grid's column count is adjustable, 1-12**, via a slider next to the view toggle (shown only in that view) — a "zoom" between an overview of the whole issue and a close look at a few pages, remembered per browser.
---
## [1.51.0] — 2026-09-22 ## [1.51.0] — 2026-09-22
### Fixed ### Fixed
@ -4401,7 +4409,8 @@ Initial feature-complete release.
--- ---
[Unreleased]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.51.0...HEAD [Unreleased]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.52.0...HEAD
[1.52.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.51.0...v1.52.0
[1.51.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.50.0...v1.51.0 [1.51.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.50.0...v1.51.0
[1.50.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.49.0...v1.50.0 [1.50.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.49.0...v1.50.0
[1.49.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.48.2...v1.49.0 [1.49.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.48.2...v1.49.0

View File

@ -1 +1 @@
1.51.0 1.52.0

View File

@ -10,7 +10,7 @@
<meta name="format-detection" content="telephone=no"> <meta name="format-detection" content="telephone=no">
<meta name="color-scheme" content="light dark"> <meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#2563EB"> <meta name="theme-color" content="#2563EB">
<meta name="app-version" content="1.51.0"> <meta name="app-version" content="1.52.0">
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
<link rel="icon" href="/static/icon.svg" type="image/svg+xml"> <link rel="icon" href="/static/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/static/icon.svg"> <link rel="apple-touch-icon" href="/static/icon.svg">
@ -27,12 +27,12 @@
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── --> <!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css"> <link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/style.css?v=1.51.0"> <link rel="stylesheet" href="/static/style.css?v=1.52.0">
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.51.0"> <link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.52.0">
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.51.0"> <link rel="stylesheet" href="/static/css/background-jobs.css?v=1.52.0">
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.51.0"> <link rel="stylesheet" href="/static/css/voice-design.css?v=1.52.0">
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.51.0"> <link rel="stylesheet" href="/static/css/rehearser.css?v=1.52.0">
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.51.0"> <link rel="stylesheet" href="/static/css/audiobook.css?v=1.52.0">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── --> <!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -403,7 +403,7 @@ window.toggleNavTree = function(treeId, chevronId) {
</script> </script>
<!-- loader.js: fetches sections → loads JS modules → removes skeleton --> <!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
<script src="/static/loader.js?v=1.51.0"></script> <script src="/static/loader.js?v=1.52.0"></script>
</body> </body>
</html> </html>

View File

@ -22,9 +22,10 @@ const state = {
sentences: [], // [{ text, blockId, page, bbox }] sentences: [], // [{ text, blockId, page, bbox }]
images: [], // current article's figures, in reading order images: [], // current article's figures, in reading order
view: 'library', // library | contents | review | read view: 'library', // library | contents | review | read
contentsView: 'list', // list | grid — how the article list itself is shown contentsView: 'pages', // list | pages — how the article list itself is shown
contentsSelectMode: false, // batch-select articles for bulk approve/reject contentsSelectMode: false, // batch-select articles for bulk approve/reject
contentsSelected: new Set(), contentsSelected: new Set(),
pagesCols: 8, // "All pages" grid column count, user-adjustable 1-12
playlistCurrent: null, // article id of the MP3 playlist's current track playlistCurrent: null, // article id of the MP3 playlist's current track
pane: 'reflow', // reflow | pdf pane: 'reflow', // reflow | pdf
playing: false, playing: false,
@ -724,17 +725,21 @@ function renderContents() {
bySection.get(key).push(a); bySection.get(key).push(a);
}); });
const grid = state.contentsView === 'grid';
const toggleBtn = $('mag-contents-view-toggle'); const toggleBtn = $('mag-contents-view-toggle');
if (toggleBtn) { if (toggleBtn) {
const nextLabel = { list: 'Thumbnails', grid: 'All pages', pages: 'List' }[state.contentsView]; const nextLabel = { list: 'Thumbnails', pages: 'List' }[state.contentsView];
const nextIcon = { list: 'mdi-view-grid-outline', grid: 'mdi-view-module-outline', pages: 'mdi-view-list-outline' }[state.contentsView]; const nextIcon = { list: 'mdi-view-grid-outline', pages: 'mdi-view-list-outline' }[state.contentsView];
toggleBtn.innerHTML = '<span class="mdi ' + nextIcon + '"></span> ' + nextLabel; toggleBtn.innerHTML = '<span class="mdi ' + nextIcon + '"></span> ' + nextLabel;
toggleBtn.setAttribute('aria-pressed', String(state.contentsView !== 'list')); toggleBtn.setAttribute('aria-pressed', String(state.contentsView !== 'list'));
toggleBtn.title = 'Currently: ' + { list: 'list', grid: 'thumbnails (by article)', pages: 'all pages' }[state.contentsView] + toggleBtn.title = 'Currently: ' + { list: 'article list', pages: 'page thumbnails' }[state.contentsView] +
'. Click to switch to ' + nextLabel.toLowerCase() + '.'; '. Click to switch to ' + nextLabel.toLowerCase() + '.';
} }
const colsControl = $('mag-cols-control');
if (colsControl) colsControl.hidden = state.contentsView !== 'pages';
if (state.contentsView === 'pages') { if (state.contentsView === 'pages') {
const range = $('mag-cols-range'), valueEl = $('mag-cols-value');
if (range) range.value = state.pagesCols;
if (valueEl) valueEl.textContent = String(state.pagesCols);
renderPagesView(host); renderPagesView(host);
renderContentsBulkBar(); renderContentsBulkBar();
magUpdateAudioZipLink(); magUpdateAudioZipLink();
@ -768,53 +773,37 @@ function renderContents() {
let html = ''; let html = '';
bySection.forEach((arts, section) => { bySection.forEach((arts, section) => {
html += '<div class="mag-section"><h4>' + esc(section) + '</h4>' + (grid ? '<div class="mag-grid">' : '<ul class="mag-toc">'); html += '<div class="mag-section"><h4>' + esc(section) + '</h4><ul class="mag-toc">';
arts.forEach((a) => { arts.forEach((a) => {
const conf = confidenceLabel(a.confidence); const conf = confidenceLabel(a.confidence);
const done = a.status === 'approved'; const done = a.status === 'approved';
const progress = (state.doc.progress || {})[a.id]; const progress = (state.doc.progress || {})[a.id];
const statusBadge = '<span class="mag-pill ' + (done ? 'ok' : '') + '">' + const statusBadge = '<span class="mag-pill ' + (done ? 'ok' : '') + '">' +
(done ? 'approved' : a.status === 'rejected' ? 'rejected' : 'needs review') + '</span>'; (done ? 'approved' : a.status === 'rejected' ? 'rejected' : 'needs review') + '</span>';
if (grid) { html += '<li class="mag-toc-row' + (done ? ' is-approved' : '') + '">' +
const firstPage = (a.pages || [])[0]; selectHtml(a) +
html += '<div class="mag-card-wrap"><div class="mag-card' + (done ? ' is-approved' : '') + '">' + '<div class="mag-toc-main">' +
selectHtml(a) + titleHtml(a, 'mag-toc-title') +
(firstPage (a.author ? '<span class="mag-toc-by">by ' + esc(a.author) + '</span>' : '') +
? '<img class="mag-card-thumb" loading="lazy" alt="Page ' + firstPage + '" ' + '</div>' +
'src="/api/magazine/' + esc(state.doc.id) + '/pages/' + firstPage + '">' '<div class="mag-toc-meta">' +
: '<div class="mag-card-thumb"></div>') + '<span>' + fmtPages(a.pages) + '</span>' +
titleHtml(a, 'mag-card-title') + '<span>' + fmtDuration(a.estimated_seconds) + '</span>' +
(a.author ? '<span class="mag-card-sub">by ' + esc(a.author) + '</span>' : '') + '<span class="mag-pill ' + conf.cls + '" title="' + esc(conf.text) + '">' +
'<span class="mag-card-sub">' + fmtPages(a.pages) + ' · ' + fmtDuration(a.estimated_seconds) + '</span>' + esc(conf.badge) + '</span>' +
'<div class="mag-toc-meta"><span class="mag-pill ' + conf.cls + '" title="' + esc(conf.text) + '">' + statusBadge +
esc(conf.badge) + '</span>' + statusBadge + '</div>' + '</div>' +
'<div class="mag-toc-actions">' + actionsHtml(a, done, progress) + '</div>' + '<div class="mag-toc-actions">' + actionsHtml(a, done, progress) + '</div></li>';
'</div></div>';
} else {
html += '<li class="mag-toc-row' + (done ? ' is-approved' : '') + '">' +
selectHtml(a) +
'<div class="mag-toc-main">' +
titleHtml(a, 'mag-toc-title') +
(a.author ? '<span class="mag-toc-by">by ' + esc(a.author) + '</span>' : '') +
'</div>' +
'<div class="mag-toc-meta">' +
'<span>' + fmtPages(a.pages) + '</span>' +
'<span>' + fmtDuration(a.estimated_seconds) + '</span>' +
'<span class="mag-pill ' + conf.cls + '" title="' + esc(conf.text) + '">' +
esc(conf.badge) + '</span>' +
statusBadge +
'</div>' +
'<div class="mag-toc-actions">' + actionsHtml(a, done, progress) + '</div></li>';
}
}); });
html += grid ? '</div></div>' : '</ul></div>'; html += '</ul></div>';
}); });
host.innerHTML = html; host.innerHTML = html;
renderContentsBulkBar(); renderContentsBulkBar();
magUpdateAudioZipLink(); magUpdateAudioZipLink();
} }
const MAG_CONTENTS_VIEW_CYCLE = ['list', 'grid', 'pages']; const MAG_CONTENTS_VIEW_CYCLE = ['list', 'pages'];
const MAG_PAGES_COLS_KEY = 'ttsvc_mag_pages_cols';
// Every page, 1..pageCount, regardless of whether an article claims it -- // Every page, 1..pageCount, regardless of whether an article claims it --
// the article grid/list above can only ever show what detection already // the article grid/list above can only ever show what detection already
@ -830,7 +819,7 @@ function renderPagesView(host) {
if (!byPage.has(p)) byPage.set(p, []); if (!byPage.has(p)) byPage.set(p, []);
byPage.get(p).push(a); byPage.get(p).push(a);
})); }));
let html = '<div class="mag-grid mag-pages-grid">'; let html = '<div class="mag-pages-grid" style="--cols:' + state.pagesCols + '">';
for (let p = 1; p <= pageCount; p++) { for (let p = 1; p <= pageCount; p++) {
const arts = byPage.get(p) || []; const arts = byPage.get(p) || [];
html += '<div class="mag-card-wrap"><div class="mag-card' + (arts.length ? '' : ' is-uncovered') + '">' + html += '<div class="mag-card-wrap"><div class="mag-card' + (arts.length ? '' : ' is-uncovered') + '">' +
@ -851,6 +840,15 @@ function renderPagesView(host) {
host.innerHTML = html; host.innerHTML = html;
} }
function magSetPagesCols(n) {
state.pagesCols = Math.max(1, Math.min(12, parseInt(n, 10) || 8));
try { localStorage.setItem(MAG_PAGES_COLS_KEY, String(state.pagesCols)); } catch (_) {}
const valueEl = $('mag-cols-value');
if (valueEl) valueEl.textContent = String(state.pagesCols);
const grid = document.querySelector('.mag-pages-grid');
if (grid) grid.style.setProperty('--cols', state.pagesCols);
}
function magToggleContentsView() { function magToggleContentsView() {
const i = MAG_CONTENTS_VIEW_CYCLE.indexOf(state.contentsView); const i = MAG_CONTENTS_VIEW_CYCLE.indexOf(state.contentsView);
state.contentsView = MAG_CONTENTS_VIEW_CYCLE[(i + 1) % MAG_CONTENTS_VIEW_CYCLE.length]; state.contentsView = MAG_CONTENTS_VIEW_CYCLE[(i + 1) % MAG_CONTENTS_VIEW_CYCLE.length];
@ -3559,6 +3557,8 @@ async function magInit() {
try { try {
const savedView = localStorage.getItem(MAG_CONTENTS_VIEW_KEY); const savedView = localStorage.getItem(MAG_CONTENTS_VIEW_KEY);
if (MAG_CONTENTS_VIEW_CYCLE.includes(savedView)) state.contentsView = savedView; if (MAG_CONTENTS_VIEW_CYCLE.includes(savedView)) state.contentsView = savedView;
const savedCols = parseInt(localStorage.getItem(MAG_PAGES_COLS_KEY), 10);
if (savedCols >= 1 && savedCols <= 12) state.pagesCols = savedCols;
} catch (_) {} } catch (_) {}
if (!$('mag-library-list')) return; // section not mounted if (!$('mag-library-list')) return; // section not mounted
magLoadLibrary(); magLoadLibrary();
@ -3594,6 +3594,7 @@ root.magContentsCommitTitle = magContentsCommitTitle;
root.magGenerateAllAudio = magGenerateAllAudio; root.magGenerateAllAudio = magGenerateAllAudio;
root.magTogglePlaylist = magTogglePlaylist; root.magTogglePlaylist = magTogglePlaylist;
root.magReviewPage = magReviewPage; root.magReviewPage = magReviewPage;
root.magSetPagesCols = magSetPagesCols;
root.magPlaylistPlay = magPlaylistPlay; root.magPlaylistPlay = magPlaylistPlay;
root.magContentsToggleArticle = magContentsToggleArticle; root.magContentsToggleArticle = magContentsToggleArticle;
root.magContentsSelectAllVisible = magContentsSelectAllVisible; root.magContentsSelectAllVisible = magContentsSelectAllVisible;

View File

@ -58,9 +58,14 @@
</button> </button>
<h3 id="mag-doc-title">Magazine</h3> <h3 id="mag-doc-title">Magazine</h3>
<div class="mag-bar-actions"> <div class="mag-bar-actions">
<button type="button" class="btn-sm" id="mag-contents-view-toggle" onclick="magToggleContentsView()" title="Switch between a list and a thumbnail grid of articles"> <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 <span class="mdi mdi-view-grid-outline"></span> Thumbnails
</button> </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"> <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 <span class="mdi mdi-checkbox-multiple-marked-outline"></span> Select
</button> </button>

View File

@ -4935,8 +4935,13 @@ input[list="lang-codes"] {
background: color-mix(in srgb, var(--yellow, #d97706) 10%, var(--surface)); } background: color-mix(in srgb, var(--yellow, #d97706) 10%, var(--surface)); }
.mag-coverage-banner .mdi { color: var(--yellow, #d97706); flex-shrink: 0; margin-top: 2px; } .mag-coverage-banner .mdi { color: var(--yellow, #d97706); flex-shrink: 0; margin-top: 2px; }
.mag-coverage-ranges { display: flex; flex-wrap: wrap; gap: 6px; } .mag-coverage-ranges { display: flex; flex-wrap: wrap; gap: 6px; }
.mag-pages-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); } .mag-pages-grid { display: grid; grid-template-columns: repeat(var(--cols, 8), 1fr); gap: 12px; }
.mag-pages-grid .mag-card-thumb { height: 120px; } .mag-pages-grid .mag-card-thumb { height: 120px; }
.mag-cols-control { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--subtext); }
.mag-cols-control input[type="range"] { width: 90px; accent-color: var(--accent); }
#mag-cols-value { min-width: 16px; text-align: right; }
@media (max-width: 900px) { .mag-pages-grid { grid-template-columns: repeat(min(var(--cols, 8), 4), 1fr); } }
@media (max-width: 560px) { .mag-pages-grid { grid-template-columns: repeat(min(var(--cols, 8), 2), 1fr); } }
.mag-card.is-uncovered { border-color: var(--yellow, #d97706); .mag-card.is-uncovered { border-color: var(--yellow, #d97706);
background: color-mix(in srgb, var(--yellow, #d97706) 6%, var(--surface)); } background: color-mix(in srgb, var(--yellow, #d97706) 6%, var(--surface)); }