Add ink integration UI and media playback

This commit is contained in:
2026-05-15 21:23:46 +02:00
parent 44dc64f830
commit f2e786d5bc
89 changed files with 6561 additions and 556 deletions
+78 -2
View File
@@ -7,7 +7,7 @@ class UIControllerModule extends BaseModule {
// Remove 'tts' from direct dependencies to break circular dependency
// UI Controller will access TTS through the Game Loop instead
this.dependencies = ['animation-queue', 'ui-display-handler', 'ui-input-handler', 'ui-effects', 'text-buffer', 'socket-client', 'sentence-queue', 'playback-coordinator'];
this.dependencies = ['animation-queue', 'ui-display-handler', 'ui-input-handler', 'ui-effects', 'text-buffer', 'socket-client', 'sentence-queue', 'playback-coordinator', 'persistence-manager'];
// References to sub-modules
this.displayHandler = null;
@@ -47,6 +47,8 @@ class UIControllerModule extends BaseModule {
'syncTopControls',
'getStoredTtsPreference',
'setStoredTtsPreference',
'getStoredAppPreference',
'setStoredAppPreference',
'sliderValueFromSpeed',
'speedFromSliderValue',
'initializeTextBuffer',
@@ -267,7 +269,8 @@ class UIControllerModule extends BaseModule {
}
const playbackCoordinator = this.getModule('playback-coordinator');
if (playbackCoordinator && playbackCoordinator.isPlaying) {
const hasSkippablePause = document.documentElement.dataset.skippablePause === 'true';
if ((playbackCoordinator && playbackCoordinator.isPlaying) || hasSkippablePause) {
this.handleCommand({ type: 'continue', source: 'book-click' });
}
@@ -350,6 +353,9 @@ class UIControllerModule extends BaseModule {
document.addEventListener('preference-updated', (event) => {
const { category, key, value } = event.detail || {};
if (category !== 'tts') {
if (category === 'app' && key === 'autoplay') {
this.syncTopControls();
}
return;
}
@@ -391,6 +397,7 @@ class UIControllerModule extends BaseModule {
bindTopControls() {
const speechToggle = document.getElementById('speech');
const autoplayToggle = document.getElementById('autoplay');
const speedSlider = document.getElementById('speed');
const speedReset = document.getElementById('speed_reset');
@@ -428,6 +435,22 @@ class UIControllerModule extends BaseModule {
});
}
if (autoplayToggle && autoplayToggle.dataset.uiControllerBound !== 'true') {
autoplayToggle.dataset.uiControllerBound = 'true';
autoplayToggle.removeAttribute('disabled');
autoplayToggle.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
const nextAutoplay = !this.getStoredAppPreference('autoplay', true);
this.setStoredAppPreference('autoplay', nextAutoplay);
console.log(`UIController: Autoplay set to ${nextAutoplay ? 'enabled' : 'disabled'}`);
this.syncTopControls();
document.dispatchEvent(new CustomEvent('app:autoplay:change', {
detail: { enabled: nextAutoplay, source: 'topbar' }
}));
});
}
if (speedSlider && speedSlider.dataset.uiControllerBound !== 'true') {
speedSlider.dataset.uiControllerBound = 'true';
speedSlider.min = speedSlider.min || '50';
@@ -518,6 +541,48 @@ class UIControllerModule extends BaseModule {
console.warn('UIController: Failed to write TTS preference fallback:', error);
}
}
getStoredAppPreference(key, defaultValue) {
const persistenceManager = this.getModule('persistence-manager');
if (persistenceManager && typeof persistenceManager.getPreference === 'function') {
const value = persistenceManager.getPreference('app', key, undefined);
if (typeof value !== 'undefined' && value !== null) {
return value;
}
}
try {
const raw = localStorage.getItem('ai-interactive-fiction-preferences');
if (raw) {
const prefs = JSON.parse(raw);
if (prefs && prefs.app && Object.prototype.hasOwnProperty.call(prefs.app, key)) {
return prefs.app[key];
}
}
} catch (error) {
console.warn('UIController: Failed to read app preference fallback:', error);
}
return defaultValue;
}
setStoredAppPreference(key, value) {
const persistenceManager = this.getModule('persistence-manager');
if (persistenceManager && typeof persistenceManager.updatePreference === 'function') {
persistenceManager.updatePreference('app', key, value);
}
try {
const storageKey = 'ai-interactive-fiction-preferences';
const raw = localStorage.getItem(storageKey);
const prefs = raw ? JSON.parse(raw) : {};
prefs.app = prefs.app || {};
prefs.app[key] = value;
localStorage.setItem(storageKey, JSON.stringify(prefs));
} catch (error) {
console.warn('UIController: Failed to write app preference fallback:', error);
}
}
async setupMainUI() {
// Ensure all UI components exist
@@ -583,6 +648,9 @@ class UIControllerModule extends BaseModule {
break;
case 'continue':
{
document.dispatchEvent(new CustomEvent('ui:command', {
detail: { moduleId: this.id, type: 'continue', source: command.source || 'ui-controller-forward' }
}));
const playbackCoordinator = this.getModule('playback-coordinator');
if (playbackCoordinator && playbackCoordinator.isPlaying) {
playbackCoordinator.fastForward();
@@ -633,6 +701,7 @@ class UIControllerModule extends BaseModule {
const loadButton = document.getElementById('reload');
const restartButton = document.getElementById('rewind');
const speechToggle = document.getElementById('speech');
const autoplayToggle = document.getElementById('autoplay');
// Update save button state
if (saveButton && typeof canSave === 'boolean') {
@@ -679,6 +748,13 @@ class UIControllerModule extends BaseModule {
speechToggle.title = 'Enable speech';
}
}
if (autoplayToggle) {
const autoplay = this.getStoredAppPreference('autoplay', true) !== false;
autoplayToggle.removeAttribute('disabled');
autoplayToggle.style.fontWeight = autoplay ? 'bold' : 'normal';
autoplayToggle.style.color = autoplay ? '#000' : '#999';
}
}
// Public API methods