This commit is contained in:
parent
03597d37f0
commit
48acbf6e00
10
CHANGELOG.md
10
CHANGELOG.md
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user