Skip to main content

max / goingson

18.1 KB · 395 lines History Blame Raw
1 /**
2 * GoingsOn - Settings Module
3 * Settings page with sidebar navigation.
4 */
5
6 (function() {
7 'use strict';
8 const esc = GoingsOn.utils.escapeHtml;
9 const escAttr = GoingsOn.utils.escapeAttrValue;
10
11 let currentSection = 'appearance';
12
13 // ============ Settings Page ============
14
15 /**
16 * Open settings as a modeless overlay above the current view. The
17 * underlying app stays mounted so theme/notification/account changes
18 * preview live against the real surfaces. (Phase 7 Tier 6.)
19 */
20 async function openSettings() {
21 const overlay = document.getElementById('settings-overlay');
22 if (!overlay) return;
23 overlay.classList.remove('hidden');
24 overlay.setAttribute('aria-hidden', 'false');
25 document.addEventListener('keydown', handleSettingsKeydown);
26 await showSection(currentSection);
27 }
28
29 /**
30 * Called by navigation.loadViewData when settings view becomes active.
31 * Renders the last-active section. Retained for back-compat with any
32 * remaining callers; new code should call openSettings.
33 */
34 async function loadSettings() {
35 await showSection(currentSection);
36 }
37
38 /**
39 * Close the settings overlay. Returns the user to whatever view was
40 * underneath — no view switch needed since settings is now an overlay,
41 * not a navigated subview.
42 */
43 function goBack() {
44 const overlay = document.getElementById('settings-overlay');
45 if (!overlay) return;
46 overlay.classList.add('hidden');
47 overlay.setAttribute('aria-hidden', 'true');
48 document.removeEventListener('keydown', handleSettingsKeydown);
49 }
50
51 function handleSettingsKeydown(e) {
52 if (e.key !== 'Escape') return;
53 const isTyping = ['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)
54 || e.target.isContentEditable;
55 if (isTyping) return;
56 goBack();
57 }
58
59 /**
60 * Show a specific settings section and update sidebar active state.
61 * @param {string} section - Section name (appearance, notifications, planning, sync, data)
62 */
63 async function showSection(section) {
64 currentSection = section;
65
66 // Update sidebar active state
67 document.querySelectorAll('.settings-nav-item').forEach(btn => {
68 btn.classList.toggle('active', btn.dataset.section === section);
69 });
70
71 const container = document.getElementById('settings-content');
72 if (!container) return;
73
74 switch (section) {
75 case 'appearance': await renderAppearance(container); break;
76 case 'notifications': renderNotifications(container); break;
77 case 'email': await GoingsOn.emails.renderAccountsSection(container); break;
78 case 'planning': renderPlanning(container); break;
79 case 'sync': await renderSync(container); break;
80 case 'data': await renderData(container); break;
81 case 'about': await renderAbout(container); break;
82 }
83 }
84
85 // ============ Section Renderers ============
86
87 async function renderAppearance(container) {
88 const savedTheme = localStorage.getItem('goingson-theme') || 'system';
89 const { light: lightThemes, dark: darkThemes, highContrast: highContrastThemes } = await GoingsOn.themes.getByType();
90
91 const highContrastGroup = highContrastThemes.length > 0
92 ? `<optgroup label="High Contrast">
93 ${highContrastThemes.map(t => `<option value="${escAttr(t.id)}" ${savedTheme === t.id ? 'selected' : ''}>${esc(t.name)}</option>`).join('')}
94 </optgroup>`
95 : '';
96
97 container.innerHTML = `
98 <div class="settings-section">
99 <h3 class="settings-heading">Appearance</h3>
100 <div class="form-group">
101 <label class="form-label">Theme</label>
102 <select id="theme-selector" class="form-select" data-change="themes.onChange" data-a1="@value">
103 <optgroup label="System">
104 <option value="system" ${savedTheme === 'system' ? 'selected' : ''}>Follow System</option>
105 </optgroup>
106 <optgroup label="Light Themes">
107 ${lightThemes.map(t => `<option value="${escAttr(t.id)}" ${savedTheme === t.id ? 'selected' : ''}>${esc(t.name)}</option>`).join('')}
108 </optgroup>
109 <optgroup label="Dark Themes">
110 ${darkThemes.map(t => `<option value="${escAttr(t.id)}" ${savedTheme === t.id ? 'selected' : ''}>${esc(t.name)}</option>`).join('')}
111 </optgroup>
112 ${highContrastGroup}
113 </select>
114 <p class="form-hint" style="margin-top: 0.5rem;">Choose a color theme for the interface.</p>
115 <div class="row-flex row-flex-2" style="margin-top: 0.5rem;">
116 <button class="btn btn-small" data-act="themes.importTheme">Import Theme</button>
117 <button class="btn btn-small" data-act="themes.exportTheme">Export Current</button>
118 </div>
119 </div>
120 </div>
121 `;
122 }
123
124 function renderNotifications(container) {
125 container.innerHTML = `
126 <div class="settings-section">
127 <h3 class="settings-heading">Notifications</h3>
128 <div class="form-group">
129 <label class="form-label">Event indicator lead time</label>
130 <select id="event-lead-time-selector" class="form-select" data-change="settings.onEventLeadTimeChange" data-a1="@value">
131 ${[5, 10, 15, 30, 60].map(m => {
132 const currentLead = parseInt(localStorage.getItem('goingson-event-lead-minutes') || '15', 10);
133 const label = m === 60 ? '1 hour' : m + ' minutes';
134 const suffix = m === 15 ? ' (default)' : '';
135 return '<option value="' + m + '"' + (currentLead === m ? ' selected' : '') + '>' + label + suffix + '</option>';
136 }).join('')}
137 </select>
138 <p class="form-hint" style="margin-top: 0.5rem;">How far in advance the Events tab dot turns yellow.</p>
139 </div>
140 </div>
141 `;
142 }
143
144 function renderPlanning(container) {
145 container.innerHTML = `
146 <div class="settings-section">
147 <h3 class="settings-heading">Planning & Review</h3>
148 <div class="form-group">
149 <label class="form-label">Work hours</label>
150 <div class="work-hours-row">
151 <select class="form-select" data-change="settings.onWorkHoursChange" data-a1="start" data-a2="@value">
152 ${buildHourOptions(parseInt(localStorage.getItem('goingson-work-start-hour') || '9', 10))}
153 </select>
154 <span>to</span>
155 <select class="form-select" data-change="settings.onWorkHoursChange" data-a1="end" data-a2="@value">
156 ${buildHourOptions(parseInt(localStorage.getItem('goingson-work-end-hour') || '17', 10))}
157 </select>
158 </div>
159 <p class="form-hint" style="margin-top: 0.5rem;">Controls when plan/review nudge dots appear.</p>
160 </div>
161 <div class="form-group">
162 <label class="form-label">Plan nudges</label>
163 <select class="form-select" data-change="ui.setLocalStorage" data-a1="goingson-plan-nudges" data-a2="@value">
164 <option value="enabled" ${localStorage.getItem('goingson-plan-nudges') !== 'disabled' ? 'selected' : ''}>Enabled (default)</option>
165 <option value="disabled" ${localStorage.getItem('goingson-plan-nudges') === 'disabled' ? 'selected' : ''}>Disabled</option>
166 </select>
167 </div>
168 <div class="form-group">
169 <label class="form-label">Review nudges</label>
170 <select class="form-select" data-change="ui.setLocalStorage" data-a1="goingson-review-nudges" data-a2="@value">
171 <option value="enabled" ${localStorage.getItem('goingson-review-nudges') !== 'disabled' ? 'selected' : ''}>Enabled (default)</option>
172 <option value="disabled" ${localStorage.getItem('goingson-review-nudges') === 'disabled' ? 'selected' : ''}>Disabled</option>
173 </select>
174 </div>
175 </div>
176 `;
177 }
178
179 async function renderSync(container) {
180 // Delegate to settings-sync.js which extends GoingsOn.settings
181 if (GoingsOn.settings.renderSyncSection) {
182 await GoingsOn.settings.renderSyncSection(container);
183 } else {
184 container.innerHTML = '<p class="text-secondary">Cloud sync module not loaded.</p>';
185 }
186 }
187
188 async function renderData(container) {
189 // Build backup settings inline
190 let backupHtml = '';
191 try {
192 const settings = await GoingsOn.api.export.getBackupSettings();
193 const lastBackupText = settings.lastBackupAt
194 ? `Last backup: ${new Date(settings.lastBackupAt).toLocaleString()}`
195 : 'No backups yet';
196
197 const frequencyOptions = [
198 { value: 15, label: 'Every 15 minutes (Recommended)' },
199 { value: 30, label: 'Every 30 minutes' },
200 { value: 60, label: 'Every hour' },
201 { value: 360, label: 'Every 6 hours' },
202 { value: 1440, label: 'Daily' },
203 ];
204
205 const retentionOptions = [
206 { value: 1, label: 'Keep 1 backup (Recommended)' },
207 { value: 3, label: 'Keep 3 backups' },
208 { value: 7, label: 'Keep 7 backups' },
209 { value: 14, label: 'Keep 14 backups' },
210 { value: 0, label: 'Keep all backups' },
211 ];
212
213 const renderFormField = GoingsOn.ui.renderFormField;
214 backupHtml = `
215 <div class="settings-section-block">
216 <h4 class="settings-subheading">Backups</h4>
217 <div class="settings-actions-row">
218 <button class="btn btn-secondary" data-act="export.createBackup">Create Backup</button>
219 <button class="btn btn-secondary" data-act="export.openBackupsModal">Manage Backups</button>
220 </div>
221
222 <h4 class="settings-subheading">Automatic Backups</h4>
223 <p class="settings-desc-block">
224 Automatic backups protect your data by creating compressed snapshots on a schedule.
225 </p>
226
227 <div class="form-group">
228 <label class="form-checkbox-label">
229 <input type="checkbox" id="backup-enabled" ${settings.autoBackupEnabled ? 'checked' : ''}>
230 <span>Enable automatic backups</span>
231 </label>
232 </div>
233 ${renderFormField({
234 kind: 'select',
235 name: 'backup-frequency',
236 id: 'backup-frequency',
237 label: 'Backup Frequency',
238 value: settings.backupFrequencyMinutes,
239 options: frequencyOptions.map(o => ({
240 value: String(o.value),
241 label: o.label,
242 selected: settings.backupFrequencyMinutes === o.value,
243 })),
244 })}
245 ${renderFormField({
246 kind: 'select',
247 name: 'backup-retention',
248 id: 'backup-retention',
249 label: 'Retention Policy',
250 value: settings.maxBackupsToKeep,
251 options: retentionOptions.map(o => ({
252 value: String(o.value),
253 label: o.label,
254 selected: settings.maxBackupsToKeep === o.value,
255 })),
256 hint: 'Older backups are automatically deleted to save space.',
257 })}
258 <div class="settings-actions-row settings-actions-row--center">
259 <button class="btn btn-primary" data-act="export.saveBackupSettings">Save Backup Settings</button>
260 <span class="settings-status-text">${esc(lastBackupText)}</span>
261 </div>
262 </div>
263 `;
264 } catch (err) {
265 backupHtml = `
266 <div class="settings-section-block">
267 <h4 class="settings-subheading">Backups</h4>
268 <div class="settings-actions-row">
269 <button class="btn btn-secondary" data-act="export.createBackup">Create Backup</button>
270 <button class="btn btn-secondary" data-act="export.openBackupsModal">Manage Backups</button>
271 </div>
272 </div>
273 `;
274 }
275
276 container.innerHTML = `
277 <div class="settings-section">
278 <h3 class="settings-heading">Import & Export</h3>
279 <p class="settings-desc">
280 Import data from external sources or export for safekeeping.
281 </p>
282
283 <div class="settings-actions-row">
284 <button class="btn btn-secondary" data-act="importExternal.openDialog">Import Contacts / Calendar</button>
285 <button class="btn btn-secondary" data-act="import.openModal">Import from CSV</button>
286 </div>
287
288 <div class="settings-actions-row">
289 <button class="btn btn-secondary" data-act="export.exportJSON">Export All (JSON)</button>
290 <button class="btn btn-secondary" data-act="export.exportTasksCSV">Export Tasks (CSV)</button>
291 <button class="btn btn-secondary" data-act="export.exportEventsICS">Export Calendar (ICS)</button>
292 </div>
293
294 ${backupHtml}
295 </div>
296 `;
297 }
298
299 // ============ About Section ============
300
301 /**
302 * Phase 7 Tier 2 #7 — surface the version + support info in Settings rather
303 * than only in the one-shot welcome modal. Required for support flows.
304 */
305 async function setUpdateCheckOnLaunch(enabled) {
306 try {
307 await GoingsOn.api.preferences.setUpdateCheckOnLaunch(enabled);
308 GoingsOn.ui.showToast(enabled ? 'Update checks enabled' : 'Update checks disabled');
309 } catch (err) {
310 GoingsOn.ui.showToast('Failed to save preference: ' + GoingsOn.utils.getErrorMessage(err), 'error');
311 }
312 }
313
314 async function renderAbout(container) {
315 let appVersion = 'unknown';
316 try { appVersion = await window.__TAURI__.app.getVersion(); } catch (_) {}
317
318 const platform = (navigator.userAgentData?.platform || navigator.platform || 'unknown');
319
320 let updateCheckOnLaunch = true;
321 try {
322 const prefs = await GoingsOn.api.preferences.get();
323 updateCheckOnLaunch = prefs.updateCheckOnLaunch !== false;
324 } catch (_) {}
325
326 container.innerHTML = `
327 <div class="settings-section">
328 <h3 class="settings-heading">About GoingsOn</h3>
329 <p class="settings-desc">Tasks, email, calendar, contacts.</p>
330
331 <dl class="about-info-list">
332 <dt>Version</dt><dd id="about-app-version">${esc(appVersion)}</dd>
333 <dt>Platform</dt><dd>${esc(platform)}</dd>
334 <dt>Publisher</dt><dd>Make Creative, LLC</dd>
335 <dt>License</dt><dd>PolyForm Noncommercial 1.0.0</dd>
336 <dt>Contact</dt><dd><a href="mailto:info@makenot.work">info@makenot.work</a></dd>
337 <dt>Source</dt><dd><a href="https://makenot.work" target="_blank" rel="noopener">makenot.work</a></dd>
338 <dt>Privacy</dt><dd><a href="https://makenot.work/policy" target="_blank" rel="noopener">makenot.work/policy</a></dd>
339 </dl>
340
341 <div class="settings-toggle-row">
342 <div>
343 <p class="settings-subheading">Check for updates on launch</p>
344 <p class="settings-desc">When a new signed release is available, a banner appears in the app. Install is always user-initiated.</p>
345 </div>
346 <label class="toggle-switch">
347 <input type="checkbox" ${updateCheckOnLaunch ? 'checked' : ''}
348 data-change="settings.setUpdateCheckOnLaunch" data-a1="@checked">
349 <span class="toggle-slider"></span>
350 </label>
351 </div>
352
353 <p class="settings-desc-block about-copyright">&copy; 2026 Make Creative, LLC</p>
354 </div>
355 `;
356 }
357
358
359 // ============ Helpers ============
360
361 function onEventLeadTimeChange(value) {
362 localStorage.setItem('goingson-event-lead-minutes', value);
363 if (GoingsOn.events && GoingsOn.events.updateEventStatusDot) {
364 GoingsOn.events.updateEventStatusDot();
365 }
366 }
367
368 function buildHourOptions(selected) {
369 let html = '';
370 for (let h = 0; h < 24; h++) {
371 const label = h === 0 ? '12:00 AM' : h < 12 ? `${h}:00 AM` : h === 12 ? '12:00 PM' : `${h - 12}:00 PM`;
372 html += `<option value="${h}" ${h === selected ? 'selected' : ''}>${label}</option>`;
373 }
374 return html;
375 }
376
377 function onWorkHoursChange(which, value) {
378 const key = which === 'start' ? 'goingson-work-start-hour' : 'goingson-work-end-hour';
379 localStorage.setItem(key, value);
380 }
381
382 // ============ Populate GoingsOn Namespace ============
383
384 GoingsOn.settings = {
385 open: openSettings,
386 load: loadSettings,
387 showSection,
388 goBack,
389 onEventLeadTimeChange,
390 onWorkHoursChange,
391 setUpdateCheckOnLaunch,
392 };
393
394 })();
395