diff --git a/CHANGELOG.md b/CHANGELOG.md index b0c7e5a..76d684e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi changing those fields causes the existing pair to be flagged stale. - **Character Table Pagination.** Productions now render 50 character cards/rows at a time with an explicit Show more control, keeping sorting and record lookups intact while reducing DOM and image work for large casts. +- **Voice Table Pagination.** The voice library now renders up to 50 sorted rows initially and adds a Show more + control for larger result sets, while retaining the full dataset for filtering and sorting. - **Upload Filename Boundaries.** Audio, STT, route-sound and picture uploads now reject path-like or control-character filenames before deriving extensions; voice ZIP imports also reject absolute, traversal and malformed archive paths. - **Security Regression Coverage.** Added a Python test gate for character metadata validation, private-download URL diff --git a/TODO.md b/TODO.md index a67e214..827c2d4 100644 --- a/TODO.md +++ b/TODO.md @@ -53,8 +53,8 @@ the first block before investing in architecture or polish. cross-record validation so mismatched profile/reference files are detected even when both are current-version. - [ ] **M — Add bounded duplicate-generation protection.** Consistent portrait/reference jobs now deduplicate repeated requests with `Idempotency-Key`; extend the same bounded contract to voice and casting jobs. -- [ ] **M — Improve large-table performance.** Character productions now paginate at 50 visible rows; paginate voice - tables, update only affected rows, and disable preloading on low-memory devices. +- [ ] **M — Improve large-table performance.** Character productions and the flat voice library now paginate at 50 + visible rows; updating only affected rows and disabling preloading on low-memory devices remain. - [ ] **M — Add explicit cache invalidation and eviction.** Fish.audio search responses now have bounded eviction; add equivalent LRU cleanup for audio/image entries and object URLs. - [ ] **M — Add constrained-device validation.** Test with Chrome CPU/memory throttling, diff --git a/static/js/voice-library.js b/static/js/voice-library.js index f6df84b..2b09016 100644 --- a/static/js/voice-library.js +++ b/static/js/voice-library.js @@ -1816,6 +1816,22 @@ function renderVoiceList() { return 0; }); + // Keep the complete filtered/sorted list in memory, but avoid creating a + // large DOM (and hundreds of audio/image nodes) for big libraries. + const pageSignature = JSON.stringify([ + cat, showDisabled, window._voiceGroupFilter || '', window._voiceTagFilter || '', + _libraryIssueFilter || '', _sortField, _sortDir, !!window._voiceTableView, _libraryFilters, + ]); + if (window._voicePageSignature !== pageSignature) { + window._voicePageSignature = pageSignature; + window._voicePage = 1; + } + const totalFiltered = filtered.length; + const pageSize = 50; + const page = Math.max(1, Number(window._voicePage) || 1); + const pageEnd = Math.min(totalFiltered, page * pageSize); + const pageItems = filtered.slice(0, pageEnd); + updateLibraryInsights(); if (_libraryIssueFilter) { const note = document.createElement('div'); @@ -1955,8 +1971,20 @@ function renderVoiceList() { if (window._voiceGroupByTag) renderVoicesByTag(list, filtered); else { const frag = document.createDocumentFragment(); - filtered.forEach(v => frag.appendChild(makeVoiceRow(v))); + pageItems.forEach(v => frag.appendChild(makeVoiceRow(v))); list.appendChild(frag); + if (pageEnd < totalFiltered) { + const more = document.createElement('button'); + more.type = 'button'; + more.className = 'btn-secondary vl-load-more'; + more.style.margin = '12px 0'; + more.textContent = `Show more voices (${pageEnd} / ${totalFiltered})`; + more.addEventListener('click', () => { + window._voicePage = page + 1; + renderVoiceList(); + }); + list.appendChild(more); + } } if (_pendingSelectId) { const wrap = list.querySelector(`[data-id="${CSS.escape(_pendingSelectId)}"]`);