(async function () { 'use strict'; // Hard safety net: never show skeleton longer than 8 s regardless of what // fails below. A partial UI beats an indefinite spinner. var _skTimeout = setTimeout(function () { var sk = document.getElementById('app-skeleton'); if (sk) { sk.classList.add('sk-hidden'); // Fully remove from DOM — otherwise the (display:none) skeleton still sits // in the document and earlier reveal logic would skip removal. setTimeout(function () { if (sk.parentNode) sk.parentNode.removeChild(sk); }, 300); document.querySelectorAll('.page-section').forEach(function (s) { s.style.display = ''; }); } }, 8000); 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 _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.indexOf(raw) !== -1 ? raw : ''; } // Determine which section to show first (same logic as nav.js): hash > stored > My Voices. var _savedSection = _storedSection(); var _activeSection = _hashSection() || ((_savedSection && SECTIONS.indexOf(_savedSection) !== -1) ? _savedSection : 's-voices'); // ── App version for cache-busting ──────────────────────────────────────── // Read from the already-loaded style.css href — no extra network round trip. // Falls back to fetching /api/version only if the tag isn't found. var _appVersion = 'dev'; var _styleHref = (document.querySelector('link[href*="style.css"]') || {}).getAttribute ? document.querySelector('link[href*="style.css"]').getAttribute('href') : ''; var _styleMatch = _styleHref && _styleHref.match(/[?&]v=([^&]+)/); if (_styleMatch) { _appVersion = _styleMatch[1]; } else { // Fallback: fetch from API (only on first ever load before style tag exists) try { var _vr = await fetch('/api/version'); var _vd = await _vr.json(); _appVersion = _vd.version || _appVersion; } catch (_) {} } // Session timestamp used ONLY for section HTML files (never cached by SW anyway). // JS/CSS use the version string so the service worker cache stays valid across reloads. var _ts = Date.now(); function _inject(id, html) { var el = document.getElementById(id); if (el) el.innerHTML = html; } // ── 1. Load ONLY the active section before anything else ───────────────── // Feature modules set up DOM event listeners — the active section's elements // must exist when JS runs. Other sections are loaded in step 2. try { var _ar = await fetch('/static/sections/' + _activeSection + '.html?v=' + _ts); if (!_ar.ok) throw new Error(_ar.status + ' ' + _ar.statusText); _inject(_activeSection, await _ar.text()); } catch (e) { var _ael = document.getElementById(_activeSection); if (_ael) _ael.innerHTML = '

Failed to load section ' + _activeSection + ': ' + e.message + '

'; } // ── 2. Fetch remaining sections in background (parallel with JS loading) ── // These run concurrently with utils + settings below. Because section HTML // files are much smaller than JS, they finish before the feature-module batch // needs them. var _bgSections = Promise.all( SECTIONS.filter(function (id) { return id !== _activeSection; }) .map(async function (id) { try { var res = await fetch('/static/sections/' + id + '.html?v=' + _ts); if (!res.ok) throw new Error(res.status + ' ' + res.statusText); _inject(id, await res.text()); } catch (e) { var el = document.getElementById(id); if (el) el.innerHTML = '

Failed to load section ' + id + ': ' + e.message + '

'; } }) ); // ── 3. Load JS in parallel batches ──────────────────────────────────────── // Scripts with async=false are FETCHED in parallel but EXECUTED in DOM // insertion order — safe for modules that share a global scope. function _load(src) { return new Promise(function (resolve, reject) { var s = document.createElement('script'); s.src = src + '?v=' + _appVersion; s.async = false; // fetch in parallel, execute in DOM order s.onload = resolve; s.onerror = function () { reject(new Error('Failed to load ' + src)); }; document.body.appendChild(s); }); } function _loadBatch(srcs) { return Promise.all(srcs.map(_load)); } // A — foundation (sequential: each must finish before the next batch starts) await _load('/static/js/utils.js'); await _load('/static/js/i18n.js'); // defines window.t / applyI18n before features run await _load('/static/js/settings.js'); // Wait for background sections BEFORE feature modules run. // Feature modules set up event listeners on DOM elements; all section HTML // must already be injected. On a local server the sections finish well before // settings.js completes, so this await is typically instant. await _bgSections; // C — feature modules: always try the bundle first (one request instead of 21). // Falls back to individual files if the bundle is missing or fails to load. var _featureFiles = [ '/static/js/voice-picker.js', '/static/js/benchmark-voice-picker.js', '/static/js/voice-inspector.js', '/static/js/seed-finder.js', '/static/js/voice-sources.js', '/static/js/fishaudio-browser.js', '/static/js/integrations.js', '/static/js/routing.js', '/static/js/voice-clone.js', '/static/js/voice-library.js', '/static/js/tts-preview.js', '/static/js/generation.js', '/static/js/benchmark.js', '/static/js/stt.js', '/static/js/rehearser-parse.js', '/static/js/rehearser.js', '/static/js/reader.js', '/static/js/audiobook.js', '/static/js/character-sheets.js', '/static/js/characters-library.js', '/static/js/sillytavern.js', '/static/js/library.js', ]; var _bundleOk = false; try { await _load('/static/dist/main.min.js'); _bundleOk = true; } catch (_) { _bundleOk = false; } if (!_bundleOk) await _loadBatch(_featureFiles); // D — init (needs everything above to be defined) try { await _load('/static/js/init.js'); } catch (_) {} // F — navigation (must run before revealing UI so the correct section shows) try { await _load('/static/nav.js'); } catch (_) {} // ── Remove skeleton, reveal sections ──────────────────────────────────── // Always runs — even if a script above failed to load, showing a partial UI // is far better than leaving the skeleton on screen indefinitely. _revealApp(); function _revealApp() { clearTimeout(_skTimeout); var sk = document.getElementById('app-skeleton'); // Always remove the skeleton if it's still in the DOM — never gate on // sk-hidden (the 8s fallback may have added that class without removing it). if (sk) { sk.classList.add('sk-hidden'); setTimeout(function () { if (sk.parentNode) sk.parentNode.removeChild(sk); }, 300); } document.querySelectorAll('.page-section').forEach(function (s) { s.style.display = ''; }); } // ── i18n: add the language picker and translate the (now fully-rendered) UI ── if (window.initLangPicker) window.initLangPicker(); if (window.applyI18n) window.applyI18n(document); if (window.initAccessibilityEnhancements) window.initAccessibilityEnhancements(); // E — post-init modules load in background AFTER the UI is already visible. // Engines, AI backends, and conversation load while the user browses other tabs. _loadBatch([ '/static/js/engines.js', '/static/js/ai-backends.js', '/static/js/conversation.js', ]); })();