refactor: extract rehearser cast controls
This commit is contained in:
parent
ed3b2d42cd
commit
d613591526
@ -8,6 +8,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- **Rehearser cast-controls extraction.** Cast view switching, sorting, filtering, and their guarded event wiring now
|
||||||
|
live in `rehearser-cast.js`, reducing the main renderer's responsibility while preserving the legacy API.
|
||||||
- **Namespace migration surface.** Extracted Rehearser and Audiobook modules now publish parse, stage, voice-picker,
|
- **Namespace migration surface.** Extracted Rehearser and Audiobook modules now publish parse, stage, voice-picker,
|
||||||
and persistence APIs through `window.TTVC` while retaining legacy globals for the ongoing migration.
|
and persistence APIs through `window.TTVC` while retaining legacy globals for the ongoing migration.
|
||||||
- **CI frontend gate.** The manifest-driven JavaScript syntax and top-level declaration collision check now runs in the
|
- **CI frontend gate.** The manifest-driven JavaScript syntax and top-level declaration collision check now runs in the
|
||||||
|
|||||||
1
TODO.md
1
TODO.md
@ -130,6 +130,7 @@ architecture change: the larger Audiobook/Rehearser/style split.
|
|||||||
a full style/lint policy remains a follow-up.
|
a full style/lint policy remains a follow-up.
|
||||||
- [ ] Extract Rehearser voice matching and voice design.
|
- [ ] Extract Rehearser voice matching and voice design.
|
||||||
- [x] Extract Rehearser voice-picker presentation and preview state into `rehearser-voice-picker.js`.
|
- [x] Extract Rehearser voice-picker presentation and preview state into `rehearser-voice-picker.js`.
|
||||||
|
- [x] Extract Rehearser cast view, sorting, filtering, and toolbar wiring into `rehearser-cast.js`.
|
||||||
- [x] Extract Rehearser library persistence and IndexedDB migration into `rehearser-library.js`.
|
- [x] Extract Rehearser library persistence and IndexedDB migration into `rehearser-library.js`.
|
||||||
- [ ] Extract Audiobook parsing and casting.
|
- [ ] Extract Audiobook parsing and casting.
|
||||||
- [x] Extract Audiobook parsing primitives and speaker-heuristic constants into `audiobook-parse.js`.
|
- [x] Extract Audiobook parsing primitives and speaker-heuristic constants into `audiobook-parse.js`.
|
||||||
|
|||||||
@ -33,6 +33,7 @@ Done incrementally as global-scope ordered files (no ES-module change needed)
|
|||||||
- ⬜ `rehearser-cast.js` — `renderCastList`, sort/filter, online voice picker, character research.
|
- ⬜ `rehearser-cast.js` — `renderCastList`, sort/filter, online voice picker, character research.
|
||||||
- ✅ `rehearser-stage.js` — stage font/cast sidebar controls and cast-strip rendering; playback and transport remain in `rehearser.js`.
|
- ✅ `rehearser-stage.js` — stage font/cast sidebar controls and cast-strip rendering; playback and transport remain in `rehearser.js`.
|
||||||
- ✅ `rehearser-voice-picker.js` — voice preview state and matched-choice presentation; import and assignment actions remain in `rehearser.js`.
|
- ✅ `rehearser-voice-picker.js` — voice preview state and matched-choice presentation; import and assignment actions remain in `rehearser.js`.
|
||||||
|
- ✅ `rehearser-cast.js` — cast view, sort/filter state handlers, and cast toolbar wiring; card rendering remains in `rehearser.js`.
|
||||||
- ✅ `audiobook-parse.js` — dialogue detection, dehyphenation, and speaker-heuristic constants; the casting pipeline remains in `audiobook.js`.
|
- ✅ `audiobook-parse.js` — dialogue detection, dehyphenation, and speaker-heuristic constants; the casting pipeline remains in `audiobook.js`.
|
||||||
- ⬜ `rehearser-train.js` — mic record / STT compare.
|
- ⬜ `rehearser-train.js` — mic record / STT compare.
|
||||||
- ✅ `rehearser-library.js` — SQLite persistence helpers and IndexedDB migration; import/export and auto-save remain
|
- ✅ `rehearser-library.js` — SQLite persistence helpers and IndexedDB migration; import/export and auto-save remain
|
||||||
|
|||||||
@ -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.24.30">
|
<meta name="app-version" content="1.24.31">
|
||||||
<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,7 +27,7 @@
|
|||||||
|
|
||||||
<!-- ── 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.24.30">
|
<link rel="stylesheet" href="/static/style.css?v=1.24.31">
|
||||||
|
|
||||||
|
|
||||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||||
@ -392,7 +392,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.24.30"></script>
|
<script src="/static/loader.js?v=1.24.31"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
66
static/js/rehearser-cast.js
Normal file
66
static/js/rehearser-cast.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// ── Rehearser cast view controls ────────────────────────────────────────────
|
||||||
|
// The cast renderer remains in rehearser.js; these handlers only resolve state
|
||||||
|
// when the cast panel is rendered, making them safe to load as a leaf module.
|
||||||
|
|
||||||
|
function applyCastView() {
|
||||||
|
const list = $('reh-cast-list'); if (!list) return;
|
||||||
|
const view = rehState.castView === 'list' ? 'list' : 'card';
|
||||||
|
list.classList.toggle('reh-cast-view-card', view === 'card');
|
||||||
|
list.classList.toggle('reh-cast-view-list', view === 'list');
|
||||||
|
document.querySelectorAll('#reh-cast-view-toggle .reh-view-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view));
|
||||||
|
}
|
||||||
|
|
||||||
|
function _wireCastControls() {
|
||||||
|
const toggle = $('reh-cast-view-toggle');
|
||||||
|
if (!toggle || toggle._wired) return;
|
||||||
|
toggle._wired = true;
|
||||||
|
toggle.querySelectorAll('.reh-view-btn').forEach(b => b.addEventListener('click', () => {
|
||||||
|
rehState.castView = b.dataset.view;
|
||||||
|
try { localStorage.setItem('reh-cast-view', b.dataset.view); } catch (_) {}
|
||||||
|
applyCastView();
|
||||||
|
}));
|
||||||
|
const search = $('reh-cast-search'), fg = $('reh-cast-filter-gender'), fl = $('reh-cast-filter-lang');
|
||||||
|
const sortSel = $('reh-cast-sort'), dirBtn = $('reh-cast-sort-dir');
|
||||||
|
const setDirIcon = () => { if (dirBtn) { dirBtn.dataset.dir = rehState.castSort.dir; dirBtn.querySelector('.mdi').className = 'mdi mdi-sort-' + (rehState.castSort.dir === 'desc' ? 'descending' : 'ascending'); } };
|
||||||
|
const persist = () => { try { localStorage.setItem('reh-cast-sort', JSON.stringify(rehState.castSort)); } catch (_) {} };
|
||||||
|
if (sortSel) sortSel.value = rehState.castSort.by;
|
||||||
|
setDirIcon();
|
||||||
|
search?.addEventListener('input', () => { rehState.castFilter.search = search.value.trim(); renderCastList(); search.focus(); });
|
||||||
|
fg?.addEventListener('change', () => { rehState.castFilter.gender = fg.value; renderCastList(); });
|
||||||
|
fl?.addEventListener('change', () => { rehState.castFilter.lang = fl.value; renderCastList(); });
|
||||||
|
sortSel?.addEventListener('change', () => {
|
||||||
|
rehState.castSort.by = sortSel.value;
|
||||||
|
rehState.castSort.dir = sortSel.value === 'lines' ? 'desc' : 'asc';
|
||||||
|
setDirIcon(); persist(); renderCastList();
|
||||||
|
});
|
||||||
|
dirBtn?.addEventListener('click', () => {
|
||||||
|
rehState.castSort.dir = rehState.castSort.dir === 'desc' ? 'asc' : 'desc';
|
||||||
|
setDirIcon(); persist(); renderCastList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _castSortFilter(speakers, lineCount) {
|
||||||
|
const f = rehState.castFilter || {}, s = rehState.castSort || { by: 'name', dir: 'asc' };
|
||||||
|
const out = speakers.filter(sp => {
|
||||||
|
const c = rehState.cast[sp] || {};
|
||||||
|
if (f.search) { const q = f.search.toLowerCase(); if (!sp.toLowerCase().includes(q) && !(c.tags || '').toLowerCase().includes(q)) return false; }
|
||||||
|
if (f.gender && (c.gender || '') !== f.gender) return false;
|
||||||
|
if (f.lang && (c.lang || '') !== f.lang) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
const byName = (a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' });
|
||||||
|
const cmp = {
|
||||||
|
name: byName,
|
||||||
|
gender: (a, b) => (rehState.cast[a].gender || '~').localeCompare(rehState.cast[b].gender || '~') || byName(a, b),
|
||||||
|
lang: (a, b) => (rehState.cast[a].lang || '~').localeCompare(rehState.cast[b].lang || '~') || byName(a, b),
|
||||||
|
lines: (a, b) => (lineCount(a) - lineCount(b)) || byName(a, b),
|
||||||
|
tag: (a, b) => (rehState.cast[a].tags || '~').localeCompare(rehState.cast[b].tags || '~') || byName(a, b),
|
||||||
|
}[s.by] || byName;
|
||||||
|
out.sort(cmp);
|
||||||
|
if (s.dir === 'desc') out.reverse();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TTVC = window.TTVC || {};
|
||||||
|
window.TTVC.rehearser = window.TTVC.rehearser || {};
|
||||||
|
window.TTVC.rehearser.cast = { applyCastView, filterAndSort: _castSortFilter };
|
||||||
@ -1508,14 +1508,6 @@ function renderCastList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cast card / list view toggle
|
|
||||||
function applyCastView() {
|
|
||||||
const list = $('reh-cast-list'); if (!list) return;
|
|
||||||
const view = rehState.castView === 'list' ? 'list' : 'card';
|
|
||||||
list.classList.toggle('reh-cast-view-card', view === 'card');
|
|
||||||
list.classList.toggle('reh-cast-view-list', view === 'list');
|
|
||||||
document.querySelectorAll('#reh-cast-view-toggle .reh-view-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view));
|
|
||||||
}
|
|
||||||
try { rehState.castView = localStorage.getItem('reh-cast-view') || 'card'; } catch (_) { rehState.castView = 'card'; }
|
try { rehState.castView = localStorage.getItem('reh-cast-view') || 'card'; } catch (_) { rehState.castView = 'card'; }
|
||||||
|
|
||||||
// Cast sort + filter state (narrator excluded — it's pinned on top by renderCastList)
|
// Cast sort + filter state (narrator excluded — it's pinned on top by renderCastList)
|
||||||
@ -1523,59 +1515,6 @@ rehState.castFilter = { search: '', gender: '', lang: '' };
|
|||||||
try { rehState.castSort = JSON.parse(localStorage.getItem('reh-cast-sort')) || { by: 'name', dir: 'asc' }; }
|
try { rehState.castSort = JSON.parse(localStorage.getItem('reh-cast-sort')) || { by: 'name', dir: 'asc' }; }
|
||||||
catch (_) { rehState.castSort = { by: 'name', dir: 'asc' }; }
|
catch (_) { rehState.castSort = { by: 'name', dir: 'asc' }; }
|
||||||
|
|
||||||
// Wire the cast toolbar (view toggle + sort/filter). Called from renderCastList so it
|
|
||||||
// always binds once the section is mounted; guarded to attach only once.
|
|
||||||
function _wireCastControls() {
|
|
||||||
const toggle = $('reh-cast-view-toggle');
|
|
||||||
if (!toggle || toggle._wired) return;
|
|
||||||
toggle._wired = true;
|
|
||||||
toggle.querySelectorAll('.reh-view-btn').forEach(b => b.addEventListener('click', () => {
|
|
||||||
rehState.castView = b.dataset.view;
|
|
||||||
try { localStorage.setItem('reh-cast-view', b.dataset.view); } catch (_) {}
|
|
||||||
applyCastView();
|
|
||||||
}));
|
|
||||||
const search = $('reh-cast-search'), fg = $('reh-cast-filter-gender'), fl = $('reh-cast-filter-lang');
|
|
||||||
const sortSel = $('reh-cast-sort'), dirBtn = $('reh-cast-sort-dir');
|
|
||||||
const setDirIcon = () => { if (dirBtn) { dirBtn.dataset.dir = rehState.castSort.dir; dirBtn.querySelector('.mdi').className = 'mdi mdi-sort-' + (rehState.castSort.dir === 'desc' ? 'descending' : 'ascending'); } };
|
|
||||||
const persist = () => { try { localStorage.setItem('reh-cast-sort', JSON.stringify(rehState.castSort)); } catch (_) {} };
|
|
||||||
if (sortSel) sortSel.value = rehState.castSort.by;
|
|
||||||
setDirIcon();
|
|
||||||
search?.addEventListener('input', () => { rehState.castFilter.search = search.value.trim(); renderCastList(); search.focus(); });
|
|
||||||
fg?.addEventListener('change', () => { rehState.castFilter.gender = fg.value; renderCastList(); });
|
|
||||||
fl?.addEventListener('change', () => { rehState.castFilter.lang = fl.value; renderCastList(); });
|
|
||||||
sortSel?.addEventListener('change', () => {
|
|
||||||
rehState.castSort.by = sortSel.value;
|
|
||||||
rehState.castSort.dir = sortSel.value === 'lines' ? 'desc' : 'asc'; // lines → most first
|
|
||||||
setDirIcon(); persist(); renderCastList();
|
|
||||||
});
|
|
||||||
dirBtn?.addEventListener('click', () => {
|
|
||||||
rehState.castSort.dir = rehState.castSort.dir === 'desc' ? 'asc' : 'desc';
|
|
||||||
setDirIcon(); persist(); renderCastList();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function _castSortFilter(speakers, lineCount) {
|
|
||||||
const f = rehState.castFilter || {}, s = rehState.castSort || { by: 'name', dir: 'asc' };
|
|
||||||
const out = speakers.filter(sp => {
|
|
||||||
const c = rehState.cast[sp] || {};
|
|
||||||
if (f.search) { const q = f.search.toLowerCase(); if (!sp.toLowerCase().includes(q) && !(c.tags || '').toLowerCase().includes(q)) return false; }
|
|
||||||
if (f.gender && (c.gender || '') !== f.gender) return false;
|
|
||||||
if (f.lang && (c.lang || '') !== f.lang) return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
const byName = (a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' });
|
|
||||||
const cmp = {
|
|
||||||
name: byName,
|
|
||||||
gender: (a, b) => (rehState.cast[a].gender || '~').localeCompare(rehState.cast[b].gender || '~') || byName(a, b),
|
|
||||||
lang: (a, b) => (rehState.cast[a].lang || '~').localeCompare(rehState.cast[b].lang || '~') || byName(a, b),
|
|
||||||
lines: (a, b) => (lineCount(a) - lineCount(b)) || byName(a, b),
|
|
||||||
tag: (a, b) => (rehState.cast[a].tags || '~').localeCompare(rehState.cast[b].tags || '~') || byName(a, b),
|
|
||||||
}[s.by] || byName;
|
|
||||||
out.sort(cmp);
|
|
||||||
if (s.dir === 'desc') out.reverse();
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove a character and all of their dialog lines (with index-state remap)
|
// Remove a character and all of their dialog lines (with index-state remap)
|
||||||
function _castDeleteCharacter(sp) {
|
function _castDeleteCharacter(sp) {
|
||||||
const n = rehState.lines.filter(l => l.speaker === sp && l.type === 'dialog').length;
|
const n = rehState.lines.filter(l => l.speaker === sp && l.type === 'dialog').length;
|
||||||
|
|||||||
@ -4,9 +4,9 @@
|
|||||||
"integrations", "routing", "voice-clone", "voice-library", "tts-preview", "generation", "benchmark", "stt", "namespace"
|
"integrations", "routing", "voice-clone", "voice-library", "tts-preview", "generation", "benchmark", "stt", "namespace"
|
||||||
],
|
],
|
||||||
"lazy": {
|
"lazy": {
|
||||||
"s-rehearser": ["rehearser-parse", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker"],
|
"s-rehearser": ["rehearser-parse", "rehearser-cast", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker"],
|
||||||
"s-reader": ["rehearser-parse", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook", "character-sheets"],
|
"s-reader": ["rehearser-parse", "rehearser-cast", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook", "character-sheets"],
|
||||||
"s-library": ["rehearser-parse", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern"],
|
"s-library": ["rehearser-parse", "rehearser-cast", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern"],
|
||||||
"s-caststudio": ["rehearser-parse", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern", "studio"]
|
"s-caststudio": ["rehearser-parse", "rehearser-cast", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern", "studio"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user