diff --git a/CHANGELOG.md b/CHANGELOG.md index b5aabd1..2109bf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,20 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.14.3] — 2026-07-06 + +### Added +- **Rich voice-picker dropdown for App Routing's output voice field** — same searchable, avatar-thumbnail dropdown used elsewhere in the app, attached as a browse button next to the field. The field itself stays free-text so it can still target `vd_` Voice Design presets that aren't part of the voice library. +- **Gender, Tag, Rating, and Group filters restored/added to the My Voices table** — the Gender and Rating filtering logic already existed in the code but its ` - +
@@ -214,6 +214,12 @@ function renderRoutingList() {
`).join(''); + // Same searchable avatar dropdown used for voice selection elsewhere in the + // app — the field itself stays a free-text input (routes can also target + // vd_ Voice Design presets, which aren't part of the voice library). + if (typeof VoicePicker !== 'undefined') { + document.querySelectorAll('.route-output').forEach(inp => VoicePicker.attachTextPicker(inp)); + } } function readRoutingForm() { diff --git a/static/js/voice-inspector.js b/static/js/voice-inspector.js index d371344..a0142a0 100644 --- a/static/js/voice-inspector.js +++ b/static/js/voice-inspector.js @@ -98,9 +98,9 @@ function selectVoice(wrap) {
- ${escHtml(voiceId)} - - + ${escHtml(voiceId)} + +
diff --git a/static/js/voice-library.js b/static/js/voice-library.js index 2a41e34..5ea0f80 100644 --- a/static/js/voice-library.js +++ b/static/js/voice-library.js @@ -308,32 +308,45 @@ function populateLibraryFilters() { const langSel = $('library-filter-lang'); const sexSel = $('library-filter-sex'); const typeSel = $('library-filter-type'); + const tagSel = $('library-filter-tag'); + const groupSel = $('library-filter-group'); if (!langSel || !sexSel || !typeSel) return; const langSet = new Set(); const sexSet = new Set(); const typeSet = new Set(); + const tagSet = new Set(); + const groupSet = new Set(); (_voices || []).forEach(v => { const lang = libraryVoiceLang(v); if (lang) langSet.add(lang); if (v.gender) sexSet.add(v.gender); const type = voiceFileType(v); if (type) typeSet.add(type); + String(v.tag || '').split(',').map(t => t.trim()).filter(Boolean).forEach(t => tagSet.add(t)); + const g = (v.group || '').trim(); + if (g) groupSet.add(g); }); const langs = [...langSet].sort((a,b) => libraryLanguageLabel(a).localeCompare(libraryLanguageLabel(b))); const sexOrder = ['F','M','N']; const sexes = [...sexSet] .sort((a, b) => (sexOrder.indexOf(a) < 0 ? 99 : sexOrder.indexOf(a)) - (sexOrder.indexOf(b) < 0 ? 99 : sexOrder.indexOf(b))); const types = [...typeSet].sort(); - const sig = JSON.stringify([langs, sexes, types]); + const tags = [...tagSet].sort((a, b) => a.localeCompare(b)); + const groups = [...groupSet].sort((a, b) => a.localeCompare(b)); + const sig = JSON.stringify([langs, sexes, types, tags, groups]); if (sig === _libraryFilterOptionsSig) return; _libraryFilterOptionsSig = sig; - const keep = {lang: langSel.value, sex: sexSel.value, type: typeSel.value}; + const keep = {lang: langSel.value, sex: sexSel.value, type: typeSel.value, tag: tagSel?.value, group: groupSel?.value}; langSel.innerHTML = '' + langs.map(x => ``).join(''); sexSel.innerHTML = '' + sexes.map(x => ``).join(''); typeSel.innerHTML = '' + types.map(x => ``).join(''); + if (tagSel) tagSel.innerHTML = '' + tags.map(x => ``).join(''); + if (groupSel) groupSel.innerHTML = '' + groups.map(x => ``).join(''); langSel.value = langs.includes(keep.lang) ? keep.lang : ''; sexSel.value = sexes.includes(keep.sex) ? keep.sex : ''; typeSel.value = types.includes(keep.type) ? keep.type : ''; + if (tagSel) tagSel.value = tags.includes(keep.tag) ? keep.tag : ''; + if (groupSel) groupSel.value = groups.includes(keep.group) ? keep.group : ''; } function readLibraryFilters() { @@ -1644,7 +1657,16 @@ $('show-disabled-cb').addEventListener('change', () => { $(id)?.addEventListener('change', () => { readLibraryFilters(); renderVoiceList(); }); }); $('library-filter-text')?.addEventListener('input', debounce(() => { readLibraryFilters(); renderVoiceList(); }, 180)); -$('library-clear-filters')?.addEventListener('click', clearLibraryFilters); +// Tag/group use the same globals the sidebar chips already set (window._voiceTagFilter / +// _voiceGroupFilter), so both entry points stay in sync with whichever was used last. +$('library-filter-tag')?.addEventListener('change', function () { window._voiceTagFilter = this.value || null; renderVoiceList(); }); +$('library-filter-group')?.addEventListener('change', function () { window._voiceGroupFilter = this.value || null; renderVoiceList(); }); +$('library-clear-filters')?.addEventListener('click', () => { + window._voiceTagFilter = null; window._voiceGroupFilter = null; + const tagSel = $('library-filter-tag'), groupSel = $('library-filter-group'); + if (tagSel) tagSel.value = ''; if (groupSel) groupSel.value = ''; + clearLibraryFilters(); +}); $('library-tts-backend-select')?.addEventListener('change', () => { status('Library TTS engine: ' + (backendById(libraryTtsBackend())?.label || libraryTtsBackend())); }); function renderVoiceList() { @@ -3231,6 +3253,7 @@ function renderVoiceGroupsBar() { if (e.target.closest('.vg-del')) return; const g = chip.dataset.group; window._voiceGroupFilter = (window._voiceGroupFilter === g) ? '' : g; + const groupSel = $('library-filter-group'); if (groupSel) groupSel.value = window._voiceGroupFilter || ''; renderVoiceList(); }); }); @@ -3238,7 +3261,9 @@ function renderVoiceGroupsBar() { btn.addEventListener('click', e => { e.stopPropagation(); deleteVoiceGroup(btn.dataset.group); }); }); bar.querySelector('.vg-clear')?.addEventListener('click', () => { - window._voiceGroupFilter = ''; renderVoiceList(); + window._voiceGroupFilter = ''; + const groupSel = $('library-filter-group'); if (groupSel) groupSel.value = ''; + renderVoiceList(); }); } diff --git a/static/js/voice-picker.js b/static/js/voice-picker.js index 7f99fa4..dca33b8 100644 --- a/static/js/voice-picker.js +++ b/static/js/voice-picker.js @@ -266,5 +266,71 @@ _pickers[selectId]?.syncTrigger(); } - window.VoicePicker = { upgrade, populate, getValue, setValue }; + // Same searchable avatar dropdown as upgrade(), but anchored to a plain + // text instead of replacing a
`; + document.body.appendChild(drop); + const searchInp = drop.querySelector('.vp-search'); + const list = drop.querySelector('.vp-list'); + + function _voices() { return (window._voices || []).filter(v => v.enabled !== false); } + + function _renderList(filter) { + const q = (filter || '').toLowerCase(); + const filtered = q ? _voices().filter(v => v.id.toLowerCase().includes(q)) : _voices(); + if (!filtered.length) { list.innerHTML = '
No voices found
'; return; } + list.innerHTML = filtered.map(v => _buildItem(v.id, v.id)).join(''); + list.querySelectorAll('.vp-item').forEach(el => el.classList.toggle('selected', el.dataset.value === input.value)); + } + + function _position() { + const r = input.getBoundingClientRect(); + drop.style.left = r.left + 'px'; + drop.style.top = (r.bottom + 4) + 'px'; + drop.style.width = Math.max(240, r.width + 32) + 'px'; + } + + function _open() { + _position(); + drop.hidden = false; + searchInp.value = ''; + _renderList(''); + searchInp.focus(); + } + function _close() { drop.hidden = true; } + + function _select(value) { + input.value = value; + input.dispatchEvent(new Event('input', { bubbles: true })); + input.dispatchEvent(new Event('change', { bubbles: true })); + _close(); + } + + btn.addEventListener('click', (e) => { e.stopPropagation(); drop.hidden ? _open() : _close(); }); + searchInp.addEventListener('input', () => _renderList(searchInp.value)); + searchInp.addEventListener('keydown', e => { if (e.key === 'Escape') { e.stopPropagation(); _close(); } }); + list.addEventListener('click', e => { const item = e.target.closest('.vp-item'); if (item) _select(item.dataset.value); }); + document.addEventListener('click', e => { if (!drop.contains(e.target) && e.target !== btn) _close(); }, true); + window.addEventListener('resize', () => { if (!drop.hidden) _position(); }); + } + + window.VoicePicker = { upgrade, populate, getValue, setValue, attachTextPicker }; })(); diff --git a/static/sections/s-voices.html b/static/sections/s-voices.html index d60db09..394ec13 100644 --- a/static/sections/s-voices.html +++ b/static/sections/s-voices.html @@ -51,7 +51,20 @@
+ + + + +
@@ -151,9 +164,6 @@