tts-voice-creator-clone-and.../static/nav.js
mARTin-B78 41f747a7b0 fix: icon-rail sidebar now shows per-item label tooltips instead of full flyout
Hovering the collapsed 56 px rail no longer expands the whole sidebar,
which was pushing the main content left and right. Each nav item now
shows a small floating tooltip (JS-positioned fixed div) next to its
icon on hover — layout stays completely stable.

Bumps to v1.12.1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 16:00:56 +02:00

445 lines
20 KiB
JavaScript

(function () {
'use strict';
const TAB_SECTION_MAP = {
library: 's-voices',
source: 's-clone',
save: 's-clone',
design: 's-design',
custom: 's-design',
getvoices: 's-studio',
generation: 's-tryout',
'stt-tts': 's-tryout',
performance: 's-performance',
routing: 's-routing',
integrations: 's-connect',
howto: 's-connect',
settings: 's-settings',
llms: 's-llms'
};
const SECTIONS = ['s-voices', 's-clone', 's-design', 's-studio', 's-tryout', 's-rehearser', 's-reader', 's-performance', 's-routing', 's-connect', 's-settings', 's-llms', 's-conversation', 's-library'];
function storedSection() {
try { return localStorage.getItem('ttsvc_section') || ''; } catch (_) { return ''; }
}
function setStoredSection(sectionId) {
try { localStorage.setItem('ttsvc_section', sectionId); } catch (_) {}
}
function hashSection() {
var raw = String(location.hash || '').replace(/^#/, '').trim();
if (!raw) return '';
if (raw.indexOf('section=') === 0) raw = raw.slice(8);
raw = decodeURIComponent(raw).replace(/^\/?/, '');
return SECTIONS.includes(raw) ? raw : '';
}
function setSectionHash(sectionId) {
if (!SECTIONS.includes(sectionId)) return;
var next = '#' + encodeURIComponent(sectionId);
if (location.hash === next) return;
try { history.replaceState(null, '', location.pathname + location.search + next); }
catch (_) { location.hash = next; }
}
function runSideEffects(name) {
if ((name === 'source' || name === 'save') && typeof initCloneSampleText === 'function') initCloneSampleText();
if (name === 'library' && typeof loadVoiceLibrary === 'function') loadVoiceLibrary();
if ((name === 'integrations' || name === 'howto') && typeof renderIntegrationSnippets === 'function') {
if (typeof loadVoiceLibrary === 'function' && !(window._voices && window._voices.length)) loadVoiceLibrary();
renderIntegrationSnippets();
}
if (name === 'routing' && typeof loadRoutingTab === 'function') loadRoutingTab();
if (name === 'getvoices' && typeof loadGetVoices === 'function') loadGetVoices();
}
function applySettingsPage(cat) {
document.querySelectorAll('.s-settings-page').forEach(function (p) {
p.classList.toggle('is-active', p.dataset.page === cat);
});
document.querySelectorAll('[data-settings-cat]').forEach(function (el) {
el.classList.toggle('is-active', el.dataset.settingsCat === cat);
});
}
// ── Tree open/close state — each tree is independent ─────────────────────
// Load persisted open state (default: open voices + the active section's tree)
var _treeOpen = {};
try { _treeOpen = JSON.parse(localStorage.getItem('ttsvc_trees') || '{}'); } catch(_) {}
// Every collapsible tree must be listed so _applyTreeStates toggles its
// open class + chevron. `section` (optional) auto-opens the tree (and its
// ancestor trees, handled in _openTreesForSection) when that section opens.
var TREES = [
{ tree: 'nav-voices-tree', chevron: 'nav-voices-chevron', section: 's-voices' },
{ tree: 'nav-tags-tree', chevron: 'nav-tags-chevron' },
{ tree: 'nav-actions-tree', chevron: 'nav-actions-chevron' },
{ tree: 'nav-speak-tree', chevron: 'nav-speak-chevron' },
{ tree: 'nav-rehearser-tree', chevron: 'nav-rehearser-chevron', section: 's-rehearser' },
{ tree: 'nav-library-tree', chevron: 'nav-library-chevron', section: 's-library' },
{ tree: 'nav-setup-tree', chevron: 'nav-setup-chevron' },
{ tree: 'nav-engines-tree', chevron: 'nav-engines-chevron', section: 's-llms' },
{ tree: 'nav-integrations-tree', chevron: 'nav-integrations-chevron' },
{ tree: 'nav-settings-tree', chevron: 'nav-settings-chevron', section: 's-settings' },
];
// Open the tree(s) for a section: the head's own tree (via TREES.section)
// plus every ancestor .nav-tree of the active nav element, so nested items
// become visible. Persists once if anything changed.
function _openTreesForSection(sectionId) {
var changed = false;
TREES.forEach(function (t) {
if (t.section === sectionId && !_treeOpen[t.tree]) { _treeOpen[t.tree] = true; changed = true; }
});
var el = document.querySelector('[data-nav-section="' + sectionId + '"]');
while (el) {
if (el.classList && el.classList.contains('nav-tree') && el.id && !_treeOpen[el.id]) {
_treeOpen[el.id] = true; changed = true;
}
el = el.parentElement;
}
if (changed) localStorage.setItem('ttsvc_trees', JSON.stringify(_treeOpen));
}
function _applyTreeStates() {
TREES.forEach(function (t) {
var open = !!_treeOpen[t.tree];
var treeEl = document.getElementById(t.tree);
var chevEl = document.getElementById(t.chevron);
if (treeEl) treeEl.classList.toggle('open', open);
if (chevEl) chevEl.classList.toggle('open', open);
});
}
window.toggleNavTree = function (treeId, chevronId) {
_treeOpen[treeId] = !_treeOpen[treeId];
localStorage.setItem('ttsvc_trees', JSON.stringify(_treeOpen));
_applyTreeStates();
};
function showSection(sectionId) {
if (!SECTIONS.includes(sectionId)) sectionId = 's-voices';
// Leaving the Read Aloud reader: stop playback so audio doesn't keep running
if (sectionId !== 's-reader' && typeof window.readerStop === 'function') window.readerStop();
if (sectionId === 's-reader' && typeof window.readerOnShow === 'function') window.readerOnShow();
if (sectionId === 's-library' && typeof window.libraryRender === 'function') window.libraryRender(window._libraryView || 'books');
if (typeof window.initCollapsibleCards === 'function') window.initCollapsibleCards();
setStoredSection(sectionId);
setSectionHash(sectionId);
SECTIONS.forEach(function (id) {
var el = document.getElementById(id);
if (el) el.classList.toggle('is-active', id === sectionId);
});
document.querySelectorAll('[data-nav-section]').forEach(function (item) {
item.classList.toggle('active', item.dataset.navSection === sectionId);
});
// Navigating to a section auto-opens its tree AND every ancestor tree
// (so a nested item is actually visible), but never closes others.
_openTreesForSection(sectionId);
_applyTreeStates();
// When entering settings, show the active sub-page (default: connections)
if (sectionId === 's-settings') {
applySettingsPage(window._settingsSidebarCat || 'connections');
}
// When entering engines, show the active sub-page (default: llm)
if (sectionId === 's-llms') {
applyEnginesPage(window._enginesSidebarCat || 'llm');
}
var main = document.getElementById('main-content');
if (main) main.scrollTop = 0;
}
window.navTo = function (sectionId) {
showSection(sectionId);
var tabName = Object.keys(TAB_SECTION_MAP).find(function (k) {
return TAB_SECTION_MAP[k] === sectionId;
});
if (tabName) runSideEffects(tabName);
};
window.switchTab = function (name) {
var sectionId = TAB_SECTION_MAP[name];
if (sectionId) showSection(sectionId);
runSideEffects(name);
return true;
};
// ── Voice tree sub-navigation ─────────────────────────────────────────────
window._voiceSidebarCat = 'all';
window._voiceTagFilter = '';
window.navVoicesCat = function (cat) {
navTo('s-voices');
window._voiceSidebarCat = cat;
window._voiceTagFilter = ''; // a fixed category clears any tag subfolder filter
document.querySelectorAll('[data-voice-cat]').forEach(function (el) {
el.classList.toggle('is-active', el.dataset.voiceCat === cat);
});
document.querySelectorAll('[data-voice-tag]').forEach(function (el) { el.classList.remove('is-active'); });
if (typeof renderVoiceList === 'function') renderVoiceList();
};
// Click a tag "subfolder" in the My Voices tree → filter the list to that tag.
window.navVoicesTag = function (tag) {
navTo('s-voices');
window._voiceTagFilter = (window._voiceTagFilter === tag) ? '' : tag; // toggle off if re-clicked
window._voiceSidebarCat = 'all';
document.querySelectorAll('[data-voice-cat]').forEach(function (el) {
el.classList.toggle('is-active', el.dataset.voiceCat === 'all' && !window._voiceTagFilter);
});
document.querySelectorAll('[data-voice-tag]').forEach(function (el) {
el.classList.toggle('is-active', el.dataset.voiceTag === window._voiceTagFilter);
});
if (typeof renderVoiceList === 'function') renderVoiceList();
};
window.updateVoiceTree = function (voices) {
if (!voices) return;
function n(fn) { return voices.filter(fn).length; }
function set(id, v) { var el = document.getElementById(id); if (el) el.textContent = v || ''; }
set('ntc-all', n(function () { return true; }));
set('ntc-favorites',n(function (v) { return (v.rating || 0) >= 4; }));
set('ntc-hidden', n(function (v) { return v.enabled === false; }));
set('nav-voices-count', voices.length);
window.buildTagsTree(voices);
};
// ── Tags tree — distinct voice tags with counts, plus the computed
// Cloned / Designed / Favourites predicates that moved out of Library ──
window.buildTagsTree = function (voices) {
voices = voices || window._voices;
var tree = document.getElementById('nav-tags-tree');
if (!tree || !voices) return;
tree.innerHTML = '';
function n(fn) { return voices.filter(fn).length; }
// Computed predicate rows (not literal tag strings) → navVoicesCat
var preds = [
{ cat: 'cloned', icon: 'microphone-variant', label: 'Cloned', count: n(function (v) { return v.has_ref; }) },
{ cat: 'designed', icon: 'auto-fix', label: 'Designed', count: n(function (v) { return !v.has_ref; }) },
{ cat: 'favorites', icon: 'star-outline', label: 'Favourites', count: n(function (v) { return (v.rating || 0) >= 4; }) },
];
preds.forEach(function (p) {
var el = document.createElement('div');
el.className = 'nav-tree-item' + (window._voiceSidebarCat === p.cat && !window._voiceTagFilter ? ' is-active' : '');
el.dataset.voiceCat = p.cat;
el.innerHTML = '<span class="mdi mdi-' + p.icon + '"></span> ' + p.label + ' <span class="ntc">' + (p.count || '') + '</span>';
el.addEventListener('click', function () { window.navVoicesCat(p.cat); });
tree.appendChild(el);
});
// Literal tag strings → navVoicesTag (filters via _voiceTagFilter)
var counts = {};
voices.forEach(function (v) {
String(v.tag || '').split(',').map(function (t) { return t.trim(); }).filter(Boolean)
.forEach(function (t) { counts[t] = (counts[t] || 0) + 1; });
});
var names = Object.keys(counts).sort(function (a, b) { return a.toLowerCase().localeCompare(b.toLowerCase()); });
names.forEach(function (tag) {
var el = document.createElement('div');
el.className = 'nav-tree-item' + (window._voiceTagFilter === tag ? ' is-active' : '');
el.dataset.voiceTag = tag;
var icon = document.createElement('span'); icon.className = 'mdi mdi-tag-outline'; el.appendChild(icon);
el.appendChild(document.createTextNode(' ' + tag + ' '));
var c = document.createElement('span'); c.className = 'ntc'; c.textContent = counts[tag]; el.appendChild(c);
el.addEventListener('click', function () { window.navVoicesTag(tag); });
tree.appendChild(el);
});
};
// ── Settings tree sub-navigation ──────────────────────────────────────────
window._settingsSidebarCat = 'connections';
window.navSettingsCat = function (cat) {
window._settingsSidebarCat = cat;
localStorage.setItem('ttsvc_settings_cat', cat);
navTo('s-settings'); // showSection will call applySettingsPage(cat)
if (cat === 'logs' && typeof loadSettingsLogs === 'function') loadSettingsLogs();
if (cat === 'about' && typeof renderSettingsAbout === 'function') renderSettingsAbout();
};
// ── Engines tree sub-navigation ───────────────────────────────────────────
function applyEnginesPage(cat) {
document.querySelectorAll('.s-engines-page').forEach(function (p) {
p.classList.toggle('is-active', p.dataset.page === cat);
});
document.querySelectorAll('[data-engines-cat]').forEach(function (el) {
el.classList.toggle('is-active', el.dataset.enginesCat === cat);
});
}
window._enginesSidebarCat = 'llm';
window.navEnginesCat = function (cat) {
window._enginesSidebarCat = cat;
localStorage.setItem('ttsvc_engines_cat', cat);
navTo('s-llms'); // showSection will call applyEnginesPage(cat)
if ((cat === 'tts' || cat === 'stt') && typeof loadLocalContainers === 'function') loadLocalContainers();
};
// ── Rehearser tree sub-navigation ────────────────────────────────────────
window.navRehearserImpEx = function () {
navTo('s-rehearser');
// Clear phase highlights — Import/Export is its own panel, not a numbered phase
document.querySelectorAll('[data-rehearser-phase]').forEach(function (el) {
el.classList.remove('is-active');
});
document.getElementById('nav-reh-impex')?.classList.add('is-active');
if (typeof showRehImpEx === 'function') showRehImpEx();
else window._rehearserStartImpEx = true;
};
window.navRehearserPhase = function (phase) {
navTo('s-rehearser');
// highlight the correct tree item immediately
document.querySelectorAll('[data-rehearser-phase]').forEach(function (el) {
el.classList.toggle('is-active', Number(el.dataset.rehearserPhase) === phase);
});
// delegate to rehearser's own showPhase() when it's ready
if (typeof showPhase === 'function') {
showPhase(phase);
} else {
// rehearser.js not yet loaded — store the intent and let init pick it up
window._rehearserStartPhase = phase;
}
};
// Keep sidebar tree in sync when rehearser changes phase internally
window.onRehearserPhaseChange = function (phase) {
document.querySelectorAll('[data-rehearser-phase]').forEach(function (el) {
el.classList.toggle('is-active', Number(el.dataset.rehearserPhase) === phase);
});
};
// ── Reader tree sub-navigation ───────────────────────────────────────────
window.navReaderView = function (view) {
navTo('s-reader');
document.querySelectorAll('[data-reader-view]').forEach(function (el) {
el.classList.toggle('is-active', el.dataset.readerView === view);
});
if (typeof showReaderView === 'function') {
showReaderView(view);
} else {
window._readerStartView = view;
}
};
// Restore last-used section from hash/localStorage (fallback: My Voices)
var _savedSection = storedSection();
// On very first visit, default voices tree open
if (!localStorage.getItem('ttsvc_trees')) {
_treeOpen['nav-voices-tree'] = true;
}
window._settingsSidebarCat = localStorage.getItem('ttsvc_settings_cat') || 'connections';
window._enginesSidebarCat = localStorage.getItem('ttsvc_engines_cat') || 'llm';
var _startSection = hashSection() || ((_savedSection && SECTIONS.includes(_savedSection)) ? _savedSection : 's-voices');
showSection(_startSection);
window.addEventListener('hashchange', function () {
var section = hashSection();
if (section && section !== storedSection()) showSection(section);
});
var _startTab = Object.keys(TAB_SECTION_MAP).find(function (k) { return TAB_SECTION_MAP[k] === _startSection; }) || 'library';
runSideEffects(_startTab);
// ── Mobile sidebar drawer ────────────────────────────────────────────────
var sidebar = document.getElementById('sidebar');
var backdrop = document.getElementById('sidebar-backdrop');
var toggleBtn = document.getElementById('sidebar-toggle');
function openSidebar() {
if (!sidebar) return;
sidebar.classList.add('open');
if (backdrop) backdrop.classList.add('open');
document.body.style.overflow = 'hidden'; // prevent scroll behind drawer
}
function closeSidebar() {
if (!sidebar) return;
sidebar.classList.remove('open');
if (backdrop) backdrop.classList.remove('open');
document.body.style.overflow = '';
}
if (toggleBtn) toggleBtn.addEventListener('click', openSidebar);
if (backdrop) backdrop.addEventListener('click', closeSidebar);
// ── Desktop sidebar collapse/expand (button where the flag used to be) ────
try { if (localStorage.getItem('ttsvc_sidebar_collapsed') === '1') document.body.classList.add('sidebar-collapsed'); } catch (_) {}
window.toggleSidebarCollapsed = function () {
var collapsed = document.body.classList.toggle('sidebar-collapsed');
try { localStorage.setItem('ttsvc_sidebar_collapsed', collapsed ? '1' : '0'); } catch (_) {}
var btn = document.getElementById('sidebar-collapse-btn');
if (btn) btn.title = collapsed ? 'Expand sidebar' : 'Collapse sidebar';
if (!collapsed) _railTip.style.display = 'none';
};
// ── Icon-rail per-item label tooltip ────────────────────────────────────
var _railTip = document.createElement('div');
_railTip.id = 'nav-rail-tooltip';
document.body.appendChild(_railTip);
var _railTipActive = null;
var _sidebarEl = document.getElementById('sidebar');
_sidebarEl.addEventListener('mouseover', function (e) {
if (!document.body.classList.contains('sidebar-collapsed')) return;
var item = e.target.closest('.nav-item, .nav-tree-item, .nav-tree-head');
if (!item || item === _railTipActive) return;
var label = item.querySelector('.nav-label');
if (!label) return;
var text = label.textContent.trim();
if (!text) return;
var r = item.getBoundingClientRect();
_railTip.textContent = text;
_railTip.style.left = (r.right + 10) + 'px';
_railTip.style.top = (r.top + r.height / 2) + 'px';
_railTip.style.display = 'block';
_railTipActive = item;
});
_sidebarEl.addEventListener('mouseout', function (e) {
if (!e.relatedTarget || !e.relatedTarget.closest('#sidebar')) {
_railTip.style.display = 'none';
_railTipActive = null;
}
});
// Close drawer when a nav item is tapped on mobile
document.querySelectorAll('#sidebar .nav-item, #sidebar .nav-tree-item').forEach(function (el) {
el.addEventListener('click', function () {
if (window.innerWidth <= 767) closeSidebar();
});
});
// ── Mobile voice inspector: back button ──────────────────────────────────
// When a voice row is selected on mobile, show inspector full-height.
// The inspector JS will call window.onMobileInspectorOpen/Close as hooks.
window.onMobileInspectorOpen = function () {
var wb = document.querySelector('.voices-workbench');
if (!wb || window.innerWidth > 767) return;
wb.classList.add('mobile-inspector-open');
// Inject back button if not already there
var insp = document.getElementById('voices-inspector');
if (insp && !insp.querySelector('.insp-mobile-back')) {
var back = document.createElement('div');
back.className = 'insp-mobile-back';
back.innerHTML = '<span class="mdi mdi-arrow-left"></span> All voices';
back.addEventListener('click', function () { window.onMobileInspectorClose && window.onMobileInspectorClose(); });
insp.insertBefore(back, insp.firstChild);
}
};
window.onMobileInspectorClose = function () {
var wb = document.querySelector('.voices-workbench');
if (wb) wb.classList.remove('mobile-inspector-open');
var back = document.querySelector('.insp-mobile-back');
if (back) back.remove();
};
})();