/** * GoingsOn - Application State * Centralized state management with pub/sub for reactivity */ (function() { 'use strict'; // ============ State Class ============ class AppStateManager { constructor() { // Subscribers for state changes this._subscribers = {}; // ============ Data State (single source of truth) ============ // Domain data this.projects = []; this.tasks = []; this.emails = []; this.emailAccounts = []; // ============ UI State ============ // Current view/selection this.currentView = 'projects'; this.currentProjectId = null; // Day planning this.dayPlanDate = new Date(); this.dayPlanData = null; // Drag and drop this.draggedTaskId = null; // Timeline this.currentTimeIndicatorInterval = null; this.paintingState = null; // { startSlot, startTime, endSlot, endTime, preview } // Keyboard navigation this.pendingKey = null; this.pendingKeyTimeout = null; this.selectedItemIndex = -1; // Bulk selection (keeping as Sets for performance) this.selectedTaskIds = new Set(); this.selectedEmailIds = new Set(); // Pagination this.taskPage = 1; this.emailPage = 1; this.itemsPerPage = 50; // View switch tracking (in-memory only, reset on restart) this.viewSwitchCounts = {}; this.lastSubviewByTab = {}; this.sessionStart = Date.now(); } /** * Set a state property and notify subscribers * @param {string} key - Property name * @param {*} value - New value */ set(key, value) { const oldValue = this[key]; this[key] = value; this.notify(key, value, oldValue); } /** * Subscribe to changes on a state property * @param {string} key - Property name to watch * @param {Function} callback - Called with (newValue, oldValue) * @returns {Function} Unsubscribe function */ subscribe(key, callback) { if (!this._subscribers[key]) { this._subscribers[key] = []; } this._subscribers[key].push(callback); // Return unsubscribe function return () => { const idx = this._subscribers[key].indexOf(callback); if (idx > -1) { this._subscribers[key].splice(idx, 1); } }; } /** * Notify all subscribers of a state change * @param {string} key - Property that changed * @param {*} newValue - New value * @param {*} oldValue - Previous value */ notify(key, newValue, oldValue) { const callbacks = this._subscribers[key]; if (callbacks) { for (const cb of callbacks) { try { cb(newValue, oldValue); } catch (err) { console.error(`State subscriber error for "${key}":`, err); } } } } /** * Batch update multiple properties * @param {Object} updates - Object with key-value pairs to update */ update(updates) { for (const [key, value] of Object.entries(updates)) { this.set(key, value); } } /** * Reset pagination for a domain * @param {string} domain - 'task' or 'email' */ resetPagination(domain) { if (domain === 'task') { this.set('taskPage', 1); } else if (domain === 'email') { this.set('emailPage', 1); } } /** * Clear selection for a domain * @param {string} domain - 'task' or 'email' */ clearSelection(domain) { if (domain === 'task') { this.selectedTaskIds.clear(); this.notify('selectedTaskIds', this.selectedTaskIds, null); } else if (domain === 'email') { this.selectedEmailIds.clear(); this.notify('selectedEmailIds', this.selectedEmailIds, null); } } } // ============ Create Global Instance ============ const AppState = new AppStateManager(); // ============ Populate GoingsOn.state ============ GoingsOn.state = AppState; })();