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

This commit is contained in:
mARTin-B78 2026-09-22 20:54:01 +02:00
parent 03597d37f0
commit 48acbf6e00
5 changed files with 173 additions and 33 deletions

View File

@ -9,6 +9,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
---
## [1.44.0] — 2026-09-22
### Added
- **Magazine: Draw region stays armed after adding a box.** Previously every box meant clicking the toolbar button again; it's now a toggle (with an on/off switch icon on the button itself, since the background colour alone was easy to miss after a few boxes in a row) that stays active until you turn it off.
- **Magazine: a freshly drawn region is ticked in automatically** when its type is one that's actually narrated (Paragraph, Headline, Subheading, Standfirst, Author) — drawing a region already means it belongs in the article. Furniture-like kinds (Page number, Source/footer, Advertisement, Table, Duplicate, OCR noise, Caption, Pull quote, Sidenote, Text in picture) stay unticked, since those are excluded from narration on purpose.
- **Magazine: the region type list is now searchable, with an icon per type.** Filter any of the 16 kinds by typing instead of scrolling a plain dropdown.
## [1.43.1] — 2026-09-22
### Fixed
@ -4321,7 +4328,8 @@ Initial feature-complete release.
---
[Unreleased]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.43.1...HEAD
[Unreleased]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.44.0...HEAD
[1.44.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.43.1...v1.44.0
[1.43.1]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.43.0...v1.43.1
[1.26.10]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.26.9...v1.26.10
[1.26.9]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.26.8...v1.26.9

View File

@ -1 +1 @@
1.43.1
1.44.0

View File

@ -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.43.1">
<meta name="app-version" content="1.44.0">
<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.43.1">
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.43.1">
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.43.1">
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.43.1">
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.43.1">
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.43.1">
<link rel="stylesheet" href="/static/style.css?v=1.44.0">
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.44.0">
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.44.0">
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.44.0">
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.44.0">
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.44.0">
<!-- ── 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.43.1"></script>
<script src="/static/loader.js?v=1.44.0"></script>
</body>
</html>

View File

@ -919,12 +919,26 @@ function magReviewGoMagazinePage(page) {
// read" without reading the label. No table detection exists yet (P2), so a
// table currently reads as its own text region like any other paragraph.
// What a region can be changed into from the review list. Values match the server's REGION_TYPES.
// Mirrors core/magazine.py's NARRATABLE -- the kinds that are actually read
// aloud. A freshly drawn region of one of these is ticked in automatically
// (see magReviewSubmitDraw); everything else (page number, source, ad,
// table, duplicate, OCR noise, caption, pull quote, sidenote) is furniture
// or reference material excluded from narration on purpose and stays unticked.
const MAG_NARRATABLE_KINDS = new Set(['title', 'heading', 'deck', 'byline', 'body']);
const MAG_REGION_TYPES = [
['body', 'Paragraph'], ['title', 'Headline'], ['heading', 'Subheading'], ['deck', 'Standfirst'],
['byline', 'Author'], ['caption', 'Caption'], ['pullquote', 'Pull quote'], ['sidebar', 'Sidenote'],
['page_number', 'Page number'], ['source', 'Source / footer'], ['ad', 'Advertisement'],
['in_picture', 'Text in picture'], ['ocr_noise', 'OCR noise'], ['duplicate', 'Duplicate'], ['table', 'Table'], ['image', 'Image'],
];
const MAG_REGION_TYPE_ICONS = {
body: 'mdi-text-box-outline', title: 'mdi-format-title', heading: 'mdi-format-header-2',
deck: 'mdi-text-short', byline: 'mdi-account-edit-outline', caption: 'mdi-image-text',
pullquote: 'mdi-format-quote-close', sidebar: 'mdi-note-text-outline', page_number: 'mdi-numeric',
source: 'mdi-book-open-page-variant-outline', ad: 'mdi-advertisements-outline',
in_picture: 'mdi-text-recognition', ocr_noise: 'mdi-alert-circle-outline',
duplicate: 'mdi-content-duplicate', table: 'mdi-table', image: 'mdi-image-outline',
};
// Furniture kinds that the picker shows under their own name (everything else is a page number).
const MAG_FURNITURE_KINDS = ['source', 'in_picture', 'ocr_noise', 'duplicate'];
@ -1172,8 +1186,9 @@ function renderReview() {
'onclick="magReviewToggleSpread()"><span class="mdi mdi-book-open-page-variant" aria-hidden="true"></span> ' +
(state.reviewSpread ? 'Single page' : 'Two-page spread') + '</button></div>' +
'<div class="mag-review-draw-controls" role="group" aria-label="Draw regions">' +
'<button type="button" class="btn-sm" onclick="magReviewToggleDraw()" id="mag-draw-toggle">' +
'<span class="mdi mdi-vector-square-edit" aria-hidden="true"></span> Draw region</button>' +
'<button type="button" class="btn-sm" onclick="magReviewToggleDraw()" id="mag-draw-toggle" aria-pressed="false">' +
'<span class="mdi mdi-vector-square-edit" aria-hidden="true"></span> Draw region' +
'<span class="mdi mdi-toggle-switch-off-outline mag-draw-toggle-switch" id="mag-draw-toggle-switch" aria-hidden="true"></span></button>' +
'<button type="button" class="btn-sm" id="mag-edit-toggle" aria-pressed="false" ' +
'title="Drag a region to move or resize it, or delete one that was detected wrongly" ' +
'onclick="magReviewToggleEdit()"><span class="mdi mdi-vector-polygon" aria-hidden="true"></span> Edit boxes</button>' +
@ -1675,19 +1690,36 @@ function magReviewHighlightBox(blockId) {
// cover that). Percent-of-image coordinates while dragging, converted to
// PDF-point page coordinates (matching every other bbox in the layout) only
// once the user commits the region.
function magReviewToggleDraw(kind = 'text') {
state.drawMode = state.drawMode && state.drawKind === kind ? false : true;
state.drawKind = kind;
if (state.drawMode && state.editMode) magReviewToggleEdit();
// Applies state.drawMode/drawKind to the DOM -- pulled out of
// magReviewToggleDraw so magReviewSubmitDraw can re-assert the same "still
// drawing" visuals after renderReview() rebuilds the toolbar from scratch
// (which has no memory of drawMode, since that class was only ever applied
// imperatively, never baked into the template).
function _magSyncDrawUi() {
const hint = $('mag-draw-hint');
const field = kind === 'title' ? 'title' : kind === 'author' ? 'author' : '';
const field = state.drawKind === 'title' ? 'title' : state.drawKind === 'author' ? 'author' : '';
if (hint) hint.textContent = state.drawMode
? (field ? ` Drag around the ${field} text to OCR it into this field. Press Escape to cancel.`
: ' Drag around the region, then say what it is. Press Escape to cancel.') : '';
const wrap = $('mag-review-page-wrap');
if (wrap) wrap.classList.toggle('is-drawing', state.drawMode);
const btn = $('mag-draw-toggle');
if (btn) { btn.classList.toggle('active', state.drawMode && kind === 'text'); btn.setAttribute('aria-pressed', String(state.drawMode && kind === 'text')); }
if (btn) { btn.classList.toggle('active', state.drawMode); btn.setAttribute('aria-pressed', String(state.drawMode)); }
// A background-colour change alone was easy to miss once several regions
// in a row had been added -- confirmed live. An explicit on/off switch
// icon is a second, unambiguous cue for the same state.
const switchIcon = $('mag-draw-toggle-switch');
if (switchIcon) {
switchIcon.classList.toggle('mdi-toggle-switch', state.drawMode);
switchIcon.classList.toggle('mdi-toggle-switch-off-outline', !state.drawMode);
}
}
function magReviewToggleDraw(kind = 'text') {
state.drawMode = state.drawMode && state.drawKind === kind ? false : true;
state.drawKind = kind;
if (state.drawMode && state.editMode) magReviewToggleEdit();
_magSyncDrawUi();
magReviewCancelDrawPanel();
}
@ -2163,9 +2195,13 @@ function magReviewOpenDrawPanel(box) {
'<button type="button" class="btn-sm primary" onclick="magReviewSubmitDraw()"><span class="mdi mdi-text-recognition"></span> Use recognised text</button></div>'
:
'<div class="mag-draw-panel-row">' +
'<label>Region type <select id="mag-draw-kind">' +
drawKindOptions.map(([v, label]) => '<option value="' + v + '">' + label + '</option>').join('') +
'</select></label>' +
'<div class="mag-type-picker" id="mag-draw-kind-picker">' +
'<label for="mag-draw-kind-search">Region type</label>' +
'<input type="text" id="mag-draw-kind-search" class="mag-type-picker-input" autocomplete="off" ' +
'placeholder="Search region type…" aria-expanded="false" role="combobox" aria-autocomplete="list">' +
'<input type="hidden" id="mag-draw-kind" value="body">' +
'<ul class="mag-type-picker-list" id="mag-draw-kind-list" role="listbox" hidden></ul>' +
'</div>' +
'</div>' +
'<label id="mag-draw-exclude" hidden><input id="mag-draw-exclude-check" type="checkbox" checked> Exclude enclosed text from narration</label>' +
'<textarea id="mag-draw-text" rows="2" placeholder="Type the text, or leave blank and use OCR"></textarea>' +
@ -2178,15 +2214,78 @@ wrap.appendChild(panel);
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) + '%';
const kindSelect = $('mag-draw-kind');
if (kindSelect) {
kindSelect.addEventListener('change', () => {
const isImage = kindSelect.value === 'image';
const ta = $('mag-draw-text');
if (ta) ta.hidden = isImage;
$('mag-draw-exclude').hidden = !isImage;
});
}
if ($('mag-draw-kind-picker')) _magSetupDrawKindPicker(drawKindOptions);
}
// A 16-option <select> for something picked constantly deserved faster
// access than scrolling a native dropdown -- type to filter, arrow keys or
// click to choose, icon per type so the list scans visually instead of by
// re-reading every label. #mag-draw-kind stays a plain hidden input so
// magReviewSubmitDraw's existing $('mag-draw-kind').value read needed no
// changes.
function _magDrawKindOnPick(value) {
const isImage = value === 'image';
const ta = $('mag-draw-text');
if (ta) ta.hidden = isImage;
const exclude = $('mag-draw-exclude');
if (exclude) exclude.hidden = !isImage;
}
function _magSetupDrawKindPicker(options) {
const search = $('mag-draw-kind-search');
const hidden = $('mag-draw-kind');
const list = $('mag-draw-kind-list');
if (!search || !hidden || !list) return;
const renderList = (query) => {
const q = (query || '').trim().toLowerCase();
const matches = q ? options.filter(([, label]) => label.toLowerCase().includes(q)) : options;
list.innerHTML = matches.length
? matches.map(([v, label]) => '<li role="option" data-value="' + v + '" ' +
'class="mag-type-picker-option' + (v === hidden.value ? ' is-selected' : '') + '">' +
'<span class="mdi ' + (MAG_REGION_TYPE_ICONS[v] || 'mdi-text-box-outline') + '" aria-hidden="true"></span> ' +
esc(label) + '</li>').join('')
: '<li class="mag-type-picker-empty muted">No matching region type</li>';
};
const pick = (value, label) => {
hidden.value = value;
search.value = label;
list.hidden = true;
search.setAttribute('aria-expanded', 'false');
_magDrawKindOnPick(value);
};
const open = () => {
renderList(search.value === (options.find(([v]) => v === hidden.value) || [])[1] ? '' : search.value);
list.hidden = false;
search.setAttribute('aria-expanded', 'true');
};
// Start on the default (Paragraph) with its label showing, matching the
// hidden input's own default value set in the HTML above.
pick(hidden.value, (options.find(([v]) => v === hidden.value) || [])[1] || '');
search.addEventListener('focus', open);
search.addEventListener('input', () => { list.hidden = false; search.setAttribute('aria-expanded', 'true'); renderList(search.value); });
search.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { list.hidden = true; search.setAttribute('aria-expanded', 'false'); search.blur(); }
else if (e.key === 'Enter') {
e.preventDefault();
const first = list.querySelector('.mag-type-picker-option');
if (first) { const v = first.dataset.value; pick(v, options.find(([ov]) => ov === v)[1]); }
} else if (e.key === 'ArrowDown' && list.hidden) { open(); }
});
list.addEventListener('mousedown', (e) => {
// mousedown (not click) fires before the input's blur, so the list is
// still in the DOM to read from when the handler below reacts to blur.
const li = e.target.closest('.mag-type-picker-option');
if (!li) return;
e.preventDefault();
pick(li.dataset.value, options.find(([v]) => v === li.dataset.value)[1]);
});
search.addEventListener('blur', () => {
setTimeout(() => { list.hidden = true; search.setAttribute('aria-expanded', 'false'); }, 0);
});
}
async function magReviewSubmitDraw() {
@ -2211,9 +2310,9 @@ async function magReviewSubmitDraw() {
article.author = $('mag-rev-author').value;
if (!_magReviewChecked) _magReviewChecked = new Set(article.blocks || []);
magReviewCancelDrawPanel();
state.drawMode = false;
if (field) {
magReviewCancelDrawPanel();
state.drawMode = false;
_magSyncDrawUi();
say(`Reading selected ${field}…`, '');
try {
const result = await api('/api/magazine/' + state.doc.id + '/pages/' + state.reviewPage + '/extract', {
@ -2265,8 +2364,20 @@ async function magReviewSubmitDraw() {
x + w <= bbox[0] + bbox[2] && y + h <= bbox[1] + bbox[3]) _magReviewChecked.delete(b.id);
}
}
} else if (kind === 'text' || kind === 'heading') added.forEach(r => _magReviewChecked.add(r.id));
} else if (MAG_NARRATABLE_KINDS.has(kind)) {
// Drawing a region at all means the reader judged it part of the
// article -- it should read as included, not force a second click per
// box to tick what was just drawn. Furniture-like kinds (page number,
// source, advertisement, table, duplicate, OCR noise) stay unchecked,
// since those are excluded from narration on purpose.
added.forEach(r => _magReviewChecked.add(r.id));
}
renderReview();
// Draw region is a toggle, not a one-shot: stay armed for the next box
// instead of forcing another click on the toolbar button every time.
// renderReview() just rebuilt the toolbar from scratch with no memory of
// drawMode, so the "still drawing" visuals need reapplying here.
if (state.drawMode) _magSyncDrawUi();
}
// Keep the review's unsaved selection in step with a region change made on the server. Without this,

View File

@ -4969,6 +4969,8 @@ input[list="lang-codes"] {
.mag-review-draw-controls { display: flex; gap: 6px; }
.mag-review-draw-controls .btn-sm { min-height: 34px; }
.mag-review-draw-controls .active { background: var(--accent); color: var(--on-accent); }
.mag-draw-toggle-switch { margin-left: 6px; font-size: 17px; vertical-align: -3px; }
.mag-review-draw-controls .active .mag-draw-toggle-switch { color: var(--green, #16a34a); }
#mag-draw-hint:not(:empty) { padding: 8px 12px; margin-bottom: 12px; border-left: 3px solid var(--accent); }
.mag-review-pagenav-spacer { flex: 1; min-width: 0; }
@ -5019,6 +5021,25 @@ input[list="lang-codes"] {
padding: 4px 6px; border: 1px solid var(--border); border-radius: 4px; background: var(--surface);
color: var(--text); }
/* Searchable region-type combobox in the draw panel — a 16-option native
<select> is slow to scan and slow to reach by scrolling; this filters by
typing and shows an icon per type instead. */
.mag-type-picker { position: relative; display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; }
.mag-type-picker label { flex-shrink: 0; }
.mag-type-picker-input { flex: 1; min-width: 0; font: inherit; font-size: 12px; padding: 4px 6px;
border: 1px solid var(--border); border-radius: 4px; background: var(--surface); color: var(--text); }
.mag-type-picker-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px rgba(37,99,235,.18); }
.mag-type-picker-list { position: absolute; top: 100%; left: 0; right: 0; z-index: 5; margin: 2px 0 0;
padding: 4px; list-style: none; max-height: 240px; overflow-y: auto; background: var(--surface);
border: 1px solid var(--border); border-radius: var(--radius); box-shadow: 0 6px 18px rgba(0,0,0,.25); }
.mag-type-picker-option { display: flex; align-items: center; gap: 8px; padding: 6px 8px; font-size: 13px;
border-radius: 5px; cursor: pointer; color: var(--text); }
.mag-type-picker-option .mdi { color: var(--subtext); font-size: 16px; flex-shrink: 0; }
.mag-type-picker-option:hover { background: var(--panel); }
.mag-type-picker-option.is-selected { background: var(--accent); color: var(--on-accent); }
.mag-type-picker-option.is-selected .mdi { color: inherit; }
.mag-type-picker-empty { padding: 6px 8px; font-size: 13px; }
/* Editing an already-detected region: move by dragging the box body, resize
from its corner handle, delete with its ×. Handles and the delete button
only render usably in edit mode -- kept in the DOM at all times (rather