/** * State Manager - Manages application state */ export class StateManager { constructor() { this.state = {}; this.listeners = {}; // Load persisted state this.loadPersistedState(); } get(key) { return this.state[key]; } set(key, value) { const oldValue = this.state[key]; this.state[key] = value; // Persist certain keys if (this.shouldPersist(key)) { this.persistState(key, value); } // Notify listeners this.notifyListeners(key, value, oldValue); } remove(key) { const oldValue = this.state[key]; delete this.state[key]; // Remove from localStorage if (this.shouldPersist(key)) { localStorage.removeItem(`clean-tracks-${key}`); } // Notify listeners this.notifyListeners(key, undefined, oldValue); } clear(keys = null) { if (keys) { keys.forEach(key => this.remove(key)); } else { // Clear all state Object.keys(this.state).forEach(key => this.remove(key)); } } // State persistence shouldPersist(key) { const persistedKeys = [ 'userSettings', 'theme', 'language', 'defaultWordList', 'defaultCensorMethod', 'defaultWhisperModel', // Onboarding-related state 'hasCompletedOnboarding', 'onboardingProgress', 'onboardingMilestones', 'onboardingVersion', 'onboardingSkipped', 'onboardingCompletedAt' ]; return persistedKeys.includes(key); } persistState(key, value) { try { localStorage.setItem(`clean-tracks-${key}`, JSON.stringify(value)); } catch (error) { console.error('Error persisting state:', error); } } loadPersistedState() { const persistedKeys = [ 'userSettings', 'theme', 'language', 'defaultWordList', 'defaultCensorMethod', 'defaultWhisperModel', // Onboarding-related state 'hasCompletedOnboarding', 'onboardingProgress', 'onboardingMilestones', 'onboardingVersion', 'onboardingSkipped', 'onboardingCompletedAt' ]; persistedKeys.forEach(key => { try { const value = localStorage.getItem(`clean-tracks-${key}`); if (value) { this.state[key] = JSON.parse(value); } } catch (error) { console.error(`Error loading persisted state for ${key}:`, error); } }); } // State listeners subscribe(key, listener) { if (!this.listeners[key]) { this.listeners[key] = []; } this.listeners[key].push(listener); // Return unsubscribe function return () => { this.unsubscribe(key, listener); }; } unsubscribe(key, listener) { if (this.listeners[key]) { this.listeners[key] = this.listeners[key].filter(l => l !== listener); } } notifyListeners(key, newValue, oldValue) { if (this.listeners[key]) { this.listeners[key].forEach(listener => { try { listener(newValue, oldValue, key); } catch (error) { console.error(`Error in state listener for ${key}:`, error); } }); } // Notify global listeners if (this.listeners['*']) { this.listeners['*'].forEach(listener => { try { listener(key, newValue, oldValue); } catch (error) { console.error('Error in global state listener:', error); } }); } } // Computed values compute(key, computeFn) { Object.defineProperty(this.state, key, { get: computeFn, enumerable: true, configurable: true }); } // State snapshots getSnapshot() { return JSON.parse(JSON.stringify(this.state)); } restoreSnapshot(snapshot) { this.state = JSON.parse(JSON.stringify(snapshot)); // Notify all listeners Object.keys(this.state).forEach(key => { this.notifyListeners(key, this.state[key], undefined); }); } // Debugging debug() { console.group('State Manager Debug'); console.log('Current State:', this.state); console.log('Listeners:', Object.keys(this.listeners).map(key => ({ key, count: this.listeners[key].length }))); console.groupEnd(); } }