/**
* 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 = `
Close
`;
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
${step1}
${step2}
3. Add an email account
Three Tabs
Work — tasks & projects
Time — day plan, weekly review & calendar
Messages — email & contacts
${shortcutsHint}
Get Started
`;
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);
}
});
})();
})();