clean-tracks/src/static/js/modules/state.js

187 lines
5.0 KiB
JavaScript

/**
* 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();
}
}