diff --git a/public/css/style.css b/public/css/style.css
index e104c4a..8b2e925 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -723,7 +723,7 @@ ol.choice {
}
/* Options Modal Styling */
-.options-modal {
+.modal {
position: fixed;
top: 0;
left: 0;
@@ -736,7 +736,7 @@ ol.choice {
background-color: rgba(0, 0, 0, 0.5);
}
-.options-content {
+.modal-content {
background-color: rgba(255, 252, 245, 0.97);
border-radius: 0.4rem;
box-shadow: 0 0 1.5rem rgba(0, 0, 0, 0.4);
@@ -754,7 +754,7 @@ ol.choice {
transform-origin: center center;
}
-.options-header {
+.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
@@ -763,7 +763,7 @@ ol.choice {
padding-bottom: 0.7rem;
}
-.options-header h2 {
+.modal-header h2 {
margin: 0;
font-family: 'EB Garamond', var(--book-font), serif;
font-weight: normal;
@@ -772,7 +772,7 @@ ol.choice {
letter-spacing: 0.02rem;
}
-.options-close {
+.close {
background: none;
border: none;
font-size: 1.4rem;
@@ -781,7 +781,7 @@ ol.choice {
font-family: 'EB Garamond', var(--book-font), serif;
}
-.options-close:hover {
+.close:hover {
color: #5a3921;
}
@@ -800,7 +800,7 @@ ol.choice {
color: #5a3921;
}
-.options-row {
+.option-item {
display: flex;
justify-content: space-between;
align-items: center;
@@ -808,14 +808,14 @@ ol.choice {
padding: 0.25rem 0;
}
-.options-row label {
+.option-item label {
font-family: 'EB Garamond', var(--book-font), serif;
font-size: 1rem;
color: #4a4234;
}
/* Elegant checkboxes */
-.options-row input[type="checkbox"] {
+.option-item input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
width: 1rem;
@@ -829,7 +829,7 @@ ol.choice {
overflow: hidden;
}
-.options-row input[type="checkbox"]:checked::before {
+.option-item input[type="checkbox"]:checked::before {
content: "✓";
position: absolute;
font-family: 'EB Garamond', var(--book-font), serif;
@@ -840,7 +840,7 @@ ol.choice {
}
/* Elegant select dropdowns */
-.options-row select {
+.option-item select {
appearance: none;
-webkit-appearance: none;
background-color: transparent;
@@ -858,12 +858,12 @@ ol.choice {
min-width: 8rem;
}
-.options-row select:focus {
+.option-item select:focus {
outline: none;
}
/* Range inputs (sliders) - match the main menu style */
-.options-row input[type="range"] {
+.option-item input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 8rem;
@@ -878,7 +878,7 @@ ol.choice {
cursor: pointer;
}
-.options-row input[type="range"]::-webkit-slider-thumb {
+.option-item input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
height: 0.5rem;
@@ -889,7 +889,7 @@ ol.choice {
box-shadow: -407px 0 0 400px rgba(0,0,0,0.3);
}
-.options-row input[type="range"]::-moz-range-thumb {
+.option-item input[type="range"]::-moz-range-thumb {
height: 0.5rem;
width: 0.5rem;
border-radius: 0.25rem;
@@ -912,7 +912,7 @@ ol.choice {
text-align: center;
}
-/* API Settings in Options Panel */
+/* API Settings in options Panel */
.api-settings-container {
margin-top: 10px;
padding: 10px;
diff --git a/public/index.html b/public/index.html
index ee2784c..822f102 100644
--- a/public/index.html
+++ b/public/index.html
@@ -281,16 +281,20 @@
diff --git a/public/js/audio-manager-module.js b/public/js/audio-manager-module.js
index 1faae4e..4244be6 100644
--- a/public/js/audio-manager-module.js
+++ b/public/js/audio-manager-module.js
@@ -13,6 +13,7 @@ class AudioManagerModule extends BaseModule {
this.masterVolume = 1.0;
this.musicVolume = 1.0;
this.sfxVolume = 1.0;
+ this.ttsVolume = 1.0;
// Add persistence-manager as a dependency
this.dependencies = ['persistence-manager'];
@@ -193,7 +194,19 @@ class AudioManagerModule extends BaseModule {
this.masterVolume = Math.max(0, Math.min(1, volume));
this.updateVolumes();
}
-
+
+ /**
+ * Set the speech volume
+ * @param {number} volume - The volume level (0.0 to 1.0)
+ */
+ setTtsVolume(volume) {
+ this.ttsVolume = Math.max(0, Math.min(1, volume));
+ // Apply to current non-loop audio if it exists
+ if (this.currentAudio) {
+ this.currentAudio.volume = this.masterVolume * this.ttsVolume;
+ }
+ }
+
/**
* Set the music volume
* @param {number} volume - The volume level (0.0 to 1.0)
@@ -318,7 +331,7 @@ class AudioManagerModule extends BaseModule {
}
// Apply master volume and speech volume
- audio.volume = this.masterVolume * speechVolume;
+ audio.volume = this.masterVolume * speechVolume * this._ttsVolume;
// Set up cleanup
audio.onended = () => {
diff --git a/public/js/browser-tts-module.js b/public/js/browser-tts-module.js
index 4cc81fd..9a83a69 100644
--- a/public/js/browser-tts-module.js
+++ b/public/js/browser-tts-module.js
@@ -27,7 +27,7 @@ export class BrowserTTSModule extends TTSHandlerModule {
this.currentUtterance = null;
// Bind additional methods
- this.bindMethods(['onVoicesChanged', 'handleVoicePreferenceChanged']);
+ this.bindMethods(['handleVoicePreferenceChanged']);
}
/**
diff --git a/public/js/kokoro-tts-module.js b/public/js/kokoro-tts-module.js
index a824e7e..7011e42 100644
--- a/public/js/kokoro-tts-module.js
+++ b/public/js/kokoro-tts-module.js
@@ -41,7 +41,6 @@ export class KokoroTTSModule extends TTSHandlerModule {
async initialize() {
try {
console.log('Kokoro TTS: Initializing');
- this.state = 'INITIALIZING';
// Get dependencies
this.reportProgress(10, 'Loading dependencies');
@@ -195,21 +194,21 @@ export class KokoroTTSModule extends TTSHandlerModule {
case 'kokoro:error':
console.error('Kokoro TTS: Error from iframe:', event.data.error);
- this.state = 'ERROR';
+ // this.changeState('ERROR');
break;
- case 'kokoro:speech-generated':
+ case 'kokoro-generated':
// Handle speech generation completion
if (event.data.id !== undefined && this.pendingGenerations.has(event.data.id)) {
const resolver = this.pendingGenerations.get(event.data.id);
this.pendingGenerations.delete(event.data.id);
- if (event.data.error) {
- resolver.reject(new Error(event.data.error));
+ if (!event.data.success || event.data.error) {
+ resolver.reject(new Error(event.data.error || 'Speech generation failed'));
} else {
resolver.resolve({
success: true,
- audioData: event.data.audioData,
+ audioData: event.data.result && event.data.result.buffer,
duration: event.data.duration || 0
});
}
@@ -541,10 +540,10 @@ export class KokoroTTSModule extends TTSHandlerModule {
// Send request to iframe
this.iframe.contentWindow.postMessage({
- type: 'kokoro:generate-speech',
+ type: 'kokoro-generate',
text: processedText,
id,
- voiceId: this.currentVoice ? this.currentVoice.id : null
+ voice: this.currentVoice ? this.currentVoice.id : null
}, '*');
});
}
diff --git a/public/js/loader.js b/public/js/loader.js
index e8e63a1..e1ae211 100644
--- a/public/js/loader.js
+++ b/public/js/loader.js
@@ -51,20 +51,7 @@ const ModuleLoader = (function() {
}
console.log('Module Loader: Initialization started');
-
- // Check for circular dependencies before proceeding
- const circularDependencies = moduleRegistry.checkForCircularDependencies();
- if (circularDependencies) {
- const errorMsg = `Circular dependency detected: ${circularDependencies.join(' -> ')} -> ${circularDependencies[0]}`;
- console.error(errorMsg);
- document.body.innerHTML = `
-
Fatal Error: Circular Module Dependency
-
${errorMsg}
-
Please check the browser console for more details.
-
`;
- return;
- }
-
+
// Create the loading overlay
createLoadingOverlay();
diff --git a/public/js/module-registry.js b/public/js/module-registry.js
index f1f3d0e..a1abf10 100644
--- a/public/js/module-registry.js
+++ b/public/js/module-registry.js
@@ -7,9 +7,6 @@ export class ModuleRegistry {
this.modules = {};
this.readyPromises = {};
this.moduleDependencies = new Map(); // Track module dependencies
- this.visitedModules = new Set(); // For circular dependency detection
- this.recursionStack = new Set(); // For circular dependency detection
- this.untrackedDependencies = new Map(); // Track unregistered dependencies
}
/**
@@ -42,16 +39,6 @@ export class ModuleRegistry {
this.moduleDependencies.set(module.id, []);
}
- // Check for circular dependencies
- this.visitedModules.clear();
- this.recursionStack.clear();
- const circularDependency = this.detectCircularDependency(module.id);
- if (circularDependency) {
- const errorMsg = `Circular dependency detected: ${circularDependency.join(' -> ')} -> ${circularDependency[0]}`;
- console.error(errorMsg);
- throw new Error(errorMsg);
- }
-
// Create a promise that will resolve when this module is ready
this.readyPromises[module.id] = new Promise((resolve) => {
// Set up a state change listener for this module
@@ -70,77 +57,7 @@ export class ModuleRegistry {
}
});
}
-
- /**
- * Detect circular dependencies using DFS algorithm
- * @param {string} moduleId - Starting module ID
- * @param {Array} [path=[]] - Current dependency path
- * @returns {Array|null} - Array representing the circular dependency path, or null if none
- */
- detectCircularDependency(moduleId, path = []) {
- // If we've already checked this module completely, no need to check again
- if (this.visitedModules.has(moduleId)) {
- return null;
- }
- // If we're already visiting this module in the current path, we found a cycle
- if (this.recursionStack.has(moduleId)) {
- // Return the path that forms the cycle
- const cycleStartIndex = path.indexOf(moduleId);
- if (cycleStartIndex >= 0) {
- return path.slice(cycleStartIndex);
- }
- return path;
- }
-
- // Add to recursion stack to mark as being visited
- this.recursionStack.add(moduleId);
- path.push(moduleId);
-
- // Get dependencies for this module
- const dependencies = this.getDependencies(moduleId);
-
- // Check each dependency
- for (const depId of dependencies) {
- // Even if the dependency isn't registered yet, we need to track it
- // for potential circular dependencies that will manifest later
- // Create a temporary placeholder in the path for unregistered dependencies
- const depPath = [...path];
- if (!this.modules[depId]) {
- // Log that we're tracking an unregistered dependency
- console.log(`Module Registry: Tracking potential circular dependency with unregistered module: ${depId}`);
- // Add to the dependency tracking for future checks
- this.trackDependency(moduleId, depId);
- continue;
- }
-
- const result = this.detectCircularDependency(depId, depPath);
- if (result) {
- return result;
- }
- }
-
- // Remove from recursion stack as we're done with this module
- this.recursionStack.delete(moduleId);
-
- // Mark as fully visited
- this.visitedModules.add(moduleId);
-
- return null;
- }
-
- /**
- * Track an unregistered dependency
- * @param {string} moduleId - Module ID
- * @param {string} depId - Unregistered dependency ID
- */
- trackDependency(moduleId, depId) {
- if (!this.untrackedDependencies.has(moduleId)) {
- this.untrackedDependencies.set(moduleId, new Set());
- }
- this.untrackedDependencies.get(moduleId).add(depId);
- }
-
/**
* Get a module by id
* @param {string} id - Module id
@@ -166,25 +83,7 @@ export class ModuleRegistry {
getDependencies(id) {
return this.moduleDependencies.get(id) || [];
}
-
- /**
- * Check if the dependency graph has any circular dependencies
- * @returns {Array|null} - Array representing the circular dependency path, or null if none
- */
- checkForCircularDependencies() {
- this.visitedModules.clear();
-
- for (const moduleId in this.modules) {
- this.recursionStack.clear();
- const result = this.detectCircularDependency(moduleId);
- if (result) {
- return result;
- }
- }
-
- return null;
- }
-
+
/**
* Wait for a module to be ready (in FINISHED state)
* @param {string} id - Module id to wait for
diff --git a/public/js/openai-tts-module.js b/public/js/openai-tts-module.js
index 0ea4261..6ba2a25 100644
--- a/public/js/openai-tts-module.js
+++ b/public/js/openai-tts-module.js
@@ -11,7 +11,7 @@ export class OpenAITTSModule extends ApiTTSModuleBase {
// Voice options specific to OpenAI
this.voiceOptions = {
voice: 'alloy', // Default voice for OpenAI
- model: 'tts-1', // Standard model
+ model: 'tts-1-hd', // Standard model
speed: 1.0,
response_format: 'mp3' // OpenAI supports mp3, opus, aac, and flac (not wav)
};
@@ -19,11 +19,16 @@ export class OpenAITTSModule extends ApiTTSModuleBase {
// Predefined voices - OpenAI has a fixed set
this.voices = [
{ id: 'alloy', name: 'Alloy', language: 'en' },
+ { id: 'ash', name: 'Ash', language: 'en' },
+ { id: 'ballad', name: 'Ballad', language: 'en' },
+ { id: 'coral', name: 'Coral', language: 'en' },
{ id: 'echo', name: 'Echo', language: 'en' },
{ id: 'fable', name: 'Fable', language: 'en' },
{ id: 'onyx', name: 'Onyx', language: 'en' },
{ id: 'nova', name: 'Nova', language: 'en' },
- { id: 'shimmer', name: 'Shimmer', language: 'en' }
+ { id: 'sage', name: 'Sage', language: 'en' },
+ { id: 'shimmer', name: 'Shimmer', language: 'en' },
+ { id: 'verse', name: 'Verse', language: 'en' }
];
}
@@ -208,7 +213,14 @@ export class OpenAITTSModule extends ApiTTSModuleBase {
}
if (typeof options.speed === 'number') {
- this.voiceOptions.speed = Math.max(0.5, Math.min(2.0, options.speed));
+ // OpenAI API supports speed values from 0.25 to 4.0 with 1 as default
+ if (options.speed <= 0.5) {
+ // Map [0, 0.5] -> [0.25, 1]
+ this.voiceOptions.speed = 0.25 + (1 - 0.25) * (options.speed / 0.5);
+ } else {
+ // Map [0.5, 1] -> [1, 4]
+ this.voiceOptions.speed = 1 + (4 - 1) * ((options.speed - 0.5) / 0.5);
+ }
}
// Handle OpenAI-specific options
diff --git a/public/js/options-ui-module.js b/public/js/options-ui-module.js
index ee2034d..599904a 100644
--- a/public/js/options-ui-module.js
+++ b/public/js/options-ui-module.js
@@ -38,12 +38,9 @@ class OptionsUIModule extends BaseModule {
'populateVoices',
'populateLanguages',
'loadPreferences',
- 'applySettings',
- 'handleTtsSystemChanged',
'showReloadNotice',
'toggle',
'setupEventListeners',
- 'saveCurrentSettings',
'setupApiUrlFields',
'setupInitialState',
'dispatchApiChangeEvent',
@@ -109,8 +106,8 @@ class OptionsUIModule extends BaseModule {
// Set up initial state
await this.setupInitialState();
- // Set up immediate save listeners
- this.setupImmediateSaveListeners();
+ // Set up automatic bindings using the persistence manager
+ this.setupPreferenceBindings();
this.reportProgress(100, 'Options UI initialized');
return true;
@@ -120,356 +117,420 @@ class OptionsUIModule extends BaseModule {
* Create the options modal
*/
createModal() {
- if (this.modal) return;
-
- const body = document.body;
+ console.log('Options UI: Creating options modal');
// Create modal container
- this.modal = createUIElement('div', { className: 'options-modal', id: 'options-modal' }, null, body);
-
- // Create modal content
- const modalContent = createUIElement('div', { className: 'options-content' }, null, this.modal);
-
- // Create header
- const header = createUIElement('div', { className: 'options-header' }, null, modalContent);
- createUIElement('h2', {}, 'Options', header);
- this.elements.closeButton = createUIElement('button', { className: 'options-close', 'aria-label': 'Close' }, '×', header);
-
- // Create settings container
- const settings = createUIElement('div', { className: 'options-settings' }, null, modalContent);
-
- // Language Section
- const languageSection = createUIElement('div', { className: 'options-section' }, null, settings);
- createUIElement('h3', {}, 'Language Settings', languageSection);
-
- // Language selection
- const languageContainer = createUIElement('div', { className: 'options-row' }, null, languageSection);
- createUIElement('label', {}, 'Language:', languageContainer);
- this.elements.language = createUIElement('select', { id: 'app-language' }, null, languageContainer);
-
- // TTS Settings
- const ttsSection = createUIElement('div', { className: 'options-section' }, null, settings);
- createUIElement('h3', {}, 'Text-to-Speech', ttsSection);
-
- // TTS Toggle
- const ttsSpeechToggleContainer = createUIElement('div', { className: 'options-row' }, null, ttsSection);
- createUIElement('label', {}, 'Enable Text-to-Speech:', ttsSpeechToggleContainer);
- this.elements.ttsEnabled = createUIElement('input', { type: 'checkbox', id: 'tts-enabled' }, null, ttsSpeechToggleContainer);
-
- // TTS System
- const ttsSystemContainer = createUIElement('div', { className: 'options-row' }, null, ttsSection);
- createUIElement('label', {}, 'TTS System:', ttsSystemContainer);
- this.elements.ttsSystem = createUIElement('select', { id: 'tts-system' }, null, ttsSystemContainer);
-
- // TTS Voice
- const ttsVoiceContainer = createUIElement('div', { className: 'options-row' }, null, ttsSection);
- createUIElement('label', {}, 'Voice:', ttsVoiceContainer);
- this.elements.ttsVoice = createUIElement('select', { id: 'tts-voice' }, null, ttsVoiceContainer);
-
- // TTS Speed
- const speedContainer = createUIElement('div', { className: 'options-row' }, null, ttsSection);
- createUIElement('label', {}, 'TTS Speed:', speedContainer);
- this.elements.ttsSpeed = createUIElement('input', {
- type: 'range',
- id: 'tts-speed',
- min: '0',
- max: '100'
- }, null, speedContainer);
-
- // Create API settings for each provider
- const apiSettings = this.createApiSettings(ttsSection);
-
- // Audio Settings Section
- const audioSection = createUIElement('div', { className: 'options-section' }, null, settings);
- createUIElement('h3', {}, 'Audio', audioSection);
-
- // Master Volume
- const masterVolumeContainer = createUIElement('div', { className: 'options-row' }, null, audioSection);
- createUIElement('label', {}, 'Master Volume:', masterVolumeContainer);
- this.elements.masterVolume = createUIElement('input', {
- type: 'range',
- id: 'master-volume',
- min: '0',
- max: '100'
- }, null, masterVolumeContainer);
-
- // Music Volume
- const musicVolumeContainer = createUIElement('div', { className: 'options-row' }, null, audioSection);
- createUIElement('label', {}, 'Music Volume:', musicVolumeContainer);
- this.elements.musicVolume = createUIElement('input', {
- type: 'range',
- id: 'music-volume',
- min: '0',
- max: '100'
- }, null, musicVolumeContainer);
-
- // SFX Volume
- const sfxVolumeContainer = createUIElement('div', { className: 'options-row' }, null, audioSection);
- createUIElement('label', {}, 'Sound Effects Volume:', sfxVolumeContainer);
- this.elements.sfxVolume = createUIElement('input', {
- type: 'range',
- id: 'sfx-volume',
- min: '0',
- max: '100'
- }, null, sfxVolumeContainer);
-
- // Ambience Volume
- const ambienceVolumeContainer = createUIElement('div', { className: 'options-row' }, null, audioSection);
- createUIElement('label', {}, 'Ambience Volume:', ambienceVolumeContainer);
- this.elements.ambienceVolume = createUIElement('input', {
- type: 'range',
- id: 'ambience-volume',
- min: '0',
- max: '100'
- }, null, ambienceVolumeContainer);
-
- // Initialize with display: none
+ this.modal = document.createElement('div');
+ this.modal.id = 'options-modal';
+ this.modal.className = 'modal';
this.modal.style.display = 'none';
- // Add event handlers
- this.elements.closeButton.addEventListener('click', () => {
- this.saveCurrentSettings();
- this.hide();
- });
- }
-
- /**
- * Create API settings for TTS providers
- * @param {HTMLElement} parentSection - Parent section for API settings
- * @returns {Object} - Object with API settings elements
- */
- createApiSettings(parentSection) {
- // ElevenLabs settings
- // API Key
- const elevenLabsApiKeyContainer = createUIElement('div', {
- className: 'options-row elevenlabs-tts-setting',
- 'data-provider': 'elevenlabs-tts'
- }, null, parentSection);
+ // Create modal content
+ const modalContent = document.createElement('div');
+ modalContent.className = 'modal-content';
- createUIElement('label', {}, 'ElevenLabs API Key:', elevenLabsApiKeyContainer);
- this.elements.elevenLabsApiKey = createUIElement('input', {
- type: 'password',
- placeholder: 'Enter your ElevenLabs API key'
- }, null, elevenLabsApiKeyContainer);
+ // Create header
+ const header = document.createElement('div');
+ header.className = 'modal-header';
- // API URL
- const elevenLabsApiUrlContainer = createUIElement('div', {
- className: 'options-row elevenlabs-tts-setting',
- 'data-provider': 'elevenlabs-tts'
- }, null, parentSection);
+ const title = document.createElement('h2');
+ title.textContent = 'Options';
+ header.appendChild(title);
- createUIElement('label', {}, 'ElevenLabs API URL:', elevenLabsApiUrlContainer);
- this.elements.elevenLabsApiUrl = createUIElement('input', {
- type: 'text',
- placeholder: 'https://api.elevenlabs.io/v1'
- }, null, elevenLabsApiUrlContainer);
+ const closeButton = document.createElement('span');
+ closeButton.className = 'close';
+ closeButton.innerHTML = '×';
+ closeButton.onclick = () => this.hide();
+ header.appendChild(closeButton);
- // OpenAI settings
- // API Key
- const openaiApiKeyContainer = createUIElement('div', {
- className: 'options-row openai-tts-setting',
- 'data-provider': 'openai-tts'
- }, null, parentSection);
+ modalContent.appendChild(header);
- createUIElement('label', {}, 'OpenAI API Key:', openaiApiKeyContainer);
- this.elements.openaiApiKey = createUIElement('input', {
- type: 'password',
- placeholder: 'Enter your OpenAI API key'
- }, null, openaiApiKeyContainer);
+ // Create body
+ const body = document.createElement('div');
+ body.className = 'modal-body';
- // API URL
- const openaiApiUrlContainer = createUIElement('div', {
- className: 'options-row openai-tts-setting',
- 'data-provider': 'openai-tts'
- }, null, parentSection);
+ // Create sections
+ // App Settings Section (Language and Speed)
+ const appSettingsSection = document.createElement('div');
+ appSettingsSection.className = 'options-section';
- createUIElement('label', {}, 'OpenAI API URL:', openaiApiUrlContainer);
- this.elements.openaiApiUrl = createUIElement('input', {
- type: 'text',
- placeholder: 'https://api.openai.com/v1'
- }, null, openaiApiUrlContainer);
+ const appSettingsTitle = document.createElement('h3');
+ appSettingsTitle.textContent = 'Application Settings';
+ appSettingsSection.appendChild(appSettingsTitle);
- // Initially hide API settings
- const apiSettings = document.querySelectorAll('.elevenlabs-tts-setting, .openai-tts-setting');
- apiSettings.forEach(setting => {
- setting.style.display = 'none';
+ // Language
+ const languageContainer = document.createElement('div');
+ languageContainer.className = 'option-item';
+
+ const languageLabel = document.createElement('label');
+ languageLabel.textContent = 'Language:';
+ languageContainer.appendChild(languageLabel);
+
+ this.elements.language = createUIElement('select', {
+ 'data-pref-bind': 'app.locale'
+ }, null, languageContainer);
+
+ appSettingsSection.appendChild(languageContainer);
+
+ // Speed
+ const speedContainer = document.createElement('div');
+ speedContainer.className = 'option-item';
+
+ const speedLabel = document.createElement('label');
+ speedLabel.textContent = 'Speed:';
+ speedContainer.appendChild(speedLabel);
+
+ const speedValue = document.createElement('span');
+ speedValue.className = 'slider-value';
+ speedValue.textContent = '100%';
+ speedContainer.appendChild(speedValue);
+
+ this.elements.ttsSpeed = createUIElement('input', {
+ type: 'range',
+ min: 50,
+ max: 200,
+ value: 100,
+ 'data-pref-bind': 'app.speed',
+ 'data-pref-transform': 'range:0.5,2.0'
+ }, null, speedContainer);
+
+ // Update displayed value when slider changes
+ this.elements.ttsSpeed.addEventListener('input', () => {
+ speedValue.textContent = `${this.elements.ttsSpeed.value}%`;
});
- return { elevenLabsApiKeyContainer, elevenLabsApiUrlContainer, openaiApiKeyContainer, openaiApiUrlContainer };
- }
-
- /**
- * Set up event listeners for UI elements
- */
- setupEventListeners() {
- // TTS System change event
- if (this.elements.ttsSystem) {
- this.elements.ttsSystem.addEventListener('change', this.handleTtsSystemChanged);
- }
+ appSettingsSection.appendChild(speedContainer);
- // TTS Enable toggle event
- if (this.elements.ttsEnabled) {
- this.elements.ttsEnabled.addEventListener('change', (event) => {
- const enabled = event.target.checked;
- console.log('Options UI: TTS enabled changed to', enabled);
-
- // Save setting
- this.updatePreference('tts', 'enabled', enabled);
-
- // Update TTS Factory
- const ttsFactory = this.getModule('tts-factory');
- if (ttsFactory) {
- ttsFactory.configure({ enabled });
- }
- });
- }
+ body.appendChild(appSettingsSection);
- // Voice change event
- if (this.elements.ttsVoice) {
- this.elements.ttsVoice.addEventListener('change', (event) => {
- const voice = event.target.value;
- console.log('Options UI: TTS voice changed to', voice);
-
- // Save setting
- this.updatePreference('tts', 'voice', voice);
-
- // Update TTS Factory
- const ttsFactory = this.getModule('tts-factory');
- if (ttsFactory) {
- ttsFactory.configure({ voice });
- }
- });
- }
+ // TTS Section
+ const ttsSection = document.createElement('div');
+ ttsSection.className = 'options-section';
- // TTS Speed change event
- if (this.elements.ttsSpeed) {
- this.elements.ttsSpeed.addEventListener('input', (event) => {
- const speed = parseInt(event.target.value) / 100;
- console.log('Options UI: TTS speed changed to', speed);
-
- // Save setting
- this.updatePreference('tts', 'speed', speed);
-
- // Update TTS Factory
- const ttsFactory = this.getModule('tts-factory');
- if (ttsFactory) {
- ttsFactory.configure({ speed });
- }
- });
- }
+ const ttsTitle = document.createElement('h3');
+ ttsTitle.textContent = 'Text-to-Speech';
+ ttsSection.appendChild(ttsTitle);
- // Language change event
- if (this.elements.language) {
- this.elements.language.addEventListener('change', (event) => {
- const locale = event.target.value;
- console.log('Options UI: Language changed to', locale);
-
- // Save settings
- this.updatePreference('app', 'locale', locale);
-
- // Update Localization module
- const localization = this.getModule('localization');
- if (localization) {
- localization.setLocale(locale);
- }
-
- // Show reload notice
- this.showReloadNotice();
- });
- }
+ // TTS Enable
+ const ttsEnableContainer = document.createElement('div');
+ ttsEnableContainer.className = 'option-item';
+
+ const ttsEnableLabel = document.createElement('label');
+ ttsEnableLabel.textContent = 'Enable TTS:';
+ ttsEnableContainer.appendChild(ttsEnableLabel);
+
+ this.elements.ttsEnabled = createUIElement('input', {
+ type: 'checkbox',
+ 'data-pref-bind': 'tts.enabled'
+ }, null, ttsEnableContainer);
+
+ ttsSection.appendChild(ttsEnableContainer);
+
+ // TTS System
+ const ttsSystemContainer = document.createElement('div');
+ ttsSystemContainer.className = 'option-item';
+
+ const ttsSystemLabel = document.createElement('label');
+ ttsSystemLabel.textContent = 'TTS System:';
+ ttsSystemContainer.appendChild(ttsSystemLabel);
+
+ this.elements.ttsSystem = createUIElement('select', {
+ 'data-pref-bind': 'tts.preferred_handler'
+ }, null, ttsSystemContainer);
+
+ ttsSection.appendChild(ttsSystemContainer);
+
+ // TTS Voice
+ const ttsVoiceContainer = document.createElement('div');
+ ttsVoiceContainer.className = 'option-item';
+
+ const ttsVoiceLabel = document.createElement('label');
+ ttsVoiceLabel.textContent = 'Voice:';
+ ttsVoiceContainer.appendChild(ttsVoiceLabel);
+
+ this.elements.ttsVoice = createUIElement('select', {
+ 'data-pref-bind': 'tts.voice'
+ }, null, ttsVoiceContainer);
+
+ ttsSection.appendChild(ttsVoiceContainer);
+
+ // Add API Settings
+ const apiSettings = this.createApiSettings();
+ ttsSection.appendChild(apiSettings);
+
+ body.appendChild(ttsSection);
+
+ // Audio Section
+ const audioSection = document.createElement('div');
+ audioSection.className = 'options-section';
+
+ const audioTitle = document.createElement('h3');
+ audioTitle.textContent = 'Audio';
+ audioSection.appendChild(audioTitle);
- // Audio Settings
// Master Volume
- if (this.elements.masterVolume) {
- this.elements.masterVolume.addEventListener('input', (event) => {
- const volume = parseInt(event.target.value) / 100;
- console.log('Options UI: Master volume changed to', volume);
-
- // Save setting
- this.updatePreference('audio', 'masterVolume', volume);
-
- // Update Audio Manager
- const audioManager = this.getModule('audio-manager');
- if (audioManager) {
- audioManager.setMasterVolume(volume);
- }
- });
- }
+ const masterVolumeContainer = document.createElement('div');
+ masterVolumeContainer.className = 'option-item';
+
+ const masterVolumeLabel = document.createElement('label');
+ masterVolumeLabel.textContent = 'Master Volume:';
+ masterVolumeContainer.appendChild(masterVolumeLabel);
+
+ const masterVolumeValue = document.createElement('span');
+ masterVolumeValue.className = 'slider-value';
+ masterVolumeValue.textContent = '100%';
+ masterVolumeContainer.appendChild(masterVolumeValue);
+
+ this.elements.masterVolume = createUIElement('input', {
+ type: 'range',
+ min: 0,
+ max: 100,
+ value: 100,
+ 'data-pref-bind': 'audio.masterVolume',
+ 'data-pref-transform': 'range:0,1'
+ }, null, masterVolumeContainer);
+
+ // Update displayed value when slider changes
+ this.elements.masterVolume.addEventListener('input', () => {
+ masterVolumeValue.textContent = `${this.elements.masterVolume.value}%`;
+ });
+
+ audioSection.appendChild(masterVolumeContainer);
+
+ // Speech Volume
+ const ttsVolumeContainer = document.createElement('div');
+ ttsVolumeContainer.className = 'option-item';
+
+ const ttsVolumeLabel = document.createElement('label');
+ ttsVolumeLabel.textContent = 'Speech Volume:';
+ ttsVolumeContainer.appendChild(ttsVolumeLabel);
+
+ const ttsVolumeValue = document.createElement('span');
+ ttsVolumeValue.className = 'slider-value';
+ ttsVolumeValue.textContent = '100%';
+ ttsVolumeContainer.appendChild(ttsVolumeValue);
+
+ this.elements.ttsVolume = createUIElement('input', {
+ type: 'range',
+ min: 0,
+ max: 100,
+ value: 100,
+ 'data-pref-bind': 'audio.ttsVolume',
+ 'data-pref-transform': 'range:0,1'
+ }, null, ttsVolumeContainer);
+
+ // Update displayed value when slider changes
+ this.elements.ttsVolume.addEventListener('input', () => {
+ ttsVolumeValue.textContent = `${this.elements.ttsVolume.value}%`;
+ });
+
+ audioSection.appendChild(ttsVolumeContainer);
// Music Volume
- if (this.elements.musicVolume) {
- this.elements.musicVolume.addEventListener('input', (event) => {
- const volume = parseInt(event.target.value) / 100;
- console.log('Options UI: Music volume changed to', volume);
-
- // Save setting
- this.updatePreference('audio', 'musicVolume', volume);
-
- // Update Audio Manager
- const audioManager = this.getModule('audio-manager');
- if (audioManager) {
- audioManager.setMusicVolume(volume);
- }
- });
- }
+ const musicVolumeContainer = document.createElement('div');
+ musicVolumeContainer.className = 'option-item';
- // SFX Volume
- if (this.elements.sfxVolume) {
- this.elements.sfxVolume.addEventListener('input', (event) => {
- const volume = parseInt(event.target.value) / 100;
- console.log('Options UI: SFX volume changed to', volume);
-
- // Save setting
- this.updatePreference('audio', 'sfxVolume', volume);
-
- // Update Audio Manager
- const audioManager = this.getModule('audio-manager');
- if (audioManager) {
- audioManager.setSfxVolume(volume);
- }
- });
- }
+ const musicVolumeLabel = document.createElement('label');
+ musicVolumeLabel.textContent = 'Music Volume:';
+ musicVolumeContainer.appendChild(musicVolumeLabel);
- }
-
- /**
- * Handle TTS system change
- * @param {Event} event - Change event
- */
- async handleTtsSystemChanged(event) {
- const selectedSystem = event.target.value;
- console.log('Options UI: TTS system changed to', selectedSystem);
+ const musicVolumeValue = document.createElement('span');
+ musicVolumeValue.className = 'slider-value';
+ musicVolumeValue.textContent = '100%';
+ musicVolumeContainer.appendChild(musicVolumeValue);
- // Update API settings visibility
- this.updateApiSettingsVisibility(selectedSystem);
+ this.elements.musicVolume = createUIElement('input', {
+ type: 'range',
+ min: 0,
+ max: 100,
+ value: 70,
+ 'data-pref-bind': 'audio.musicVolume',
+ 'data-pref-transform': 'range:0,1'
+ }, null, musicVolumeContainer);
- // Save setting
- this.updatePreference('tts', 'preferred_handler', selectedSystem);
-
- // Notify TTSFactory of handler change
- const ttsFactory = this.getModule('tts-factory');
- if (ttsFactory) {
- await ttsFactory.setActiveHandler(selectedSystem);
-
- // Now that the handler has changed, update voices for the selected system
- await this.populateVoices();
- }
- }
-
- /**
- * Update API settings visibility based on selected TTS system
- * @param {string} selectedSystem - Selected TTS system
- */
- updateApiSettingsVisibility(selectedSystem) {
- const elevenLabsSettings = document.querySelectorAll('.elevenlabs-tts-setting');
- const openaiSettings = document.querySelectorAll('.openai-tts-setting');
-
- elevenLabsSettings.forEach(setting => {
- setting.style.display = selectedSystem === 'elevenlabs-tts' ? 'flex' : 'none';
+ // Update displayed value when slider changes
+ this.elements.musicVolume.addEventListener('input', () => {
+ musicVolumeValue.textContent = `${this.elements.musicVolume.value}%`;
});
- openaiSettings.forEach(setting => {
- setting.style.display = selectedSystem === 'openai-tts' ? 'flex' : 'none';
+ audioSection.appendChild(musicVolumeContainer);
+
+ // SFX Volume
+ const sfxVolumeContainer = document.createElement('div');
+ sfxVolumeContainer.className = 'option-item';
+
+ const sfxVolumeLabel = document.createElement('label');
+ sfxVolumeLabel.textContent = 'Sound Effects Volume:';
+ sfxVolumeContainer.appendChild(sfxVolumeLabel);
+
+ const sfxVolumeValue = document.createElement('span');
+ sfxVolumeValue.className = 'slider-value';
+ sfxVolumeValue.textContent = '100%';
+ sfxVolumeContainer.appendChild(sfxVolumeValue);
+
+ this.elements.sfxVolume = createUIElement('input', {
+ type: 'range',
+ min: 0,
+ max: 100,
+ value: 100,
+ 'data-pref-bind': 'audio.sfxVolume',
+ 'data-pref-transform': 'range:0,1'
+ }, null, sfxVolumeContainer);
+
+ // Update displayed value when slider changes
+ this.elements.sfxVolume.addEventListener('input', () => {
+ sfxVolumeValue.textContent = `${this.elements.sfxVolume.value}%`;
+ });
+
+ audioSection.appendChild(sfxVolumeContainer);
+
+ body.appendChild(audioSection);
+
+ modalContent.appendChild(body);
+
+ // Create footer
+ const footer = document.createElement('div');
+ footer.className = 'modal-footer';
+
+ const closeModalButton = document.createElement('button');
+ closeModalButton.textContent = 'Close';
+ closeModalButton.onclick = () => this.hide();
+ footer.appendChild(closeModalButton);
+
+ modalContent.appendChild(footer);
+
+ // Add modal content to modal
+ this.modal.appendChild(modalContent);
+
+ // Add modal to document
+ document.body.appendChild(this.modal);
+ }
+
+ /**
+ * Create API settings controls
+ * @returns {HTMLElement} - API settings element
+ */
+ createApiSettings() {
+ console.log('Options UI: Creating API settings');
+ const apiSettings = document.createElement('div');
+ apiSettings.className = 'options-section';
+
+ // ElevenLabs API settings
+ const elevenLabsSettings = document.createElement('div');
+ elevenLabsSettings.className = 'api-settings elevenlabs-tts-settings';
+ elevenLabsSettings.style.display = 'none';
+
+ const elevenLabsTitle = document.createElement('h3');
+ elevenLabsTitle.textContent = 'ElevenLabs API Settings';
+ elevenLabsSettings.appendChild(elevenLabsTitle);
+
+ // ElevenLabs API Key
+ const elevenLabsApiKeyContainer = document.createElement('div');
+ elevenLabsApiKeyContainer.className = 'option-item';
+
+ const elevenLabsApiKeyLabel = document.createElement('label');
+ elevenLabsApiKeyLabel.textContent = 'API Key:';
+ elevenLabsApiKeyContainer.appendChild(elevenLabsApiKeyLabel);
+
+ this.elements.elevenLabsApiKey = createUIElement('input', {
+ type: 'password',
+ 'data-pref-bind': 'tts.elevenlabs-tts_api_key'
+ }, null, elevenLabsApiKeyContainer);
+
+ elevenLabsSettings.appendChild(elevenLabsApiKeyContainer);
+
+ // ElevenLabs API URL
+ const elevenLabsApiUrlContainer = document.createElement('div');
+ elevenLabsApiUrlContainer.className = 'option-item';
+
+ const elevenLabsApiUrlLabel = document.createElement('label');
+ elevenLabsApiUrlLabel.textContent = 'API URL:';
+ elevenLabsApiUrlContainer.appendChild(elevenLabsApiUrlLabel);
+
+ this.elements.elevenLabsApiUrl = createUIElement('input', {
+ type: 'text',
+ 'data-pref-bind': 'tts.elevenlabs-tts_api_url'
+ }, null, elevenLabsApiUrlContainer);
+
+ elevenLabsSettings.appendChild(elevenLabsApiUrlContainer);
+
+ // OpenAI API settings
+ const openaiSettings = document.createElement('div');
+ openaiSettings.className = 'api-settings openai-tts-settings';
+ openaiSettings.style.display = 'none';
+
+ const openaiTitle = document.createElement('h3');
+ openaiTitle.textContent = 'OpenAI API Settings';
+ openaiSettings.appendChild(openaiTitle);
+
+ // OpenAI API Key
+ const openaiApiKeyContainer = document.createElement('div');
+ openaiApiKeyContainer.className = 'option-item';
+
+ const openaiApiKeyLabel = document.createElement('label');
+ openaiApiKeyLabel.textContent = 'API Key:';
+ openaiApiKeyContainer.appendChild(openaiApiKeyLabel);
+
+ this.elements.openaiApiKey = createUIElement('input', {
+ type: 'password',
+ 'data-pref-bind': 'tts.openai-tts_api_key'
+ }, null, openaiApiKeyContainer);
+
+ openaiSettings.appendChild(openaiApiKeyContainer);
+
+ // OpenAI API URL
+ const openaiApiUrlContainer = document.createElement('div');
+ openaiApiUrlContainer.className = 'option-item';
+
+ const openaiApiUrlLabel = document.createElement('label');
+ openaiApiUrlLabel.textContent = 'API URL:';
+ openaiApiUrlContainer.appendChild(openaiApiUrlLabel);
+
+ this.elements.openaiApiUrl = createUIElement('input', {
+ type: 'text',
+ 'data-pref-bind': 'tts.openai-tts_api_url'
+ }, null, openaiApiUrlContainer);
+
+ openaiSettings.appendChild(openaiApiUrlContainer);
+
+ // Add all API settings to container
+ apiSettings.appendChild(elevenLabsSettings);
+ apiSettings.appendChild(openaiSettings);
+
+ return apiSettings;
+ }
+
+ /**
+ * Set up event listeners for options controls
+ */
+ setupEventListeners() {
+ if (!this.modal) return;
+
+ // TTS System change
+ if (this.elements.ttsSystem) {
+ this.elements.ttsSystem.addEventListener('change', async (event) => {
+ this.updateApiSettingsVisibility(event.target.value);
+ await this.populateVoices();
+ });
+ }
+
+ // Close when clicking outside the modal content
+ this.modal.addEventListener('click', (event) => {
+ if (event.target === this.modal) {
+ this.hide();
+ }
+ });
+ }
+
+ /**
+ * Update API settings visibility based on selected TTS system
+ * @param {string} handlerId - Selected TTS system
+ */
+ updateApiSettingsVisibility(handlerId) {
+ const apiContainers = this.modal.querySelectorAll('.api-settings');
+ apiContainers.forEach(container => {
+ const shouldShow = container.classList.contains(`${handlerId}-settings`);
+ container.style.display = shouldShow ? 'block' : 'none';
});
}
@@ -586,234 +647,15 @@ class OptionsUIModule extends BaseModule {
/**
* Load user preferences from the persistence manager
+ * This is now handled by the persistence manager's setupBindings method
*/
loadPreferences() {
- const persistenceManager = this.getModule('persistence-manager');
- if (!persistenceManager) return;
-
- console.log('Options UI: Loading preferences');
-
- // TTS Settings
- // TTS Enable
- if (this.elements.ttsEnabled) {
- this.elements.ttsEnabled.checked = this.getPreference('tts', 'enabled', true);
- }
-
- // TTS System
- if (this.elements.ttsSystem) {
- const preferredHandler = this.getPreference('tts', 'preferred_handler', 'none');
- if (this.elements.ttsSystem.querySelector(`option[value="${preferredHandler}"]`)) {
- this.elements.ttsSystem.value = preferredHandler;
- }
- }
-
- // TTS Speed
- if (this.elements.ttsSpeed) {
- const speed = this.getPreference('tts', 'speed', 1);
- this.elements.ttsSpeed.value = Math.round(speed * 100);
- }
-
- // API Keys and URLs
- // ElevenLabs API Key
- if (this.elements.elevenLabsApiKey) {
- this.elements.elevenLabsApiKey.value = this.getPreference('tts', 'elevenlabs-tts_api_key', '');
- }
-
- // ElevenLabs API URL
- if (this.elements.elevenLabsApiUrl) {
- this.elements.elevenLabsApiUrl.value = this.getPreference('tts', 'elevenlabs-tts_api_url', 'https://api.elevenlabs.io/v1');
- }
-
- // OpenAI API Key
- if (this.elements.openaiApiKey) {
- this.elements.openaiApiKey.value = this.getPreference('tts', 'openai-tts_api_key', '');
- }
-
- // OpenAI API URL
- if (this.elements.openaiApiUrl) {
- this.elements.openaiApiUrl.value = this.getPreference('tts', 'openai-tts_api_url', 'https://api.openai.com/v1');
- }
-
- // Audio Settings
- // Master Volume
- if (this.elements.masterVolume) {
- const masterVolume = this.getPreference('audio', 'masterVolume', 1);
- this.elements.masterVolume.value = Math.round(masterVolume * 100);
- }
-
- // Music Volume
- if (this.elements.musicVolume) {
- const musicVolume = this.getPreference('audio', 'musicVolume', 1);
- this.elements.musicVolume.value = Math.round(musicVolume * 100);
- }
-
- // SFX Volume
- if (this.elements.sfxVolume) {
- const sfxVolume = this.getPreference('audio', 'sfxVolume', 1);
- this.elements.sfxVolume.value = Math.round(sfxVolume * 100);
- }
-
- // Ambience Volume
- if (this.elements.ambienceVolume) {
- const ambienceVolume = this.getPreference('audio', 'ambienceVolume', 1);
- this.elements.ambienceVolume.value = Math.round(ambienceVolume * 100);
- }
-
- // Language
- if (this.elements.language) {
- const locale = this.getPreference('app', 'locale', 'en');
- if (this.elements.language.querySelector(`option[value="${locale}"]`)) {
- this.elements.language.value = locale;
- }
- }
-
- // Update API settings visibility
+ // Update API settings visibility based on current TTS system
if (this.elements.ttsSystem) {
this.updateApiSettingsVisibility(this.elements.ttsSystem.value);
}
}
-
- /**
- * Set up two-way binding for TTS Enabled
- * @param {HTMLElement} element - UI element
- * @param {Object} persistenceManager - Persistence Manager module
- * @param {string} category - Preference category
- * @param {string} key - Preference key
- * @param {*} defaultValue - Default value if preference doesn't exist
- * @param {Function} [transform] - Optional transform function
- */
- setupTtsEnabledBinding(element, persistenceManager, category, key, defaultValue, transform) {
- createPreferenceBinding(
- element,
- persistenceManager,
- category,
- key,
- defaultValue,
- transform
- );
- }
-
- /**
- * Set up two-way binding for TTS Voice
- * @param {HTMLElement} element - UI element
- * @param {Object} persistenceManager - Persistence Manager module
- * @param {string} category - Preference category
- * @param {string} key - Preference key
- * @param {*} defaultValue - Default value if preference doesn't exist
- * @param {Function} [transform] - Optional transform function
- */
- setupTtsVoiceBinding(element, persistenceManager, category, key, defaultValue, transform) {
- createPreferenceBinding(
- element,
- persistenceManager,
- category,
- key,
- defaultValue,
- transform
- );
- }
-
- /**
- * Set up two-way binding for App Language
- * @param {HTMLElement} element - UI element
- * @param {Object} persistenceManager - Persistence Manager module
- * @param {string} category - Preference category
- * @param {string} key - Preference key
- * @param {*} defaultValue - Default value if preference doesn't exist
- * @param {Function} [transform] - Optional transform function
- */
- setupLanguageBinding(element, persistenceManager, category, key, defaultValue, transform) {
- createPreferenceBinding(
- element,
- persistenceManager,
- category,
- key,
- defaultValue,
- transform
- );
- }
-
- /**
- * Set up two-way binding for API settings
- * @param {Object} persistenceManager - Persistence Manager module
- */
- setupApiPreferenceBindings(persistenceManager) {
- // ElevenLabs API Key
- createPreferenceBinding(
- this.elements.elevenLabsApiKey,
- persistenceManager,
- 'tts',
- 'elevenlabs-tts_api_key',
- null,
- (value) => {
- this.dispatchApiChangeEvent('api:keyChanged', 'elevenlabs-tts', 'key', value);
- return value;
- }
- );
- // ElevenLabs API URL
- createPreferenceBinding(
- this.elements.elevenLabsApiUrl,
- persistenceManager,
- 'tts',
- 'elevenlabs-tts_api_url',
- null,
- (value) => {
- this.dispatchApiChangeEvent('api:urlChanged', 'elevenlabs-tts', 'url', value);
- return value;
- }
- );
-
- // OpenAI API Key
- createPreferenceBinding(
- this.elements.openaiApiKey,
- persistenceManager,
- 'tts',
- 'openai-tts_api_key',
- null,
- (value) => {
- this.dispatchApiChangeEvent('api:keyChanged', 'openai-tts', 'key', value);
- return value;
- }
- );
-
- // OpenAI API URL
- createPreferenceBinding(
- this.elements.openaiApiUrl,
- persistenceManager,
- 'tts',
- 'openai-tts_api_url',
- null,
- (value) => {
- this.dispatchApiChangeEvent('api:urlChanged', 'openai-tts', 'url', value);
- return value;
- }
- );
- }
-
- /**
- * Save current settings
- */
- saveCurrentSettings() {
- // With two-way binding, settings are saved automatically as they change
- console.log('Options UI: Settings saved');
- }
-
- /**
- * Apply settings
- */
- applySettings() {
- const ttsFactory = this.getModule('tts-factory');
- if (ttsFactory) {
- // Apply TTS settings
- const enabled = this.getPreference('tts', 'enabled', false);
- const preferredHandler = this.getPreference('tts', 'preferred_handler', 'none');
-
- ttsFactory.configure({ enabled });
- ttsFactory.setActiveHandler(preferredHandler);
- }
- }
-
/**
* Show a reload notice
* @param {string} message - Message to show
@@ -827,33 +669,7 @@ class OptionsUIModule extends BaseModule {
* Set up listeners for settings that should save immediately
*/
setupImmediateSaveListeners() {
- // Settings are saved immediately with two-way binding
- }
-
- /**
- * Update UI text based on current language
- */
- updateUIText() {
- // Update UI text based on current language
- const localization = this.getModule('localization');
- if (!localization) return;
-
- // Update modal title
- const modalTitle = this.modal.querySelector('h2');
- if (modalTitle) {
- modalTitle.textContent = localization.translate('options.title', 'Options');
- }
-
- // Update section titles
- const ttsSectionTitle = this.modal.querySelector('.options-section h3:first-child');
- if (ttsSectionTitle) {
- ttsSectionTitle.textContent = localization.translate('options.tts.title', 'Text-to-Speech');
- }
-
- const langSectionTitle = this.modal.querySelector('.options-section:nth-child(2) h3');
- if (langSectionTitle) {
- langSectionTitle.textContent = localization.translate('options.language.title', 'Language Settings');
- }
+ // Settings are saved immediately with two-way binding via data-pref-bind attributes
}
/**
@@ -896,60 +712,106 @@ class OptionsUIModule extends BaseModule {
/**
* Set up the initial state of the Options UI
- * @returns {Promise} - Promise resolves when setup is complete
*/
async setupInitialState() {
- try {
- console.log('Options UI: Setting up initial state');
-
- // Add event listener for toggling options UI
- document.addEventListener('ui:options:toggle', () => this.toggle());
-
- // Set up key bindings
- document.addEventListener('keydown', (e) => {
- if (e.key === 'Escape' && this.modal && this.modal.style.display === 'flex') {
- this.saveCurrentSettings();
- this.hide();
- }
- });
-
- // Populate TTS systems
- await this.populateTtsSystems();
-
- // Populate languages
+ // Add event listener for toggling options UI
+ document.addEventListener('ui:options:toggle', () => this.toggle());
+
+ // Set up key bindings
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && this.modal && this.modal.style.display === 'flex') {
+ this.hide();
+ }
+ });
+
+ // Populate TTS systems selector
+ await this.populateTtsSystems();
+
+ // Populate languages
+ await this.populateLanguages();
+
+ // Populate voices based on current TTS system
+ await this.populateVoices();
+
+ // Register for TTS events to update voices when they change
+ document.addEventListener('tts:voices:updated', () => {
+ console.log('Options UI: Received tts:voices:updated event, updating voice dropdown');
+ this.populateVoices();
+ });
+
+ // Set up language change listener
+ document.addEventListener('locale:changed', async () => {
+ this.updateUIText();
await this.populateLanguages();
-
- // Populate voices based on current TTS system
+ });
+
+ // Register event listeners for TTS availability and voiceId changes
+ document.addEventListener('tts:engine:change', async (event) => {
+ console.log('Options UI: Received TTS engine change event:', event.detail);
await this.populateVoices();
-
- // Load current preferences
- this.loadPreferences();
-
- // Register for TTS events to update voices when they change
- document.addEventListener('tts:voices:updated', () => {
- console.log('Options UI: Received tts:voices:updated event, updating voice dropdown');
- this.populateVoices();
- });
-
- // Set up language change listener
- document.addEventListener('locale:changed', async () => {
- this.updateUIText();
- await this.populateLanguages();
- });
-
- // Register event listeners for TTS availability and voiceId changes
- document.addEventListener('tts:engine:change', async (event) => {
- console.log('Options UI: Received TTS engine change event:', event.detail);
- await this.populateVoices();
- this.updateApiSettingsVisibility(this.elements.ttsSystem.value);
- });
-
- console.log('Options UI: Initial state setup complete');
- return true;
- } catch (error) {
- console.error('Options UI: Error setting up initial state', error);
- return false;
+ this.updateApiSettingsVisibility(this.elements.ttsSystem.value);
+ });
+ }
+
+ /**
+ * Set up two-way bindings for preferences using data attributes
+ */
+ setupPreferenceBindings() {
+ const persistenceManager = this.getModule('persistence-manager');
+ if (!persistenceManager || !persistenceManager.setupBindings) {
+ console.error('Options UI: Cannot set up preference bindings, persistence manager not available or missing setupBindings method');
+ return;
}
+
+ // Setup all bindings in the modal
+ this.bindings = persistenceManager.setupBindings('#options-modal');
+ console.log('Options UI: Preference bindings set up', this.bindings.length);
+
+ // Add event listeners for side effects when preferences change
+ document.addEventListener('preference-updated', (event) => {
+ const { category, key, value } = event.detail;
+
+ // Handle audio settings side effects
+ if (category === 'audio') {
+ const audioManager = this.getModule('audio-manager');
+ if (!audioManager) return;
+
+ if (key === 'masterVolume') {
+ audioManager.setMasterVolume(value);
+ } else if (key === 'musicVolume') {
+ audioManager.setMusicVolume(value);
+ } else if (key === 'sfxVolume') {
+ audioManager.setSfxVolume(value);
+ } else if (key === 'ttsVolume') {
+ audioManager.setTtsVolume(value);
+ }
+ }
+
+ // Handle TTS settings side effects
+ if (category === 'tts') {
+ const ttsFactory = this.getModule('tts-factory');
+ if (!ttsFactory) return;
+
+ if (key === 'preferred_handler') {
+ this.populateVoices();
+ this.updateApiSettingsVisibility(value);
+ } else if (key === 'voice') {
+ ttsFactory.configure({ voice: value });
+ } else if (key === 'speed') {
+ ttsFactory.configure({ speed: value });
+ } else if (key === 'enabled') {
+ ttsFactory.configure({ enabled: value });
+ }
+ }
+
+ // Handle locale changes
+ if (category === 'app' && key === 'locale') {
+ const localization = this.getModule('localization');
+ if (localization) {
+ localization.setLocale(value);
+ }
+ }
+ });
}
}
diff --git a/public/js/persistence-manager-module.js b/public/js/persistence-manager-module.js
index cba6f58..2b25e44 100644
--- a/public/js/persistence-manager-module.js
+++ b/public/js/persistence-manager-module.js
@@ -31,8 +31,12 @@ class PersistenceManagerModule extends BaseModule {
this.defaultPreferences = {
tts: {
enabled: false,
- provider: 'none',
+ preferred_handler: 'none',
voice: '',
+ 'elevenlabs-tts_api_key': '',
+ 'elevenlabs-tts_api_url': 'https://api.elevenlabs.io/v1',
+ 'openai-tts_api_key': '',
+ 'openai-tts_api_url': 'https://api.openai.com/v1'
},
audio: {
masterVolume: 1.0,
@@ -58,7 +62,11 @@ class PersistenceManagerModule extends BaseModule {
'createSaveSlot',
'loadSaveSlot',
'deleteSaveSlot',
- 'getAllSaveSlots'
+ 'getAllSaveSlots',
+ 'createBinding',
+ 'updateElementFromPreference',
+ 'updatePreferenceFromElement',
+ 'setupBindings'
]);
// Remove circular dependency
@@ -246,19 +254,14 @@ class PersistenceManagerModule extends BaseModule {
* @returns {boolean} - Success status
*/
updatePreference(category, setting, value) {
- if (!category || !setting) return false;
+ if (!this.preferences) return false;
- // Ensure preferences are loaded
- if (!this.preferences) {
- this.loadPreferences();
- }
-
- // Create category if it doesn't exist
+ // Ensure category exists
if (!this.preferences[category]) {
this.preferences[category] = {};
}
- // Update preference
+ // Store value
this.preferences[category][setting] = value;
// Save preferences
@@ -268,7 +271,7 @@ class PersistenceManagerModule extends BaseModule {
// Dispatch event
this.dispatchEvent('preference-updated', {
category,
- setting,
+ key: setting,
value,
timestamp: new Date().toISOString()
});
@@ -469,6 +472,201 @@ class PersistenceManagerModule extends BaseModule {
return this.saveSlots;
}
+ /**
+ * Create a binding between a DOM element and a preference
+ * @param {HTMLElement} element - Element to bind to
+ * @param {string} category - Preference category
+ * @param {string} key - Preference key
+ * @param {Object} options - Additional options (transformers, etc)
+ * @returns {Object} - Binding control object
+ */
+ createBinding(element, category, key, options = {}) {
+ if (!element) return null;
+
+ const transformer = options.transformer || {
+ toElement: (value) => value,
+ toPreference: (value) => value
+ };
+
+ // Store binding info on the element
+ element._prefBinding = { category, key, transformer };
+
+ // Set initial value
+ this.updateElementFromPreference(element);
+
+ // Set up event listeners
+ const eventHandler = () => this.updatePreferenceFromElement(element);
+
+ // Choose appropriate events based on element type
+ let events = ['change'];
+ if (element.type !== 'checkbox' && element.type !== 'radio' && element.tagName !== 'SELECT') {
+ events.push('input');
+ }
+
+ // Attach event listeners
+ events.forEach(event => {
+ element.addEventListener(event, eventHandler);
+ });
+
+ // Return control object
+ return {
+ update: () => this.updateElementFromPreference(element),
+ destroy: () => {
+ events.forEach(event => {
+ element.removeEventListener(event, eventHandler);
+ });
+ delete element._prefBinding;
+ }
+ };
+ }
+
+ /**
+ * Update an element value from its bound preference
+ * @param {HTMLElement} element - The bound element
+ */
+ updateElementFromPreference(element) {
+ if (!element || !element._prefBinding) return;
+
+ const { category, key, transformer } = element._prefBinding;
+ const value = this.getPreference(category, key);
+ const transformedValue = transformer.toElement(value);
+
+ // Set element value based on its type
+ if (element.type === 'checkbox') {
+ element.checked = !!transformedValue;
+ } else if (element.type === 'radio') {
+ element.checked = element.value === String(transformedValue);
+ } else if (element.tagName === 'SELECT') {
+ element.value = transformedValue;
+ } else {
+ element.value = transformedValue;
+ }
+ }
+
+ /**
+ * Update a preference from its bound element
+ * @param {HTMLElement} element - The bound element
+ */
+ updatePreferenceFromElement(element) {
+ if (!element || !element._prefBinding) return;
+
+ const { category, key, transformer } = element._prefBinding;
+ let value;
+
+ // Get element value based on its type
+ if (element.type === 'checkbox') {
+ value = element.checked;
+ } else if (element.type === 'radio') {
+ value = element.checked ? element.value : null;
+ } else {
+ value = element.value;
+ }
+
+ const transformedValue = transformer.toPreference(value);
+ this.updatePreference(category, key, transformedValue);
+ }
+
+ /**
+ * Set up bindings for all elements with data-pref-bind attributes
+ * @param {string} rootSelector - Root selector to search within
+ * @returns {Array} - Array of binding control objects
+ */
+ setupBindings(rootSelector = 'body') {
+ const root = document.querySelector(rootSelector);
+ if (!root) return [];
+
+ const bindings = [];
+ const elements = root.querySelectorAll('[data-pref-bind]');
+
+ elements.forEach(element => {
+ const bindingStr = element.dataset.prefBind;
+ if (!bindingStr) return;
+
+ const [category, key] = bindingStr.split('.');
+ if (!category || !key) return;
+
+ // Parse transformer if specified
+ let transformer = {
+ toElement: (value) => value,
+ toPreference: (value) => value
+ };
+
+ // Handle range transformations
+ if (element.type === 'range' && element.hasAttribute('min') && element.hasAttribute('max')) {
+ const min = parseInt(element.getAttribute('min'), 10) || 0;
+ const max = parseInt(element.getAttribute('max'), 10) || 100;
+
+ transformer = {
+ toElement: (value) => {
+ // Convert from 0-1 to min-max
+ return Math.round(value * (max - min) + min);
+ },
+ toPreference: (value) => {
+ // Convert from min-max to 0-1
+ return (parseInt(value, 10) - min) / (max - min);
+ }
+ };
+ }
+
+ // Custom transformer via data attribute
+ if (element.dataset.prefTransform) {
+ try {
+ // Check if it's a range transformer in format 'range:min,max'
+ if (element.dataset.prefTransform.startsWith('range:')) {
+ const rangeValues = element.dataset.prefTransform.substring(6).split(',');
+ if (rangeValues.length === 2) {
+ const min = parseFloat(rangeValues[0]);
+ const max = parseFloat(rangeValues[1]);
+
+ if (!isNaN(min) && !isNaN(max)) {
+ transformer = {
+ toElement: (value) => {
+ // Convert from min-max to 0-100 for the slider
+ return Math.round(((value - min) / (max - min)) * 100);
+ },
+ toPreference: (value) => {
+ // Convert from 0-100 to min-max
+ return min + (parseInt(value, 10) / 100) * (max - min);
+ }
+ };
+ }
+ }
+ } else {
+ // Try to parse as JSON for backward compatibility
+ const customTransformer = JSON.parse(element.dataset.prefTransform);
+ if (customTransformer && typeof customTransformer === 'object') {
+ transformer = customTransformer;
+ }
+ }
+ } catch (e) {
+ console.warn('Invalid transformer data attribute', e);
+ }
+ }
+
+ const binding = this.createBinding(element, category, key, { transformer });
+ if (binding) {
+ bindings.push(binding);
+ }
+ });
+
+ // Set up event listener for preference changes from other sources
+ document.addEventListener('preference-updated', (event) => {
+ const { category, key } = event.detail;
+
+ // Update any matching elements
+ elements.forEach(element => {
+ if (!element._prefBinding) return;
+
+ if (element._prefBinding.category === category &&
+ element._prefBinding.key === key) {
+ this.updateElementFromPreference(element);
+ }
+ });
+ });
+
+ return bindings;
+ }
+
/**
* Clean up when module is disposed
*/
diff --git a/public/js/tts-factory-module.js b/public/js/tts-factory-module.js
index ac36c58..e95c775 100644
--- a/public/js/tts-factory-module.js
+++ b/public/js/tts-factory-module.js
@@ -676,9 +676,9 @@ class TTSFactoryModule extends BaseModule {
* Speak text using the active TTS handler
* @param {string} text - Text to speak
* @param {Object} options - TTS options
- * @returns {boolean} - Success status
+ * @returns {Promise} - Success status
*/
- speak(text, options = {}) {
+ async speak(text, options = {}) {
// Check if we have an active handler
if (!this.activeHandler) {
console.warn('TTS Factory: No active handler set');
@@ -705,7 +705,39 @@ class TTSFactoryModule extends BaseModule {
effectiveOptions.speed = this.speed;
}
- // Call the handler's speak method
+ // Check if we have this speech cached
+ const hash = await this.generateSpeechHash(text);
+ const cached = await this.getCachedSpeech(hash);
+
+ if (cached && cached.success) {
+ console.log(`TTS Factory: Using cached speech for hash ${hash}`);
+ // Use cached speech
+ return handler.speakPreloaded(cached, result => {
+ document.dispatchEvent(new CustomEvent('tts:speechCompleted', {
+ detail: { success: result?.success === true, error: result?.error }
+ }));
+ });
+ }
+
+ // Not cached, generate and cache
+ if (typeof handler.preloadSpeech === 'function') {
+ console.log(`TTS Factory: Generating and caching speech for hash ${hash}`);
+ const preloadData = await handler.preloadSpeech(text);
+ if (preloadData && preloadData.success) {
+ // Cache the speech
+ await this.cacheSpeech(hash, preloadData);
+
+ // Speak the preloaded speech
+ return handler.speakPreloaded(preloadData, result => {
+ document.dispatchEvent(new CustomEvent('tts:speechCompleted', {
+ detail: { success: result?.success === true, error: result?.error }
+ }));
+ });
+ }
+ }
+
+ // Fallback to direct speak if preloading failed or not supported
+ console.log(`TTS Factory: Falling back to direct speak (no caching)`);
return handler.speak(text, result => {
// Forward speech completion event
document.dispatchEvent(new CustomEvent('tts:speechCompleted', {
diff --git a/public/kokoro-loader.html b/public/kokoro-loader.html
index 436c58d..7f9c7aa 100644
--- a/public/kokoro-loader.html
+++ b/public/kokoro-loader.html
@@ -95,8 +95,8 @@
// Initialize the model
const model_id = "onnx-community/Kokoro-82M-v1.0-ONNX";
this.instance = await this.kokoroTTS.from_pretrained(model_id, {
- dtype: "q8", // Use quantized model for better performance
- device: "webgpu", // Use WebGL for better performance
+ dtype: "q8",
+ device: "webgpu",
progress_callback: (progress) => {
// Skip progress updates if progress is NaN/undefined (cache loading)
if (progress === undefined || isNaN(progress)) {
diff --git a/public/templates/options-modal.html b/public/templates/options-modal.html
new file mode 100644
index 0000000..3e39f18
--- /dev/null
+++ b/public/templates/options-modal.html
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
Text-to-Speech
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 100%
+
+
+
+
+
+
+
+
ElevenLabs API Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
OpenAI API Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Language
+
+
+
+
+
+
+
+
+
+
+