/** * GoingsOn - App Bootstrap Module * DOMContentLoaded, initial data loading, Tauri menu listeners */ (function() { 'use strict'; // ============ Application Initialization ============ document.addEventListener('DOMContentLoaded', async () => { // Check if api is available if (!GoingsOn.api) { console.error('API not available'); const errorTarget = document.getElementById('projects-grid') || document.getElementById('task-list-container'); if (errorTarget) errorTarget.innerHTML = '
GoingsOn failed to start. Please relaunch the app. If this persists, contact info@makenot.work.
'; return; } // Load projects cache first (needed for task dropdowns) try { const projects = await GoingsOn.api.projects.list(); GoingsOn.projects.setCache(projects); } catch (err) { console.error('Failed to load projects:', err); } // Load email accounts cache try { const accounts = await GoingsOn.api.emailAccounts.list(); GoingsOn.emails.setAccountsCache(accounts); } catch (err) { console.error('Failed to load email accounts:', err); } // Initialize router (handles initial view from URL) if (GoingsOn.router && typeof GoingsOn.router.init === 'function') { GoingsOn.router.init(); } else { // Fallback if router not available GoingsOn.tasks.load(); } // First-run welcome if (!localStorage.getItem('go-welcomed')) { showWelcome(); } else if (!localStorage.getItem('go-hint-shortcuts')) { // One-time hint after first session setTimeout(() => showHint('go-hint-shortcuts', 'Press ? anytime to see keyboard shortcuts'), 2000); } // After an OTA update, surface this version's changelog once. No-ops on a // matching version and records first-launch silently (welcome owns that). if (GoingsOn.whatsNew && typeof GoingsOn.whatsNew.maybeShow === 'function') { GoingsOn.whatsNew.maybeShow(); } // Check weekly review nudge on startup if (GoingsOn.weeklyReview && typeof GoingsOn.weeklyReview.checkNudge === 'function') { GoingsOn.weeklyReview.checkNudge(); } // Start event status indicator polling if (GoingsOn.events && typeof GoingsOn.events.startEventStatusPolling === 'function') { GoingsOn.events.startEventStatusPolling(); } // Initialize sync status indicator if (GoingsOn.settings && typeof GoingsOn.settings.refreshSyncIndicator === 'function') { GoingsOn.settings.refreshSyncIndicator(); } // Initialize time tracking widget (check for active timer) if (GoingsOn.timeTracking && typeof GoingsOn.timeTracking.init === 'function') { GoingsOn.timeTracking.init(); } }); // Initialize theme on page load document.addEventListener('DOMContentLoaded', () => { if (GoingsOn.themes && typeof GoingsOn.themes.loadFromStorage === 'function') { GoingsOn.themes.loadFromStorage(); } }); // Close dropdowns when clicking outside document.addEventListener('click', (e) => { // If click is not on a dropdown button, close all dropdowns if (!e.target.closest('.dropdown')) { document.querySelectorAll('.dropdown-menu.show').forEach(menu => { menu.classList.remove('show'); }); } }); // ============ Native Menu Bar Event Handlers ============ // Initialize menu event listeners when Tauri is available if (window.__TAURI__) { const { listen } = window.__TAURI__.event; // Compose → main app: queue a send with an undo window. // Fired by compose.html sendEmail() so the compose window can close // immediately while the main app holds the 5 s undo toast. listen('compose:queue-send', (event) => { const payload = event?.payload || {}; if (payload.input) { GoingsOn.emails.queueSend({ input: payload.input, delaySeconds: payload.delaySeconds || 5, }); } }); // File menu listen('menu:new_task', () => GoingsOn.tasks.openNew()); listen('menu:new_project', () => GoingsOn.projects.openNew()); listen('menu:import', () => GoingsOn.import.openModal()); listen('menu:save_view', () => GoingsOn.savedViews?.openSaveModal?.()); // View menu - tab shortcuts listen('menu:view_work', () => GoingsOn.navigation.switchView('work')); listen('menu:view_time', () => GoingsOn.navigation.switchView('time')); listen('menu:view_messages', () => GoingsOn.navigation.switchView('messages')); // View menu - individual sub-views listen('menu:view_projects', () => GoingsOn.navigation.switchView('projects')); listen('menu:view_tasks', () => GoingsOn.navigation.switchView('tasks')); listen('menu:view_events', () => GoingsOn.navigation.switchView('events')); listen('menu:view_emails', () => GoingsOn.navigation.switchView('emails')); listen('menu:view_contacts', () => GoingsOn.navigation.switchView('contacts')); listen('menu:view_day_plan', () => GoingsOn.navigation.switchView('day-plan')); listen('menu:view_weekly_review', () => GoingsOn.navigation.switchView('weekly-review')); listen('menu:view_monthly_review', () => GoingsOn.navigation.switchView('monthly-review')); listen('menu:toggle_sidebar', () => GoingsOn.app.toggleSidebar()); // Tools menu listen('menu:sync_email', () => GoingsOn.app.syncAllEmailAccounts()); listen('menu:settings', () => GoingsOn.settings.open()); // Help menu listen('menu:keyboard_shortcuts', () => GoingsOn.keyboard.toggleShortcuts()); listen('menu:about', () => GoingsOn.app.openAboutModal()); // Database external change detection listen('db:external-change', () => { GoingsOn.cache.invalidateAll(); refreshCurrentViewData(); }); // Cloud sync: remote changes applied listen('sync:changes-applied', () => { GoingsOn.cache.invalidateAll(); refreshCurrentViewData(); }); // Cloud sync: subscription required (402 from server) listen('sync:subscription-required', () => { GoingsOn.ui.showToast('Cloud sync paused — subscription required', 'error', { action: { label: 'Subscribe', fn: () => GoingsOn.settings.openCloudSync() }, duration: 10000, }); }); // Cloud sync: status changed (syncing/idle/error) listen('sync:status-changed', (event) => { const dot = document.getElementById('sync-dot'); const indicator = document.getElementById('sync-indicator'); if (!dot || !indicator) return; indicator.classList.remove('hidden'); dot.className = 'sync-dot'; if (event.payload === 'syncing') { dot.classList.add('syncing'); } else if (event.payload === 'error') { dot.classList.add('error'); } else { dot.classList.add('connected'); } }); } // ============ External Change Handler ============ /** * Refresh the current view's data without full navigation. * Used when external changes are detected (e.g., an external process modified the database). */ async function refreshCurrentViewData() { // Don't refresh if a modal is open (user is editing something) if (document.querySelector('.modal:not(.hidden)')) { return; } const currentView = GoingsOn.navigation?.getCurrentView?.() || 'tasks'; try { // Refresh projects cache first (needed for dropdowns) const projects = await GoingsOn.api.projects.list(); GoingsOn.projects.setCache(projects); // Reload the current view's data await GoingsOn.navigation.loadViewData(currentView); } catch (err) { console.error('Failed to refresh view after external change:', err); } } // ============ Sidebar Toggle ============ function toggleSidebar() { const sidebar = document.querySelector('.saved-views-sidebar'); if (sidebar) { sidebar.classList.toggle('hidden'); } } // ============ Email Sync ============ async function syncAllEmailAccounts() { try { const accounts = await GoingsOn.api.emailAccounts.list(); if (accounts.length === 0) { GoingsOn.ui.showToast('No email accounts configured', 'info'); return; } GoingsOn.ui.showToast('Syncing email accounts...', 'info'); for (const account of accounts) { await GoingsOn.api.emailAccounts.sync(account.id, false); } GoingsOn.ui.showToast('Email sync complete!', 'success'); GoingsOn.emails.load(); } catch (err) { GoingsOn.ui.showToast('Email sync failed: ' + GoingsOn.utils.getErrorMessage(err), 'error', { action: { label: 'Retry', fn: syncAllEmailAccounts }, duration: 8000, }); } } // ============ About Modal ============ async function openAboutModal() { let appVersion = "unknown"; try { appVersion = await window.__TAURI__.app.getVersion(); } catch (_) {} const content = `

GoingsOn

Tasks, email, calendar, contacts.

Version ${appVersion}

Publisher
Make Creative, LLC
License
PolyForm Noncommercial 1.0.0
Contact
info@makenot.work
Source
makenot.work
Privacy
makenot.work/policy
`; GoingsOn.ui.openModal('About', content); } // ============ Populate GoingsOn.app Namespace ============ function showWelcome() { const isTouch = !!GoingsOn.touch?.isTouchDevice; const step1 = isTouch ? '1. Create your first task — tap the + tab to quick add' : '1. Create your first task — press q for quick add'; const step2 = isTouch ? '2. Plan your day — tap or long-press the timeline to schedule' : '2. Plan your day — drag tasks onto the timeline'; const shortcutsHint = isTouch ? '' : '

Press ? anytime for keyboard shortcuts.

'; const content = `

GoingsOn brings your tasks, email, calendar, and contacts into one place.

Get Started

Three Tabs

${shortcutsHint}
`; GoingsOn.ui.openModal('Welcome to GoingsOn', content); } /** * Show a one-time dismissible hint toast. Sets localStorage key so it only shows once. */ function showHint(storageKey, message) { if (localStorage.getItem(storageKey)) return; localStorage.setItem(storageKey, '1'); GoingsOn.ui.showToast(message, 'info', { duration: 5000 }); } GoingsOn.app = { toggleSidebar, syncAllEmailAccounts, openAboutModal, refreshCurrentViewData, showWelcome, showHint, }; // ============ Background/Foreground Transitions ============ // On mobile (and laptop sleep/wake), the app can sit hidden for arbitrary // durations. Browsers throttle JS while hidden; the bigger issue is that // rendered state (current time, sync status, lists) is stale on resume. // If the app was hidden long enough that anything time-sensitive could have // shifted, refresh on visibility return. (function wireVisibilityRefresh() { const STALE_THRESHOLD_MS = 30_000; let hiddenAt = null; document.addEventListener('visibilitychange', () => { if (document.hidden) { hiddenAt = Date.now(); return; } if (hiddenAt == null) return; const hiddenFor = Date.now() - hiddenAt; hiddenAt = null; if (hiddenFor < STALE_THRESHOLD_MS) return; // Refresh data and time-sensitive UI. Skip if a modal is open — // refreshCurrentViewData already guards against that. GoingsOn.cache?.invalidateAll?.(); refreshCurrentViewData(); GoingsOn.settings?.refreshSyncIndicator?.(); if (GoingsOn.dayPlanning?.updateCurrentTimeIndicator) { GoingsOn.dayPlanning.updateCurrentTimeIndicator(true); } }); })(); })();