Split everything up into dynamically loaded modules.
This commit is contained in:
+418
-414
@@ -1,441 +1,445 @@
|
||||
/**
|
||||
* UiController Module
|
||||
* Manages user interface interactions and updates UI elements.
|
||||
*/
|
||||
export class UiController {
|
||||
/**
|
||||
* Create a new UiController
|
||||
* @param {Object} config - Configuration options
|
||||
* @param {Object} config.animationQueue - The AnimationQueue instance
|
||||
* @param {Object} config.ttsPlayer - The TtsPlayer instance
|
||||
* @param {Object} config.inputHandler - The InputHandler instance
|
||||
* @param {Object} config.socketClient - The SocketClient instance (or rely on callbacks)
|
||||
* @param {HTMLElement} config.commandHistoryContainerElement - The command history container
|
||||
* @param {HTMLElement} config.storyContainerElement - The story container
|
||||
* @param {HTMLElement} config.speedSliderElement - The speed slider element
|
||||
* @param {HTMLElement} config.rewindButtonElement - The rewind button element
|
||||
* @param {HTMLElement} config.saveButtonElement - The save button element
|
||||
* @param {HTMLElement} config.loadButtonElement - The load button element
|
||||
* @param {HTMLElement} config.speechButtonElement - The speech button element
|
||||
* @param {HTMLElement} config.speedResetElement - The speed reset button element
|
||||
* @param {Object} config.translations - Translations object
|
||||
* @param {string} config.locale - Locale string
|
||||
*/
|
||||
constructor(config = {}) {
|
||||
// Store dependencies
|
||||
this.animationQueue = config.animationQueue;
|
||||
this.ttsPlayer = config.ttsPlayer; // Handles enabling/disabling TTS via its own logic
|
||||
this.inputHandler = config.inputHandler; // Needed for focus, suggestions?
|
||||
this.socketClient = config.socketClient; // Direct access or use callbacks
|
||||
import { BaseModule } from './base-module.js';
|
||||
import { moduleRegistry } from './module-registry.js';
|
||||
import { ModuleEvent } from './base-module.js';
|
||||
|
||||
// Callbacks for actions (to be set by AnimatedFiction)
|
||||
this.onRestartRequest = null;
|
||||
this.onSaveRequest = null;
|
||||
this.onLoadRequest = null;
|
||||
|
||||
// Active TTS handler (set via setTtsHandler)
|
||||
class UIController extends BaseModule {
|
||||
constructor() {
|
||||
super('ui-controller');
|
||||
|
||||
// Declare dependencies on TTS, animation-queue, and our new UI modules
|
||||
this.dependencies = ['tts', 'animation-queue', 'ui-display-handler', 'ui-input-handler', 'ui-effects'];
|
||||
|
||||
// References to sub-modules
|
||||
this.displayHandler = null;
|
||||
this.inputHandler = null;
|
||||
this.effects = null;
|
||||
|
||||
// UI state
|
||||
this.isReady = false;
|
||||
this.isVisible = false;
|
||||
|
||||
// Book interface elements
|
||||
this.bookElement = null;
|
||||
this.leftPage = null;
|
||||
this.rightPage = null;
|
||||
this.storyElement = null;
|
||||
|
||||
// Additional module references
|
||||
this.textBuffer = null;
|
||||
this.ttsHandler = null;
|
||||
this.socketClient = null;
|
||||
this.animationQueue = null;
|
||||
|
||||
// Add TTS toggle state
|
||||
this.ttsEnabled = false;
|
||||
|
||||
// Bind methods that use 'this' internally or are used as callbacks/event handlers
|
||||
this.initialize = this.initialize.bind(this); // Bind initialize as it calls dispatchEvent
|
||||
this.handleCommand = this.handleCommand.bind(this); // Bind event handler
|
||||
this.displayText = this.displayText.bind(this); // Bind if passed as callback
|
||||
this.setupBookInterface = this.setupBookInterface.bind(this);
|
||||
this.applyBookSizing = this.applyBookSizing.bind(this);
|
||||
this.setupEventListeners = this.setupEventListeners.bind(this);
|
||||
this.setupMainUI = this.setupMainUI.bind(this);
|
||||
this.initializeTextBuffer = this.initializeTextBuffer.bind(this);
|
||||
this.showUI = this.showUI.bind(this);
|
||||
this.hideUI = this.hideUI.bind(this);
|
||||
this.clearDisplay = this.clearDisplay.bind(this);
|
||||
this.sendCommand = this.sendCommand.bind(this);
|
||||
this.updateButtonStates = this.updateButtonStates.bind(this);
|
||||
|
||||
// UI elements
|
||||
this.speedSlider = config.speedSliderElement || document.getElementById('speed');
|
||||
this.commandHistoryContainer = config.commandHistoryContainerElement; // Added
|
||||
this.storyContainer = config.storyContainerElement; // Added
|
||||
this.rewindButton = config.rewindButtonElement || document.getElementById('rewind');
|
||||
this.saveButton = config.saveButtonElement || document.getElementById('save');
|
||||
this.loadButton = config.loadButtonElement || document.getElementById('reload');
|
||||
this.speechButton = config.speechButtonElement || document.getElementById('speech');
|
||||
this.speedReset = config.speedResetElement || document.getElementById('speed_reset');
|
||||
|
||||
// Translations
|
||||
this.translations = config.translations || {};
|
||||
this.locale = config.locale || 'en-us';
|
||||
|
||||
// Initial UI state
|
||||
this.updateButtonStates({ started: false, canLoad: false }); // Start with buttons disabled
|
||||
this.updateSpeechButtonAvailability(false); // Start with speech disabled
|
||||
// Store a bound version of dispatchEvent for use in methods
|
||||
this._dispatchModuleEvent = (name, detail) => {
|
||||
document.dispatchEvent(new CustomEvent(name, {
|
||||
detail: { moduleId: this.id, ...detail },
|
||||
bubbles: true
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up event listeners
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// Speed slider
|
||||
if (this.speedSlider) {
|
||||
this.speedSlider.addEventListener('input', this.handleSpeedChange.bind(this));
|
||||
}
|
||||
async initialize() {
|
||||
this.reportProgress(0, 'Initializing UI Controller');
|
||||
|
||||
// Speed reset button
|
||||
if (this.speedReset) {
|
||||
this.speedReset.addEventListener('click', this.handleSpeedReset.bind(this));
|
||||
}
|
||||
|
||||
// Rewind button
|
||||
if (this.rewindButton) {
|
||||
this.rewindButton.addEventListener('click', this.handleRewindClick.bind(this));
|
||||
}
|
||||
|
||||
// Save button
|
||||
if (this.saveButton) {
|
||||
this.saveButton.addEventListener('click', this.handleSaveClick.bind(this));
|
||||
}
|
||||
|
||||
// Load button
|
||||
if (this.loadButton) {
|
||||
this.loadButton.addEventListener('click', this.handleLoadClick.bind(this));
|
||||
}
|
||||
|
||||
// Speech button
|
||||
if (this.speechButton) {
|
||||
this.speechButton.addEventListener('click', this.handleSpeechToggle.bind(this));
|
||||
}
|
||||
|
||||
// Fast forward (spacebar or click on right page)
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.code === 'Space') {
|
||||
this.handleFastForward();
|
||||
try {
|
||||
this.reportProgress(20, 'Setting up book interface');
|
||||
|
||||
// Set up book interface
|
||||
this.setupBookInterface();
|
||||
|
||||
this.reportProgress(30, 'Setting up UI components');
|
||||
|
||||
// Get module references
|
||||
this.displayHandler = moduleRegistry.getModule('ui-display-handler');
|
||||
this.inputHandler = moduleRegistry.getModule('ui-input-handler');
|
||||
this.effects = moduleRegistry.getModule('ui-effects');
|
||||
|
||||
// Get additional dependencies
|
||||
this.textBuffer = moduleRegistry.getModule('text-buffer');
|
||||
this.ttsHandler = moduleRegistry.getModule('tts');
|
||||
this.socketClient = moduleRegistry.getModule('socket-client');
|
||||
this.animationQueue = moduleRegistry.getModule('animation-queue');
|
||||
|
||||
if (!this.displayHandler || !this.inputHandler || !this.effects) {
|
||||
console.error('UI Controller: Required UI modules not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.reportProgress(50, 'Setting up event listeners');
|
||||
|
||||
// Set up event listeners between components
|
||||
this.setupEventListeners();
|
||||
|
||||
this.reportProgress(80, 'Finalizing UI initialization');
|
||||
|
||||
// Initialize main UI container
|
||||
await this.setupMainUI();
|
||||
|
||||
// Initialize text buffer handler
|
||||
this.initializeTextBuffer();
|
||||
|
||||
this.isReady = true;
|
||||
this.isVisible = true;
|
||||
this.reportProgress(100, 'UI Controller ready');
|
||||
|
||||
// Start ambient effects
|
||||
this.effects.startAmbientEffects();
|
||||
|
||||
// Use the DOM event API directly instead of this.dispatchEvent
|
||||
this._dispatchModuleEvent('ui:ready', { controller: this });
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error initializing UI Controller:', error);
|
||||
this.changeState('ERROR');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
setupBookInterface() {
|
||||
// Create or get the book interface elements
|
||||
this.bookElement = document.getElementById('book');
|
||||
this.leftPage = document.getElementById('page_left');
|
||||
this.rightPage = document.getElementById('page_right');
|
||||
this.storyElement = document.getElementById('story');
|
||||
|
||||
// Apply book sizing based on viewport
|
||||
this.applyBookSizing();
|
||||
|
||||
// Set up window resize handler
|
||||
window.addEventListener('resize', () => this.applyBookSizing());
|
||||
}
|
||||
|
||||
applyBookSizing() {
|
||||
// Apply book sizing based on viewport dimensions
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const aspectRatio = viewportWidth / viewportHeight;
|
||||
|
||||
document.documentElement.style.setProperty('--viewport-aspect-ratio', aspectRatio);
|
||||
|
||||
const maxBookHeight = viewportHeight * 0.9;
|
||||
document.documentElement.style.setProperty('--book-height', `${maxBookHeight}px`);
|
||||
|
||||
const bookWidth = maxBookHeight * Math.min(aspectRatio, 1.613);
|
||||
document.documentElement.style.setProperty('--book-width', `${bookWidth}px`);
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Listen for command events from input handler - use arrow function to preserve context
|
||||
document.addEventListener('ui:command', (event) => {
|
||||
this.handleCommand(event.detail);
|
||||
});
|
||||
|
||||
document.getElementById('page_right')?.addEventListener('click', this.handleFastForward.bind(this));
|
||||
// Listen for text display events - use arrow function to preserve context
|
||||
document.addEventListener('ui:text:complete', () => {
|
||||
// Use the DOM event API directly
|
||||
this._dispatchModuleEvent('ui:ready:for:next', {});
|
||||
});
|
||||
|
||||
// Window resize
|
||||
window.addEventListener('resize', this.handleWindowResize.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle speed slider change
|
||||
* @param {Event} event - The input event
|
||||
*/
|
||||
handleSpeedChange(event) {
|
||||
if (!this.animationQueue) return;
|
||||
// Listen for socket connection events
|
||||
document.addEventListener('socket:connected', () => {
|
||||
console.log('UI Controller: Socket connected');
|
||||
});
|
||||
|
||||
const value = parseFloat(event.target.value);
|
||||
const speed = Math.pow(100.0 - value, 3) / 10000 * 10 + 0.01;
|
||||
this.animationQueue.setSpeed(speed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle speed reset button click
|
||||
*/
|
||||
handleSpeedReset() {
|
||||
if (!this.speedSlider || !this.animationQueue) return;
|
||||
document.addEventListener('socket:disconnected', () => {
|
||||
console.log('UI Controller: Socket disconnected');
|
||||
});
|
||||
|
||||
this.speedSlider.value = 50;
|
||||
const speed = Math.pow(100.0 - 50, 3) / 10000 * 10 + 0.01;
|
||||
this.animationQueue.setSpeed(speed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle rewind button click
|
||||
*/
|
||||
handleRewindClick() {
|
||||
if (this.rewindButton.getAttribute('disabled') === 'disabled') {
|
||||
return;
|
||||
// Handle speed reset
|
||||
const speedReset = document.getElementById('speed_reset');
|
||||
if (speedReset) {
|
||||
speedReset.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const speedSlider = document.getElementById('speed');
|
||||
if (speedSlider) {
|
||||
speedSlider.value = 50;
|
||||
if (this.animationQueue) {
|
||||
this.animationQueue.setSpeed(1.0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Use localized confirm message if available
|
||||
const confirmMsg = this.translations[this.locale]?.confirm_restart || 'Are you sure you want to restart the game? All progress will be lost.';
|
||||
if (confirm(confirmMsg)) {
|
||||
if (this.onRestartRequest) {
|
||||
this.onRestartRequest();
|
||||
} else {
|
||||
console.warn("UiController: onRestartRequest callback not set.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle save button click
|
||||
*/
|
||||
handleSaveClick() {
|
||||
if (this.saveButton.getAttribute('disabled') === 'disabled') {
|
||||
return;
|
||||
}
|
||||
if (this.onSaveRequest) {
|
||||
this.onSaveRequest();
|
||||
} else {
|
||||
console.warn("UiController: onSaveRequest callback not set.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle load button click
|
||||
*/
|
||||
handleLoadClick() {
|
||||
if (this.loadButton.getAttribute('disabled') === 'disabled') {
|
||||
return;
|
||||
}
|
||||
if (this.onLoadRequest) {
|
||||
this.onLoadRequest();
|
||||
} else {
|
||||
console.warn("UiController: onLoadRequest callback not set.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle speech toggle button click
|
||||
*/
|
||||
handleSpeechToggle() {
|
||||
if (!this.ttsHandler) {
|
||||
console.warn("UiController: ttsHandler not set. Cannot toggle speech.");
|
||||
// Attempt to use ttsPlayer as fallback if needed, but prefer ttsHandler
|
||||
if (this.ttsPlayer && this.speechButton.getAttribute('disabled') !== 'disabled') {
|
||||
const enabled = this.ttsPlayer.toggle();
|
||||
this.updateSpeechButtonStyling(enabled);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.speechButton.getAttribute('disabled') === 'disabled') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure AudioContext is resumed on user interaction if using Kokoro
|
||||
if (window.ttsFactory && window.ttsFactory.usingKokoro && this.ttsHandler.audioContext && this.ttsHandler.audioContext.state === 'suspended') {
|
||||
this.ttsHandler.audioContext.resume().catch(err => console.error('Error resuming AudioContext on click:', err));
|
||||
}
|
||||
|
||||
// Set user activation flag for the handler
|
||||
this.ttsHandler.hasUserActivation = true;
|
||||
const enabled = this.ttsHandler.toggle();
|
||||
this.updateSpeechButtonStyling(enabled); // Update visual style
|
||||
|
||||
if (enabled) {
|
||||
// Speak the last narrative if speech was just enabled and story container is available
|
||||
if (this.storyContainer) {
|
||||
const lastNarrative = this.storyContainer.lastElementChild;
|
||||
if (lastNarrative && lastNarrative.classList.contains('narrative')) { // Check if it's narrative text
|
||||
console.log("Speaking last narrative on toggle");
|
||||
// Use a slight delay to ensure audio context is resumed
|
||||
setTimeout(() => this.ttsHandler.speak(lastNarrative.textContent), 50);
|
||||
|
||||
// Handle speed slider change for animation speed
|
||||
const speedSlider = document.getElementById('speed');
|
||||
if (speedSlider) {
|
||||
speedSlider.addEventListener('input', (e) => {
|
||||
if (this.animationQueue) {
|
||||
// Convert slider value (0-100) to animation speed
|
||||
// Using formula from Documentation.md: lower values = slower speed
|
||||
const value = parseInt(e.target.value);
|
||||
const speed = Math.pow(100.0 - value, 3) / 10000 * 10 + 0.01;
|
||||
this.animationQueue.setSpeed(speed);
|
||||
console.log(`UI Controller: Animation speed set to ${speed.toFixed(3)}`);
|
||||
|
||||
// Save to persistence manager if available
|
||||
if (window.PersistenceManager) {
|
||||
window.PersistenceManager.updatePreference('animation', 'speed', value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Set initial speed from persistence manager if available
|
||||
if (window.PersistenceManager) {
|
||||
const savedSpeed = window.PersistenceManager.getPreference('animation', 'speed', 50);
|
||||
speedSlider.value = savedSpeed;
|
||||
// Apply initial speed
|
||||
if (this.animationQueue) {
|
||||
const speed = Math.pow(100.0 - savedSpeed, 3) / 10000 * 10 + 0.01;
|
||||
this.animationQueue.setSpeed(speed);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If disabling, ensure speech stops
|
||||
this.ttsHandler.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle fast forward (spacebar or click)
|
||||
*/
|
||||
handleFastForward() {
|
||||
if (!this.animationQueue) return;
|
||||
|
||||
this.animationQueue.fastForward();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle window resize
|
||||
*/
|
||||
handleWindowResize() {
|
||||
this.updateBookDimensions();
|
||||
this.updateParagraphHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the active TTS handler.
|
||||
* @param {object} handler - The TTS handler instance (e.g., KokoroHandler, BrowserTtsHandler).
|
||||
*/
|
||||
setTtsHandler(handler) {
|
||||
this.ttsHandler = handler;
|
||||
console.log("UiController: TTS Handler set.", handler);
|
||||
// Update button state based on the new handler's status
|
||||
this.updateSpeechButtonStyling(this.ttsHandler ? this.ttsHandler.isEnabled() : false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the book dimensions based on viewport size
|
||||
*/
|
||||
updateBookDimensions() {
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
const viewportAspectRatio = vw / vh;
|
||||
const imageAspectRatio = 2727 / 1691;
|
||||
|
||||
let bookWidth, bookHeight;
|
||||
|
||||
if (viewportAspectRatio > imageAspectRatio) {
|
||||
bookWidth = vh * imageAspectRatio;
|
||||
bookHeight = vh;
|
||||
} else {
|
||||
bookWidth = vw;
|
||||
bookHeight = vw / imageAspectRatio;
|
||||
}
|
||||
|
||||
document.documentElement.style.setProperty('--book-width', `${bookWidth}px`);
|
||||
document.documentElement.style.setProperty('--book-height', `${bookHeight}px`);
|
||||
|
||||
// Setting a CSS variable that will be either vw or vh depending on the viewport aspect ratio
|
||||
document.documentElement.style.setProperty(
|
||||
"--viewport-dimension",
|
||||
viewportAspectRatio > imageAspectRatio ? 'vw' : 'vh'
|
||||
);
|
||||
|
||||
document.documentElement.style.setProperty('--viewport-aspect-ratio', viewportAspectRatio);
|
||||
|
||||
const story = document.getElementById("story");
|
||||
if (story) {
|
||||
const paddingTop = window.getComputedStyle(story).paddingTop;
|
||||
const paddingBottom = window.getComputedStyle(story).paddingBottom;
|
||||
document.documentElement.style.setProperty('--story-line-height', (story.clientHeight - paddingTop - paddingBottom) / 28);
|
||||
// Handle speech toggle with proper state management
|
||||
const speechToggle = document.getElementById('speech');
|
||||
if (speechToggle && this.ttsHandler) {
|
||||
// Remove disabled attribute to make it clickable
|
||||
speechToggle.removeAttribute('disabled');
|
||||
|
||||
speechToggle.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
console.log('Speech toggle clicked');
|
||||
|
||||
// Toggle TTS state
|
||||
if (this.ttsHandler && typeof this.ttsHandler.toggle === 'function') {
|
||||
this.ttsEnabled = this.ttsHandler.toggle();
|
||||
|
||||
// Update button text
|
||||
speechToggle.textContent = this.ttsEnabled ? 'mute' : 'speech';
|
||||
|
||||
// Save preference if persistence manager is available
|
||||
const persistenceManager = moduleRegistry.getModule('persistence-manager');
|
||||
if (persistenceManager) {
|
||||
persistenceManager.updatePreference('tts', 'enabled', this.ttsEnabled);
|
||||
}
|
||||
|
||||
console.log(`UI Controller: TTS ${this.ttsEnabled ? 'enabled' : 'disabled'}`);
|
||||
} else {
|
||||
console.warn('TTS Handler does not have toggle method');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update paragraph heights based on viewport
|
||||
*/
|
||||
updateParagraphHeight() {
|
||||
document.querySelectorAll("#story p").forEach((element) => {
|
||||
if (element.dataset.vpc) {
|
||||
const pHeight = parseFloat(window.getComputedStyle(document.getElementById('page_right')).height);
|
||||
const newHeight = pHeight * element.dataset.vpc / 100 + 'px';
|
||||
element.style.height = newHeight;
|
||||
|
||||
// Add options button to controls section
|
||||
const controlsSection = document.getElementById('controls');
|
||||
if (controlsSection) {
|
||||
// Check if options button already exists
|
||||
if (!document.getElementById('options-button')) {
|
||||
const optionsButton = document.createElement('a');
|
||||
optionsButton.id = 'options-button';
|
||||
optionsButton.href = '#';
|
||||
optionsButton.textContent = 'options';
|
||||
optionsButton.title = 'Show game options';
|
||||
|
||||
// Add event listener
|
||||
optionsButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const optionsUI = moduleRegistry.getModule('options-ui');
|
||||
if (optionsUI && optionsUI.toggle) {
|
||||
optionsUI.toggle();
|
||||
}
|
||||
});
|
||||
|
||||
// Add to controls
|
||||
controlsSection.appendChild(document.createTextNode(' | '));
|
||||
controlsSection.appendChild(optionsButton);
|
||||
}
|
||||
}
|
||||
|
||||
// Enable all controls buttons
|
||||
const controlButtons = document.querySelectorAll('#controls a');
|
||||
controlButtons.forEach(button => {
|
||||
button.removeAttribute('disabled');
|
||||
});
|
||||
|
||||
// Book click for fast-forwarding - make sure it triggers the animation queue
|
||||
if (this.bookElement) {
|
||||
this.bookElement.addEventListener('click', (event) => {
|
||||
// Only if not clicking on a link or control
|
||||
if (event.target.tagName !== 'A' &&
|
||||
!event.target.closest('#controls') &&
|
||||
!event.target.closest('#command_input')) {
|
||||
if (this.animationQueue) {
|
||||
console.log('UI Controller: Fast-forwarding animations');
|
||||
this.animationQueue.fastForward();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Space key for fast-forwarding
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === ' ' &&
|
||||
document.activeElement.tagName !== 'TEXTAREA' &&
|
||||
document.activeElement.tagName !== 'INPUT') {
|
||||
if (this.animationQueue) {
|
||||
console.log('UI Controller: Fast-forwarding animations (space key)');
|
||||
this.animationQueue.fastForward();
|
||||
e.preventDefault(); // Prevent page scrolling
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the speech button styling based on enabled state.
|
||||
* @param {boolean} enabled - Whether speech is enabled.
|
||||
*/
|
||||
updateSpeechButtonStyling(enabled = false) {
|
||||
if (!this.speechButton) return;
|
||||
|
||||
if (enabled) {
|
||||
this.speechButton.style.fontWeight = 'bold';
|
||||
this.speechButton.style.color = '#000';
|
||||
this.speechButton.style.backgroundColor = '#eee';
|
||||
} else {
|
||||
this.speechButton.style.fontWeight = 'normal';
|
||||
this.speechButton.style.color = '#333';
|
||||
this.speechButton.style.backgroundColor = '';
|
||||
|
||||
async setupMainUI() {
|
||||
// Ensure all UI components exist
|
||||
if (!this.bookElement || !this.leftPage || !this.rightPage || !this.storyElement) {
|
||||
console.log('UI Controller: Creating missing UI elements');
|
||||
this.displayHandler.setupBookStructure();
|
||||
|
||||
// Re-get elements
|
||||
this.bookElement = document.getElementById('book');
|
||||
this.leftPage = document.getElementById('page_left');
|
||||
this.rightPage = document.getElementById('page_right');
|
||||
this.storyElement = document.getElementById('story');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the enabled/disabled state and title of the speech button.
|
||||
* @param {boolean} available - Whether any TTS system is available.
|
||||
* @param {string} [type] - The type of TTS system available ('kokoro', 'browser', etc.).
|
||||
*/
|
||||
updateSpeechButtonAvailability(available, type) {
|
||||
if (!this.speechButton) return;
|
||||
|
||||
if (available) {
|
||||
this.speechButton.removeAttribute('disabled');
|
||||
const ttsName = type === 'kokoro' ? 'Kokoro TTS' : (type === 'browser' ? 'Browser TTS' : 'TTS');
|
||||
const title = this.translations[this.locale]?.title_speech || `Toggle Text-to-Speech (${ttsName})`;
|
||||
this.speechButton.setAttribute('title', title);
|
||||
// Update style based on current handler state if available
|
||||
this.updateSpeechButtonStyling(this.ttsHandler ? this.ttsHandler.isEnabled() : false);
|
||||
} else {
|
||||
this.speechButton.setAttribute('disabled', 'disabled');
|
||||
const title = this.translations[this.locale]?.title_speech_unavailable || 'Text-to-Speech not available';
|
||||
this.speechButton.setAttribute('title', title);
|
||||
this.updateSpeechButtonStyling(false); // Ensure style is off
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the enabled/disabled state of control buttons based on game state.
|
||||
* @param {object} gameState - The current game state from AnimatedFiction.
|
||||
* @param {boolean} gameState.started - Whether the game has started.
|
||||
* @param {boolean} [gameState.canLoad] - Whether a saved game exists to be loaded.
|
||||
*/
|
||||
updateButtonStates(gameState) {
|
||||
if (this.rewindButton) {
|
||||
if (gameState.started) {
|
||||
this.rewindButton.removeAttribute('disabled');
|
||||
} else {
|
||||
this.rewindButton.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
}
|
||||
if (this.saveButton) {
|
||||
if (gameState.started) {
|
||||
this.saveButton.removeAttribute('disabled');
|
||||
} else {
|
||||
this.saveButton.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
}
|
||||
if (this.loadButton) {
|
||||
// Enable load button if a save exists (indicated by canLoad flag or similar)
|
||||
// We might need a more robust way to check for saved state existence.
|
||||
// For now, enable if game started OR if canLoad is explicitly true.
|
||||
if (gameState.started || gameState.canLoad) {
|
||||
this.loadButton.removeAttribute('disabled');
|
||||
} else {
|
||||
this.loadButton.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
}
|
||||
// Speech button availability is handled separately by updateSpeechButtonAvailability
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the visual display of the speed slider.
|
||||
* @param {number} value - The speed value (0-100).
|
||||
*/
|
||||
updateSpeedDisplay(value) {
|
||||
if (this.speedSlider) {
|
||||
this.speedSlider.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert an element after a delay (Helper, potentially move elsewhere or keep if used)
|
||||
* @param {number} delay - The delay in milliseconds
|
||||
* @param {HTMLElement} target - The target element to append to
|
||||
* @param {HTMLElement} el - The element to insert
|
||||
* @param {boolean} fadeIn - Whether to fade in the element
|
||||
*/
|
||||
insertAfter(delay, target, el, fadeIn = true) {
|
||||
if (this.animationQueue) {
|
||||
if (fadeIn) {
|
||||
el.classList.add("fade-in");
|
||||
this.animationQueue.schedule(function() {
|
||||
target.appendChild(el);
|
||||
}, delay);
|
||||
} else {
|
||||
this.animationQueue.schedule(function() {
|
||||
target.appendChild(el);
|
||||
}, delay);
|
||||
}
|
||||
} else {
|
||||
// Fallback if no animation queue
|
||||
if (fadeIn) {
|
||||
el.classList.add("fade-in");
|
||||
setTimeout(() => {
|
||||
target.appendChild(el);
|
||||
}, delay);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
target.appendChild(el);
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the locale for translations
|
||||
* @param {string} locale - The locale code
|
||||
*/
|
||||
setLocale(locale) {
|
||||
this.locale = locale;
|
||||
|
||||
if (this.translations[locale]) {
|
||||
Object.keys(this.translations[locale]).forEach(key => {
|
||||
const prefix = key.substring(0, 5);
|
||||
const postfix = key.substring(6, key.length);
|
||||
const elements = document.querySelectorAll(`.l10n-${(prefix === 'title' ? postfix : key)}`);
|
||||
|
||||
elements.forEach(element => {
|
||||
if (prefix === "title") {
|
||||
element.title = this.translations[locale][key];
|
||||
} else {
|
||||
element.innerHTML = this.translations[locale][key];
|
||||
}
|
||||
});
|
||||
|
||||
initializeTextBuffer() {
|
||||
// Initialize text buffer handling
|
||||
if (this.textBuffer) {
|
||||
this.textBuffer.setOnSentenceReady((text, callback) => {
|
||||
console.log('UI Controller: Displaying sentence');
|
||||
this.displayText(text).then(callback);
|
||||
});
|
||||
} else {
|
||||
console.error(`Locale ${locale} is not defined`);
|
||||
}
|
||||
}
|
||||
|
||||
handleCommand(command) {
|
||||
// Route commands to appropriate handlers
|
||||
switch (command.type) {
|
||||
case 'display':
|
||||
this.displayHandler.processCommand(command);
|
||||
break;
|
||||
case 'effect':
|
||||
this.effects.processCommand(command);
|
||||
break;
|
||||
case 'continue':
|
||||
if (this.animationQueue) {
|
||||
this.animationQueue.fastForward();
|
||||
}
|
||||
break;
|
||||
case 'input':
|
||||
if (this.socketClient) {
|
||||
this.socketClient.sendCommand(command.text);
|
||||
}
|
||||
break;
|
||||
case 'menu':
|
||||
// Toggle options menu
|
||||
const optionsUI = moduleRegistry.getModule('options-ui');
|
||||
if (optionsUI) {
|
||||
optionsUI.toggle();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Handle general UI commands or pass to game logic
|
||||
this._dispatchModuleEvent('ui:command', command);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI button states based on game state
|
||||
* @param {Object} state - Game state information
|
||||
*/
|
||||
updateButtonStates(state = {}) {
|
||||
const { canSave, canLoad, canRestart } = state;
|
||||
|
||||
// Get button elements
|
||||
const saveButton = document.getElementById('save');
|
||||
const loadButton = document.getElementById('reload');
|
||||
const restartButton = document.getElementById('rewind');
|
||||
|
||||
// Update save button state
|
||||
if (saveButton) {
|
||||
if (canSave) {
|
||||
saveButton.removeAttribute('disabled');
|
||||
} else {
|
||||
saveButton.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
}
|
||||
|
||||
// Update load button state
|
||||
if (loadButton) {
|
||||
if (canLoad) {
|
||||
loadButton.removeAttribute('disabled');
|
||||
} else {
|
||||
loadButton.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
}
|
||||
|
||||
// Update restart button state
|
||||
if (restartButton) {
|
||||
if (canRestart) {
|
||||
restartButton.removeAttribute('disabled');
|
||||
} else {
|
||||
restartButton.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public API methods
|
||||
|
||||
showUI() {
|
||||
if (!this.isVisible) {
|
||||
this.isVisible = true;
|
||||
this.displayHandler.show();
|
||||
this.effects.startAmbientEffects();
|
||||
}
|
||||
}
|
||||
|
||||
hideUI() {
|
||||
if (this.isVisible) {
|
||||
this.isVisible = false;
|
||||
this.displayHandler.hide();
|
||||
this.effects.stopAmbientEffects();
|
||||
}
|
||||
}
|
||||
|
||||
displayText(text, options = {}) {
|
||||
return this.displayHandler.displayText(text, options);
|
||||
}
|
||||
|
||||
clearDisplay() {
|
||||
this.displayHandler.clear();
|
||||
}
|
||||
|
||||
sendCommand(command) {
|
||||
if (this.socketClient) {
|
||||
return this.socketClient.sendCommand(command);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the singleton instance
|
||||
const uiController = new UIController();
|
||||
|
||||
// Register with the module registry
|
||||
moduleRegistry.register(uiController);
|
||||
|
||||
// Export the module
|
||||
export { uiController as UIController };
|
||||
|
||||
// Keep a reference in window for loader system
|
||||
window.UIController = uiController;
|
||||
|
||||
Reference in New Issue
Block a user