Skip to main content

max / goingson

14.1 KB · 359 lines History Blame Raw
1 /**
2 * GoingsOn - App Bootstrap Module
3 * DOMContentLoaded, initial data loading, Tauri menu listeners
4 */
5
6 (function() {
7 'use strict';
8
9 // ============ Application Initialization ============
10
11 document.addEventListener('DOMContentLoaded', async () => {
12 // Check if api is available
13 if (!GoingsOn.api) {
14 console.error('API not available');
15 const errorTarget = document.getElementById('projects-grid') || document.getElementById('task-list-container');
16 if (errorTarget) errorTarget.innerHTML =
17 '<div class="loading loading--error">GoingsOn failed to start. Please relaunch the app. If this persists, contact info@makenot.work.</div>';
18 return;
19 }
20
21 // Load projects cache first (needed for task dropdowns)
22 try {
23 const projects = await GoingsOn.api.projects.list();
24 GoingsOn.projects.setCache(projects);
25 } catch (err) {
26 console.error('Failed to load projects:', err);
27 }
28
29 // Load email accounts cache
30 try {
31 const accounts = await GoingsOn.api.emailAccounts.list();
32 GoingsOn.emails.setAccountsCache(accounts);
33 } catch (err) {
34 console.error('Failed to load email accounts:', err);
35 }
36
37 // Initialize router (handles initial view from URL)
38 if (GoingsOn.router && typeof GoingsOn.router.init === 'function') {
39 GoingsOn.router.init();
40 } else {
41 // Fallback if router not available
42 GoingsOn.tasks.load();
43 }
44
45 // First-run welcome
46 if (!localStorage.getItem('go-welcomed')) {
47 showWelcome();
48 } else if (!localStorage.getItem('go-hint-shortcuts')) {
49 // One-time hint after first session
50 setTimeout(() => showHint('go-hint-shortcuts', 'Press ? anytime to see keyboard shortcuts'), 2000);
51 }
52
53 // After an OTA update, surface this version's changelog once. No-ops on a
54 // matching version and records first-launch silently (welcome owns that).
55 if (GoingsOn.whatsNew && typeof GoingsOn.whatsNew.maybeShow === 'function') {
56 GoingsOn.whatsNew.maybeShow();
57 }
58
59 // Check weekly review nudge on startup
60 if (GoingsOn.weeklyReview && typeof GoingsOn.weeklyReview.checkNudge === 'function') {
61 GoingsOn.weeklyReview.checkNudge();
62 }
63
64 // Start event status indicator polling
65 if (GoingsOn.events && typeof GoingsOn.events.startEventStatusPolling === 'function') {
66 GoingsOn.events.startEventStatusPolling();
67 }
68
69 // Initialize sync status indicator
70 if (GoingsOn.settings && typeof GoingsOn.settings.refreshSyncIndicator === 'function') {
71 GoingsOn.settings.refreshSyncIndicator();
72 }
73
74 // Initialize time tracking widget (check for active timer)
75 if (GoingsOn.timeTracking && typeof GoingsOn.timeTracking.init === 'function') {
76 GoingsOn.timeTracking.init();
77 }
78 });
79
80 // Initialize theme on page load
81 document.addEventListener('DOMContentLoaded', () => {
82 if (GoingsOn.themes && typeof GoingsOn.themes.loadFromStorage === 'function') {
83 GoingsOn.themes.loadFromStorage();
84 }
85 });
86
87 // Close dropdowns when clicking outside
88 document.addEventListener('click', (e) => {
89 // If click is not on a dropdown button, close all dropdowns
90 if (!e.target.closest('.dropdown')) {
91 document.querySelectorAll('.dropdown-menu.show').forEach(menu => {
92 menu.classList.remove('show');
93 });
94 }
95 });
96
97 // ============ Native Menu Bar Event Handlers ============
98
99 // Initialize menu event listeners when Tauri is available
100 if (window.__TAURI__) {
101 const { listen } = window.__TAURI__.event;
102
103 // Compose → main app: queue a send with an undo window.
104 // Fired by compose.html sendEmail() so the compose window can close
105 // immediately while the main app holds the 5 s undo toast.
106 listen('compose:queue-send', (event) => {
107 const payload = event?.payload || {};
108 if (payload.input) {
109 GoingsOn.emails.queueSend({
110 input: payload.input,
111 delaySeconds: payload.delaySeconds || 5,
112 });
113 }
114 });
115
116 // File menu
117 listen('menu:new_task', () => GoingsOn.tasks.openNew());
118 listen('menu:new_project', () => GoingsOn.projects.openNew());
119 listen('menu:import', () => GoingsOn.import.openModal());
120 listen('menu:save_view', () => GoingsOn.savedViews?.openSaveModal?.());
121
122 // View menu - tab shortcuts
123 listen('menu:view_work', () => GoingsOn.navigation.switchView('work'));
124 listen('menu:view_time', () => GoingsOn.navigation.switchView('time'));
125 listen('menu:view_messages', () => GoingsOn.navigation.switchView('messages'));
126 // View menu - individual sub-views
127 listen('menu:view_projects', () => GoingsOn.navigation.switchView('projects'));
128 listen('menu:view_tasks', () => GoingsOn.navigation.switchView('tasks'));
129 listen('menu:view_events', () => GoingsOn.navigation.switchView('events'));
130 listen('menu:view_emails', () => GoingsOn.navigation.switchView('emails'));
131 listen('menu:view_contacts', () => GoingsOn.navigation.switchView('contacts'));
132 listen('menu:view_day_plan', () => GoingsOn.navigation.switchView('day-plan'));
133 listen('menu:view_weekly_review', () => GoingsOn.navigation.switchView('weekly-review'));
134 listen('menu:view_monthly_review', () => GoingsOn.navigation.switchView('monthly-review'));
135 listen('menu:toggle_sidebar', () => GoingsOn.app.toggleSidebar());
136
137 // Tools menu
138 listen('menu:sync_email', () => GoingsOn.app.syncAllEmailAccounts());
139 listen('menu:settings', () => GoingsOn.settings.open());
140
141 // Help menu
142 listen('menu:keyboard_shortcuts', () => GoingsOn.keyboard.toggleShortcuts());
143 listen('menu:about', () => GoingsOn.app.openAboutModal());
144
145 // Database external change detection
146 listen('db:external-change', () => {
147 GoingsOn.cache.invalidateAll();
148 refreshCurrentViewData();
149 });
150
151 // Cloud sync: remote changes applied
152 listen('sync:changes-applied', () => {
153 GoingsOn.cache.invalidateAll();
154 refreshCurrentViewData();
155 });
156
157 // Cloud sync: subscription required (402 from server)
158 listen('sync:subscription-required', () => {
159 GoingsOn.ui.showToast('Cloud sync paused — subscription required', 'error', {
160 action: { label: 'Subscribe', fn: () => GoingsOn.settings.openCloudSync() },
161 duration: 10000,
162 });
163 });
164
165 // Cloud sync: status changed (syncing/idle/error)
166 listen('sync:status-changed', (event) => {
167 const dot = document.getElementById('sync-dot');
168 const indicator = document.getElementById('sync-indicator');
169 if (!dot || !indicator) return;
170 indicator.classList.remove('hidden');
171 dot.className = 'sync-dot';
172 if (event.payload === 'syncing') {
173 dot.classList.add('syncing');
174 } else if (event.payload === 'error') {
175 dot.classList.add('error');
176 } else {
177 dot.classList.add('connected');
178 }
179 });
180 }
181
182 // ============ External Change Handler ============
183
184 /**
185 * Refresh the current view's data without full navigation.
186 * Used when external changes are detected (e.g., an external process modified the database).
187 */
188 async function refreshCurrentViewData() {
189 // Don't refresh if a modal is open (user is editing something)
190 if (document.querySelector('.modal:not(.hidden)')) {
191 return;
192 }
193
194 const currentView = GoingsOn.navigation?.getCurrentView?.() || 'tasks';
195
196 try {
197 // Refresh projects cache first (needed for dropdowns)
198 const projects = await GoingsOn.api.projects.list();
199 GoingsOn.projects.setCache(projects);
200
201 // Reload the current view's data
202 await GoingsOn.navigation.loadViewData(currentView);
203 } catch (err) {
204 console.error('Failed to refresh view after external change:', err);
205 }
206 }
207
208 // ============ Sidebar Toggle ============
209
210 function toggleSidebar() {
211 const sidebar = document.querySelector('.saved-views-sidebar');
212 if (sidebar) {
213 sidebar.classList.toggle('hidden');
214 }
215 }
216
217 // ============ Email Sync ============
218
219 async function syncAllEmailAccounts() {
220 try {
221 const accounts = await GoingsOn.api.emailAccounts.list();
222 if (accounts.length === 0) {
223 GoingsOn.ui.showToast('No email accounts configured', 'info');
224 return;
225 }
226 GoingsOn.ui.showToast('Syncing email accounts...', 'info');
227 for (const account of accounts) {
228 await GoingsOn.api.emailAccounts.sync(account.id, false);
229 }
230 GoingsOn.ui.showToast('Email sync complete!', 'success');
231 GoingsOn.emails.load();
232 } catch (err) {
233 GoingsOn.ui.showToast('Email sync failed: ' + GoingsOn.utils.getErrorMessage(err), 'error', {
234 action: { label: 'Retry', fn: syncAllEmailAccounts },
235 duration: 8000,
236 });
237 }
238 }
239
240 // ============ About Modal ============
241
242 async function openAboutModal() {
243 let appVersion = "unknown";
244 try { appVersion = await window.__TAURI__.app.getVersion(); } catch (_) {}
245 const content = `
246 <div class="about-panel">
247 <h2 class="about-title">GoingsOn</h2>
248 <p class="about-tagline">Tasks, email, calendar, contacts.</p>
249 <p class="about-version">Version ${appVersion}</p>
250 <dl class="about-info-list">
251 <dt>Publisher</dt><dd>Make Creative, LLC</dd>
252 <dt>License</dt><dd>PolyForm Noncommercial 1.0.0</dd>
253 <dt>Contact</dt><dd><a href="mailto:info@makenot.work">info@makenot.work</a></dd>
254 <dt>Source</dt><dd><a href="https://makenot.work" target="_blank" rel="noopener">makenot.work</a></dd>
255 <dt>Privacy</dt><dd><a href="https://makenot.work/policy" target="_blank" rel="noopener">makenot.work/policy</a></dd>
256 </dl>
257 <p class="about-copyright">&copy; 2026 Make Creative, LLC</p>
258 </div>
259 <div class="form-actions">
260 <button type="button" class="btn btn-secondary" onclick="GoingsOn.ui.closeModal()">Close</button>
261 </div>
262 `;
263 GoingsOn.ui.openModal('About', content);
264 }
265
266 // ============ Populate GoingsOn.app Namespace ============
267
268 function showWelcome() {
269 const isTouch = !!GoingsOn.touch?.isTouchDevice;
270 const step1 = isTouch
271 ? '<strong>1.</strong> Create your first task &mdash; tap the <strong>+</strong> tab to quick add'
272 : '<strong>1.</strong> Create your first task &mdash; press <kbd>q</kbd> for quick add';
273 const step2 = isTouch
274 ? '<strong>2.</strong> Plan your day &mdash; tap or long-press the timeline to schedule'
275 : '<strong>2.</strong> Plan your day &mdash; drag tasks onto the timeline';
276 const shortcutsHint = isTouch
277 ? ''
278 : '<p class="welcome-hint">Press <kbd>?</kbd> anytime for keyboard shortcuts.</p>';
279 const content = `
280 <div class="welcome-panel">
281 <p class="welcome-intro">
282 GoingsOn brings your tasks, email, calendar, and contacts into one place.
283 </p>
284 <div class="welcome-section">
285 <h3 class="welcome-subhead">Get Started</h3>
286 <div class="welcome-step-stack">
287 <button class="btn btn-secondary text-left" onclick="localStorage.setItem('go-welcomed', '1'); GoingsOn.ui.closeModal(); GoingsOn.keyboard.openQuickAdd();">${step1}</button>
288 <button class="btn btn-secondary text-left" onclick="localStorage.setItem('go-welcomed', '1'); GoingsOn.ui.closeModal(); GoingsOn.navigation.switchView('day-plan');">${step2}</button>
289 <button class="btn btn-secondary text-left" onclick="localStorage.setItem('go-welcomed', '1'); GoingsOn.ui.closeModal(); GoingsOn.emails.openAccountsModal();"><strong>3.</strong> Add an email account</button>
290 </div>
291 </div>
292 <div class="welcome-section">
293 <h3 class="welcome-subhead welcome-subhead--tight">Three Tabs</h3>
294 <ul class="welcome-tabs-list">
295 <li><strong>Work</strong> &mdash; tasks &amp; projects</li>
296 <li><strong>Time</strong> &mdash; day plan, weekly review &amp; calendar</li>
297 <li><strong>Messages</strong> &mdash; email &amp; contacts</li>
298 </ul>
299 </div>
300 ${shortcutsHint}
301 </div>
302 <div class="form-actions">
303 <button class="btn btn-primary" onclick="localStorage.setItem('go-welcomed', '1'); GoingsOn.ui.closeModal()">Get Started</button>
304 </div>
305 `;
306 GoingsOn.ui.openModal('Welcome to GoingsOn', content);
307 }
308
309 /**
310 * Show a one-time dismissible hint toast. Sets localStorage key so it only shows once.
311 */
312 function showHint(storageKey, message) {
313 if (localStorage.getItem(storageKey)) return;
314 localStorage.setItem(storageKey, '1');
315 GoingsOn.ui.showToast(message, 'info', { duration: 5000 });
316 }
317
318 GoingsOn.app = {
319 toggleSidebar,
320 syncAllEmailAccounts,
321 openAboutModal,
322 refreshCurrentViewData,
323 showWelcome,
324 showHint,
325 };
326
327 // ============ Background/Foreground Transitions ============
328 // On mobile (and laptop sleep/wake), the app can sit hidden for arbitrary
329 // durations. Browsers throttle JS while hidden; the bigger issue is that
330 // rendered state (current time, sync status, lists) is stale on resume.
331 // If the app was hidden long enough that anything time-sensitive could have
332 // shifted, refresh on visibility return.
333 (function wireVisibilityRefresh() {
334 const STALE_THRESHOLD_MS = 30_000;
335 let hiddenAt = null;
336
337 document.addEventListener('visibilitychange', () => {
338 if (document.hidden) {
339 hiddenAt = Date.now();
340 return;
341 }
342 if (hiddenAt == null) return;
343 const hiddenFor = Date.now() - hiddenAt;
344 hiddenAt = null;
345 if (hiddenFor < STALE_THRESHOLD_MS) return;
346
347 // Refresh data and time-sensitive UI. Skip if a modal is open —
348 // refreshCurrentViewData already guards against that.
349 GoingsOn.cache?.invalidateAll?.();
350 refreshCurrentViewData();
351 GoingsOn.settings?.refreshSyncIndicator?.();
352 if (GoingsOn.dayPlanning?.updateCurrentTimeIndicator) {
353 GoingsOn.dayPlanning.updateCurrentTimeIndicator(true);
354 }
355 });
356 })();
357
358 })();
359