/** * GoingsOn - Settings Module * Settings page with sidebar navigation, plugin manager */ (function() { 'use strict'; const esc = GoingsOn.utils.escapeHtml; const escAttr = GoingsOn.utils.escapeAttr; let currentSection = 'appearance'; // ============ Settings Page ============ /** * Open settings as a modeless overlay above the current view. The * underlying app stays mounted so theme/notification/account changes * preview live against the real surfaces. (Phase 7 Tier 6.) */ async function openSettings() { const overlay = document.getElementById('settings-overlay'); if (!overlay) return; overlay.classList.remove('hidden'); overlay.setAttribute('aria-hidden', 'false'); document.addEventListener('keydown', handleSettingsKeydown); await showSection(currentSection); } /** * Called by navigation.loadViewData when settings view becomes active. * Renders the last-active section. Retained for back-compat with any * remaining callers; new code should call openSettings. */ async function loadSettings() { await showSection(currentSection); } /** * Close the settings overlay. Returns the user to whatever view was * underneath — no view switch needed since settings is now an overlay, * not a navigated subview. */ function goBack() { const overlay = document.getElementById('settings-overlay'); if (!overlay) return; overlay.classList.add('hidden'); overlay.setAttribute('aria-hidden', 'true'); document.removeEventListener('keydown', handleSettingsKeydown); } function handleSettingsKeydown(e) { if (e.key !== 'Escape') return; const isTyping = ['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName) || e.target.isContentEditable; if (isTyping) return; goBack(); } /** * Show a specific settings section and update sidebar active state. * @param {string} section - Section name (appearance, notifications, planning, plugins, sync, data) */ async function showSection(section) { currentSection = section; // Update sidebar active state document.querySelectorAll('.settings-nav-item').forEach(btn => { btn.classList.toggle('active', btn.dataset.section === section); }); const container = document.getElementById('settings-content'); if (!container) return; switch (section) { case 'appearance': await renderAppearance(container); break; case 'notifications': renderNotifications(container); break; case 'email': await GoingsOn.emails.renderAccountsSection(container); break; case 'planning': renderPlanning(container); break; case 'plugins': await renderPlugins(container); break; case 'sync': await renderSync(container); break; case 'data': await renderData(container); break; case 'about': await renderAbout(container); break; } } // ============ Section Renderers ============ async function renderAppearance(container) { const savedTheme = localStorage.getItem('goingson-theme') || 'system'; const { light: lightThemes, dark: darkThemes, highContrast: highContrastThemes } = await GoingsOn.themes.getByType(); const highContrastGroup = highContrastThemes.length > 0 ? ` ${highContrastThemes.map(t => ``).join('')} ` : ''; container.innerHTML = `

Appearance

Choose a color theme for the interface.

`; } function renderNotifications(container) { container.innerHTML = `

Notifications

How far in advance the Events tab dot turns yellow.

`; } function renderPlanning(container) { container.innerHTML = `

Planning & Review

to

Controls when plan/review nudge dots appear.

`; } async function renderPlugins(container) { let allPlugins = []; let enabledPlugins = []; try { [allPlugins, enabledPlugins] = await Promise.all([ GoingsOn.api.plugins.listImport(), GoingsOn.api.plugins.listEnabled(), ]); } catch (err) { container.innerHTML = `

Failed to load plugins: ${esc(GoingsOn.utils.getErrorMessage(err))}

`; return; } const enabledIds = new Set(enabledPlugins.map(p => p.id)); const pluginsList = allPlugins.length === 0 ? '

No plugins installed. Drop a plugin file into the GoingsOn plugins directory inside your OS app-data location.

' : allPlugins.map(plugin => renderPluginItem(plugin, enabledIds.has(plugin.id))).join(''); container.innerHTML = `

Plugins

Plugins extend GoingsOn with support for importing data from various formats.

${pluginsList}
`; } async function renderSync(container) { // Delegate to settings-sync.js which extends GoingsOn.settings if (GoingsOn.settings.renderSyncSection) { await GoingsOn.settings.renderSyncSection(container); } else { container.innerHTML = '

Cloud sync module not loaded.

'; } } async function renderData(container) { // Build backup settings inline let backupHtml = ''; try { const settings = await GoingsOn.api.export.getBackupSettings(); const lastBackupText = settings.lastBackupAt ? `Last backup: ${new Date(settings.lastBackupAt).toLocaleString()}` : 'No backups yet'; const frequencyOptions = [ { value: 15, label: 'Every 15 minutes (Recommended)' }, { value: 30, label: 'Every 30 minutes' }, { value: 60, label: 'Every hour' }, { value: 360, label: 'Every 6 hours' }, { value: 1440, label: 'Daily' }, ]; const retentionOptions = [ { value: 1, label: 'Keep 1 backup (Recommended)' }, { value: 3, label: 'Keep 3 backups' }, { value: 7, label: 'Keep 7 backups' }, { value: 14, label: 'Keep 14 backups' }, { value: 0, label: 'Keep all backups' }, ]; const renderFormField = GoingsOn.ui.renderFormField; backupHtml = `

Backups

Automatic Backups

Automatic backups protect your data by creating compressed snapshots on a schedule.

${renderFormField({ kind: 'select', name: 'backup-frequency', id: 'backup-frequency', label: 'Backup Frequency', value: settings.backupFrequencyMinutes, options: frequencyOptions.map(o => ({ value: String(o.value), label: o.label, selected: settings.backupFrequencyMinutes === o.value, })), })} ${renderFormField({ kind: 'select', name: 'backup-retention', id: 'backup-retention', label: 'Retention Policy', value: settings.maxBackupsToKeep, options: retentionOptions.map(o => ({ value: String(o.value), label: o.label, selected: settings.maxBackupsToKeep === o.value, })), hint: 'Older backups are automatically deleted to save space.', })}
${esc(lastBackupText)}
`; } catch (err) { backupHtml = `

Backups

`; } container.innerHTML = `

Import & Export

Import data from external sources or export for safekeeping.

${backupHtml}
`; } // ============ About Section ============ /** * Phase 7 Tier 2 #7 — surface the version + support info in Settings rather * than only in the one-shot welcome modal. Required for support flows. */ async function setUpdateCheckOnLaunch(enabled) { try { await GoingsOn.api.preferences.setUpdateCheckOnLaunch(enabled); GoingsOn.ui.showToast(enabled ? 'Update checks enabled' : 'Update checks disabled'); } catch (err) { GoingsOn.ui.showToast('Failed to save preference: ' + GoingsOn.utils.getErrorMessage(err), 'error'); } } async function renderAbout(container) { let appVersion = 'unknown'; try { appVersion = await window.__TAURI__.app.getVersion(); } catch (_) {} const platform = (navigator.userAgentData?.platform || navigator.platform || 'unknown'); let updateCheckOnLaunch = true; try { const prefs = await GoingsOn.api.preferences.get(); updateCheckOnLaunch = prefs.updateCheckOnLaunch !== false; } catch (_) {} container.innerHTML = `

About GoingsOn

Tasks, email, calendar, contacts.

Version
${esc(appVersion)}
Platform
${esc(platform)}
Publisher
Make Creative, LLC
License
PolyForm Noncommercial 1.0.0
Contact
info@makenot.work
Source
makenot.work
Privacy
makenot.work/policy

Check for updates on launch

When a new signed release is available, a banner appears in the app. Install is always user-initiated.

`; } // ============ Plugin Item Renderer ============ function renderPluginItem(plugin, isEnabled) { const config = plugin.plugin_type?.import || {}; const extensions = config.file_extensions || []; const entityTypes = config.entity_types || []; return `
${esc(plugin.name)} v${esc(plugin.version)}

${esc(plugin.description)}

Files: .${extensions.join(', .')} | Types: ${entityTypes.join(', ')}
`; } // ============ Plugin Toggle ============ async function togglePlugin(pluginId, enabled) { try { if (enabled) { await GoingsOn.api.plugins.enable(pluginId); GoingsOn.ui.showToast('Plugin enabled'); } else { await GoingsOn.api.plugins.disable(pluginId); GoingsOn.ui.showToast('Plugin disabled'); } } catch (err) { GoingsOn.ui.showToast('Failed to update plugin: ' + GoingsOn.utils.getErrorMessage(err), 'error'); // Re-render plugins section to show correct state const container = document.getElementById('settings-content'); if (container) await renderPlugins(container); } } // ============ Helpers ============ function onEventLeadTimeChange(value) { localStorage.setItem('goingson-event-lead-minutes', value); if (GoingsOn.events && GoingsOn.events.updateEventStatusDot) { GoingsOn.events.updateEventStatusDot(); } } function buildHourOptions(selected) { let html = ''; for (let h = 0; h < 24; h++) { const label = h === 0 ? '12:00 AM' : h < 12 ? `${h}:00 AM` : h === 12 ? '12:00 PM' : `${h - 12}:00 PM`; html += ``; } return html; } function onWorkHoursChange(which, value) { const key = which === 'start' ? 'goingson-work-start-hour' : 'goingson-work-end-hour'; localStorage.setItem(key, value); } // ============ Populate GoingsOn Namespace ============ GoingsOn.settings = { open: openSettings, load: loadSettings, showSection, goBack, openPluginsModal: () => showSection('plugins'), togglePlugin, onEventLeadTimeChange, onWorkHoursChange, setUpdateCheckOnLaunch, }; })();