939 lines
34 KiB
JavaScript
939 lines
34 KiB
JavaScript
/**
|
|
* Options UI Module
|
|
* Provides the options UI for the game
|
|
*/
|
|
import { BaseModule } from './base-module.js';
|
|
import { createUIElement, populateDropdown, registerHandler, createPreferenceBinding } from './ui-helper.js';
|
|
|
|
class OptionsUIModule extends BaseModule {
|
|
/**
|
|
* Create a new options UI module
|
|
*/
|
|
constructor() {
|
|
super('options-ui', 'Options UI');
|
|
|
|
// Set up dependencies
|
|
this.dependencies = [
|
|
'persistence-manager',
|
|
'localization',
|
|
'tts-factory',
|
|
'audio-manager'
|
|
];
|
|
|
|
// Modal element
|
|
this.modal = null;
|
|
|
|
// UI elements
|
|
this.elements = {};
|
|
|
|
// Settings that require reload
|
|
this.reloadRequired = false;
|
|
|
|
// Bind methods
|
|
this.bindMethods([
|
|
'show',
|
|
'hide',
|
|
'createModal',
|
|
'populateTtsSystems',
|
|
'populateVoices',
|
|
'populateLanguages',
|
|
'loadPreferences',
|
|
'showReloadNotice',
|
|
'toggle',
|
|
'setupEventListeners',
|
|
'setupApiUrlFields',
|
|
'setupInitialState',
|
|
'dispatchApiChangeEvent',
|
|
'getPreference',
|
|
'updatePreference',
|
|
'renderProviderStatuses'
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Dispatches an API change event
|
|
* @param {string} eventType - Event type (e.g. 'api:key:change')
|
|
* @param {string} provider - Provider name (e.g. 'elevenlabs')
|
|
* @param {string} valueType - Value type (e.g. 'key', 'url')
|
|
* @param {string} value - Value to dispatch
|
|
*/
|
|
dispatchApiChangeEvent(eventType, provider, valueType, value) {
|
|
const eventName = `tts:${eventType}`;
|
|
console.log(`Options UI: Dispatching event ${eventName} for provider ${provider}`);
|
|
document.dispatchEvent(new CustomEvent(eventName, {
|
|
detail: { provider, [valueType]: value }
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Gets a preference from the persistence manager
|
|
* @param {string} category - Preference category
|
|
* @param {string} key - Preference key
|
|
* @param {*} defaultValue - Default value if preference doesn't exist
|
|
* @returns {*} - Preference value
|
|
*/
|
|
getPreference(category, key, defaultValue) {
|
|
const persistenceManager = this.getModule('persistence-manager');
|
|
return persistenceManager.getPreference(category, key, defaultValue);
|
|
}
|
|
|
|
/**
|
|
* Updates a preference in the persistence manager
|
|
* @param {string} category - Preference category
|
|
* @param {string} key - Preference key
|
|
* @param {*} value - Value to set
|
|
*/
|
|
updatePreference(category, key, value) {
|
|
const persistenceManager = this.getModule('persistence-manager');
|
|
persistenceManager.updatePreference(category, key, value);
|
|
}
|
|
|
|
/**
|
|
* Initialize the Options UI module
|
|
* @returns {Promise<boolean>} - Promise resolves with initialization success
|
|
*/
|
|
async initialize() {
|
|
console.log('Options UI: Initializing');
|
|
|
|
// Create DOM elements
|
|
this.createModal();
|
|
|
|
// Set up event listeners
|
|
this.setupEventListeners();
|
|
|
|
// Set up API URL fields
|
|
this.setupApiUrlFields();
|
|
|
|
// Set up initial state
|
|
await this.setupInitialState();
|
|
|
|
// Set up automatic bindings using the persistence manager
|
|
this.setupPreferenceBindings();
|
|
|
|
this.reportProgress(100, 'Options UI initialized');
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Create the options modal
|
|
*/
|
|
createModal() {
|
|
console.log('Options UI: Creating options modal');
|
|
|
|
// Create modal container
|
|
this.modal = document.createElement('div');
|
|
this.modal.id = 'options-modal';
|
|
this.modal.className = 'modal';
|
|
this.modal.style.display = 'none';
|
|
|
|
// Create modal content
|
|
const modalContent = document.createElement('div');
|
|
modalContent.className = 'modal-content';
|
|
|
|
// Create header
|
|
const header = document.createElement('div');
|
|
header.className = 'modal-header';
|
|
|
|
const title = document.createElement('h2');
|
|
title.textContent = 'Options';
|
|
header.appendChild(title);
|
|
|
|
const closeButton = document.createElement('span');
|
|
closeButton.className = 'close';
|
|
closeButton.innerHTML = '×';
|
|
closeButton.onclick = () => this.hide();
|
|
header.appendChild(closeButton);
|
|
|
|
modalContent.appendChild(header);
|
|
|
|
// Create body
|
|
const body = document.createElement('div');
|
|
body.className = 'modal-body';
|
|
|
|
// Create sections
|
|
// App Settings Section (Language and Speed)
|
|
const appSettingsSection = document.createElement('div');
|
|
appSettingsSection.className = 'options-section';
|
|
|
|
const appSettingsTitle = document.createElement('h3');
|
|
appSettingsTitle.textContent = 'Application Settings';
|
|
appSettingsSection.appendChild(appSettingsTitle);
|
|
|
|
// 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%';
|
|
this.elements.ttsSpeedValue = speedValue;
|
|
speedContainer.appendChild(speedValue);
|
|
|
|
this.elements.ttsSpeed = createUIElement('input', {
|
|
type: 'range',
|
|
min: 50,
|
|
max: 150,
|
|
value: 100,
|
|
'data-pref-bind': 'tts.speed',
|
|
'data-pref-transform': 'centered-speed'
|
|
}, null, speedContainer);
|
|
|
|
// Update displayed value when slider changes
|
|
this.elements.ttsSpeed.addEventListener('input', () => {
|
|
this.updateSpeedDisplay();
|
|
});
|
|
|
|
appSettingsSection.appendChild(speedContainer);
|
|
|
|
body.appendChild(appSettingsSection);
|
|
|
|
// TTS Section
|
|
const ttsSection = document.createElement('div');
|
|
ttsSection.className = 'options-section';
|
|
|
|
const ttsTitle = document.createElement('h3');
|
|
ttsTitle.textContent = 'Text-to-Speech';
|
|
ttsSection.appendChild(ttsTitle);
|
|
|
|
// 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);
|
|
|
|
const providerStatusContainer = document.createElement('div');
|
|
providerStatusContainer.className = 'provider-status-list';
|
|
this.elements.providerStatus = providerStatusContainer;
|
|
ttsSection.appendChild(providerStatusContainer);
|
|
|
|
// 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);
|
|
|
|
// Master 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
|
|
const musicVolumeContainer = document.createElement('div');
|
|
musicVolumeContainer.className = 'option-item';
|
|
|
|
const musicVolumeLabel = document.createElement('label');
|
|
musicVolumeLabel.textContent = 'Music Volume:';
|
|
musicVolumeContainer.appendChild(musicVolumeLabel);
|
|
|
|
const musicVolumeValue = document.createElement('span');
|
|
musicVolumeValue.className = 'slider-value';
|
|
musicVolumeValue.textContent = '100%';
|
|
musicVolumeContainer.appendChild(musicVolumeValue);
|
|
|
|
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);
|
|
|
|
// Update displayed value when slider changes
|
|
this.elements.musicVolume.addEventListener('input', () => {
|
|
musicVolumeValue.textContent = `${this.elements.musicVolume.value}%`;
|
|
});
|
|
|
|
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);
|
|
const ttsFactory = this.getModule('tts-factory');
|
|
if (ttsFactory) {
|
|
await ttsFactory.refreshHandlerStatus(event.target.value);
|
|
}
|
|
await this.populateVoices();
|
|
this.renderProviderStatuses();
|
|
});
|
|
}
|
|
|
|
// 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';
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Show the options UI
|
|
*/
|
|
show() {
|
|
if (this.modal) {
|
|
this.modal.style.display = 'flex';
|
|
document.body.classList.add('modal-open');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hide the options UI
|
|
*/
|
|
hide() {
|
|
if (this.modal) {
|
|
this.modal.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle the options UI visibility
|
|
*/
|
|
toggle() {
|
|
if (this.modal) {
|
|
if (this.modal.style.display === 'flex') {
|
|
this.hide();
|
|
} else {
|
|
this.show();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Populate the TTS systems dropdown
|
|
*/
|
|
async populateTtsSystems() {
|
|
const ttsFactory = this.getModule('tts-factory');
|
|
if (!ttsFactory || !this.elements.ttsSystem) return;
|
|
|
|
// Get available TTS systems
|
|
const handlers = ttsFactory.getAvailableHandlers();
|
|
console.log('Options UI: Available TTS handlers:', handlers);
|
|
|
|
// Format for display
|
|
const systems = handlers.map(handler => ({
|
|
id: handler.id,
|
|
name: handler.displayName || handler.id
|
|
}));
|
|
|
|
// Populate dropdown
|
|
populateDropdown(
|
|
this.elements.ttsSystem,
|
|
systems,
|
|
'id',
|
|
'name',
|
|
this.getPreference('tts', 'preferred_handler', 'none')
|
|
);
|
|
|
|
// Update API settings visibility
|
|
this.updateApiSettingsVisibility(this.elements.ttsSystem.value);
|
|
this.renderProviderStatuses();
|
|
}
|
|
|
|
/**
|
|
* Populate the voices dropdown
|
|
*/
|
|
async populateVoices() {
|
|
const ttsFactory = this.getModule('tts-factory');
|
|
if (!ttsFactory || !this.elements.ttsVoice) return;
|
|
|
|
const selectedHandler = this.elements.ttsSystem?.value || this.getPreference('tts', 'preferred_handler', 'none');
|
|
const voices = typeof ttsFactory.getVoicesForHandler === 'function'
|
|
? await ttsFactory.getVoicesForHandler(selectedHandler) || []
|
|
: await ttsFactory.getVoices() || [];
|
|
console.log('Options UI: TTS voices:', voices);
|
|
|
|
// Populate dropdown
|
|
populateDropdown(
|
|
this.elements.ttsVoice,
|
|
voices,
|
|
'id',
|
|
'name',
|
|
this.getPreference('tts', `${selectedHandler}_voice`, this.getPreference('tts', 'voice', ''))
|
|
);
|
|
}
|
|
|
|
renderProviderStatuses() {
|
|
const container = this.elements.providerStatus;
|
|
const ttsFactory = this.getModule('tts-factory');
|
|
if (!container || !ttsFactory || typeof ttsFactory.getHandlerStatuses !== 'function') {
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = '';
|
|
const statuses = ttsFactory.getHandlerStatuses();
|
|
statuses.forEach(status => {
|
|
const row = document.createElement('div');
|
|
row.className = 'provider-status-row';
|
|
|
|
const name = document.createElement('span');
|
|
name.textContent = status.name;
|
|
row.appendChild(name);
|
|
|
|
const value = document.createElement('span');
|
|
value.className = 'provider-status-value';
|
|
value.textContent = `${status.ready ? 'ready' : 'not ready'} - ${status.message}`;
|
|
row.appendChild(value);
|
|
|
|
container.appendChild(row);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Populate the languages dropdown
|
|
*/
|
|
async populateLanguages() {
|
|
const localization = this.getModule('localization');
|
|
if (!localization || !this.elements.language) return;
|
|
|
|
// Get available languages
|
|
const languages = localization.getAvailableLocales() || [];
|
|
console.log('Options UI: Available languages:', languages);
|
|
|
|
// Format languages with their names
|
|
const languageOptions = languages.map(code => ({
|
|
code,
|
|
name: localization.getLanguageName(code)
|
|
}));
|
|
|
|
// Populate dropdown
|
|
populateDropdown(
|
|
this.elements.language,
|
|
languageOptions,
|
|
'code',
|
|
'name',
|
|
this.getPreference('app', 'locale', 'en-us')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Load user preferences from the persistence manager
|
|
* This is now handled by the persistence manager's setupBindings method
|
|
*/
|
|
loadPreferences() {
|
|
// Update API settings visibility based on current TTS system
|
|
if (this.elements.ttsSystem) {
|
|
this.updateApiSettingsVisibility(this.elements.ttsSystem.value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show a reload notice
|
|
* @param {string} message - Message to show
|
|
*/
|
|
showReloadNotice(message) {
|
|
console.log('Options UI: Reload required -', message);
|
|
this.reloadRequired = true;
|
|
}
|
|
|
|
/**
|
|
* Set up listeners for settings that should save immediately
|
|
*/
|
|
setupImmediateSaveListeners() {
|
|
// Settings are saved immediately with two-way binding via data-pref-bind attributes
|
|
}
|
|
|
|
/**
|
|
* Set up API URL fields with default values
|
|
*/
|
|
setupApiUrlFields() {
|
|
// Set up ElevenLabs API URL
|
|
if (this.elements.elevenLabsApiUrl) {
|
|
const savedUrl = this.getPreference('tts', 'elevenlabs-tts_api_url');
|
|
const defaultUrl = 'https://api.elevenlabs.io/v1';
|
|
|
|
// If no saved URL, set the default
|
|
if (!savedUrl) {
|
|
console.log('Options UI: Setting default ElevenLabs API URL:', defaultUrl);
|
|
this.updatePreference('tts', 'elevenlabs-tts_api_url', defaultUrl);
|
|
}
|
|
}
|
|
|
|
// Set up OpenAI API URL
|
|
if (this.elements.openaiApiUrl) {
|
|
const savedUrl = this.getPreference('tts', 'openai-tts_api_url');
|
|
const defaultUrl = 'https://api.openai.com/v1';
|
|
|
|
// If no saved URL, set the default
|
|
if (!savedUrl) {
|
|
console.log('Options UI: Setting default OpenAI API URL:', defaultUrl);
|
|
this.updatePreference('tts', 'openai-tts_api_url', defaultUrl);
|
|
}
|
|
}
|
|
|
|
// Make sure API keys are initialized if not already set
|
|
if (!this.getPreference('tts', 'elevenlabs-tts_api_key')) {
|
|
this.updatePreference('tts', 'elevenlabs-tts_api_key', '');
|
|
}
|
|
|
|
if (!this.getPreference('tts', 'openai-tts_api_key')) {
|
|
this.updatePreference('tts', 'openai-tts_api_key', '');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set up the initial state of the Options UI
|
|
*/
|
|
async setupInitialState() {
|
|
// 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();
|
|
this.renderProviderStatuses();
|
|
});
|
|
|
|
// 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);
|
|
this.renderProviderStatuses();
|
|
});
|
|
|
|
document.addEventListener('tts:status:updated', () => {
|
|
this.renderProviderStatuses();
|
|
});
|
|
|
|
document.addEventListener('tts:enabled:change', async (event) => {
|
|
if (!event.detail || typeof event.detail.enabled !== 'boolean') {
|
|
return;
|
|
}
|
|
|
|
if (this.elements.ttsEnabled) {
|
|
this.elements.ttsEnabled.checked = event.detail.enabled;
|
|
}
|
|
|
|
const ttsFactory = this.getModule('tts-factory');
|
|
if (!ttsFactory) {
|
|
return;
|
|
}
|
|
|
|
if (event.detail.enabled) {
|
|
const preferredHandler = this.getPreference('tts', 'preferred_handler', 'none');
|
|
if (preferredHandler !== 'none') {
|
|
await ttsFactory.setActiveHandler(preferredHandler);
|
|
}
|
|
} else {
|
|
await ttsFactory.disableAfterCurrentPlayback();
|
|
}
|
|
this.renderProviderStatuses();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
this.updateSpeedDisplay();
|
|
|
|
// 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') {
|
|
const enabled = this.getPreference('tts', 'enabled', false);
|
|
const activation = enabled && value !== 'none'
|
|
? ttsFactory.setActiveHandler(value)
|
|
: Promise.resolve(ttsFactory.disableAfterCurrentPlayback());
|
|
activation.then(() => {
|
|
this.populateVoices();
|
|
this.renderProviderStatuses();
|
|
});
|
|
this.updateApiSettingsVisibility(value);
|
|
} else if (key === 'voice') {
|
|
ttsFactory.configure({ voice: value });
|
|
} else if (key === 'speed') {
|
|
ttsFactory.configure({ speed: value });
|
|
} else if (key === 'language') {
|
|
ttsFactory.configure({ language: value });
|
|
} else if (key === 'enabled') {
|
|
if (!value) {
|
|
ttsFactory.disableAfterCurrentPlayback();
|
|
} else {
|
|
const preferredHandler = this.getPreference('tts', 'preferred_handler', 'none');
|
|
if (preferredHandler !== 'none') {
|
|
ttsFactory.setActiveHandler(preferredHandler);
|
|
}
|
|
}
|
|
document.dispatchEvent(new CustomEvent('tts:enabled:change', {
|
|
detail: { enabled: value }
|
|
}));
|
|
} else if (key.endsWith('_api_key')) {
|
|
const provider = key.replace('_api_key', '');
|
|
this.dispatchApiChangeEvent('api:keyChanged', provider, 'key', value);
|
|
ttsFactory.refreshHandlerStatus(provider).then(() => this.renderProviderStatuses());
|
|
} else if (key.endsWith('_api_url')) {
|
|
const provider = key.replace('_api_url', '');
|
|
this.dispatchApiChangeEvent('api:urlChanged', provider, 'url', value);
|
|
ttsFactory.refreshHandlerStatus(provider).then(() => this.renderProviderStatuses());
|
|
}
|
|
if (key === 'speed' && this.elements.ttsSpeed) {
|
|
this.updateSpeedDisplay();
|
|
}
|
|
}
|
|
|
|
// Handle locale changes
|
|
if (category === 'app' && key === 'locale') {
|
|
const localization = this.getModule('localization');
|
|
if (localization) {
|
|
localization.setLocale(value);
|
|
}
|
|
const ttsFactory = this.getModule('tts-factory');
|
|
if (ttsFactory) {
|
|
ttsFactory.configure({ language: value });
|
|
}
|
|
this.updatePreference('tts', 'language', value);
|
|
}
|
|
});
|
|
}
|
|
|
|
updateSpeedDisplay() {
|
|
if (!this.elements.ttsSpeed || !this.elements.ttsSpeedValue) {
|
|
return;
|
|
}
|
|
|
|
this.elements.ttsSpeedValue.textContent = `${this.elements.ttsSpeed.value}%`;
|
|
}
|
|
}
|
|
|
|
// Create the singleton instance
|
|
const OptionsUI = new OptionsUIModule();
|
|
|
|
// Export the module
|
|
export { OptionsUI };
|