Skip to main content

max / goingson

18.8 KB · 359 lines History Blame Raw
1 /**
2 * @fileoverview Tauri IPC abstraction layer.
3 *
4 * Wraps every Rust `#[tauri::command]` behind a thin JS method so the UI
5 * never calls `__TAURI__.core.invoke` directly. Methods are grouped by
6 * domain (projects, tasks, emails, …) and exposed on `GoingsOn.api`.
7 *
8 * Each method maps 1:1 to a Tauri command — the method name documents
9 * which command is invoked, and the arguments mirror the Rust serde input.
10 */
11
12 (function() {
13 'use strict';
14
15 // Wait for Tauri to be ready
16 let tauriInvoke = null;
17
18 // Initialize Tauri invoke function
19 async function initTauri() {
20 if (window.__TAURI__) {
21 tauriInvoke = window.__TAURI__.core.invoke;
22 return true;
23 }
24 return false;
25 }
26
27 // Try to init immediately
28 initTauri();
29
30 /**
31 * Invoke a Tauri command. Lazily initializes the `__TAURI__` reference on
32 * first call — this handles cases where the script loads before the Tauri
33 * runtime is injected (e.g. in dev mode with slow WebView init).
34 */
35 async function invoke(command, args = {}) {
36 if (!tauriInvoke) {
37 await initTauri();
38 }
39 if (!tauriInvoke) {
40 throw new Error('Tauri not available');
41 }
42 try {
43 return await tauriInvoke(command, args);
44 } catch (err) {
45 console.error(`[api] invoke '${command}' failed:`, err, 'args:', JSON.stringify(args));
46 throw err;
47 }
48 }
49
50 // ============ API Object ============
51
52 const api = {
53 // Projects
54 projects: {
55 list: () => invoke('list_projects'),
56 get: (id) => invoke('get_project', { id }),
57 create: (input) => invoke('create_project', { input }),
58 update: (id, input) => invoke('update_project', { id, input }),
59 delete: (id) => invoke('delete_project', { id }),
60 },
61
62 // Tasks — CRUD + lifecycle (start/complete) + snooze/waiting status
63 tasks: {
64 list: () => invoke('list_tasks'),
65 listFiltered: (filters) => invoke('list_tasks_filtered', { filters }), // Server-side filter + paginate
66 listByProject: (projectId) => invoke('list_tasks_for_project', { projectId }),
67 get: (id) => invoke('get_task', { id }),
68 getOverview: (id) => invoke('get_task_overview', { id }),
69 create: (input) => invoke('create_task', { input }),
70 quickAdd: (text) => invoke('quick_add_task', { input: { text } }), // Natural language: "Fix bug +work @tomorrow !high"
71 update: (id, input) => invoke('update_task', { id, input }),
72 delete: (id) => invoke('delete_task', { id }),
73 start: (id) => invoke('start_task', { id }), // Pending → Started (sets started_at)
74 complete: (id) => invoke('complete_task', { id }), // Spawns next instance for recurring tasks
75 listSnoozed: () => invoke('list_snoozed_tasks'),
76 snooze: (id, until) => invoke('snooze_task', { id, input: { until } }),
77 unsnooze: (id) => invoke('unsnooze_task', { id }),
78 listWaiting: () => invoke('list_waiting_tasks'),
79 markWaiting: (id, expectedResponse) => invoke('mark_task_waiting', { id, input: { expectedResponseDate: expectedResponse } }),
80 clearWaiting: (id) => invoke('clear_task_waiting', { id }),
81 },
82
83 // Annotations
84 annotations: {
85 list: (taskId) => invoke('list_annotations', { taskId }),
86 add: (taskId, note) => invoke('add_annotation', { taskId, input: { note } }),
87 delete: (taskId, annotationId) => invoke('delete_annotation', { annotationId }),
88 },
89
90 // Subtasks
91 subtasks: {
92 list: (taskId) => invoke('list_subtasks', { taskId }),
93 add: (taskId, text) => invoke('add_subtask', { taskId, input: { text } }),
94 addLink: (taskId, linkedTaskId) => invoke('add_subtask_link', { taskId, linkedTaskId }),
95 toggle: (taskId, subtaskId) => invoke('toggle_subtask', { subtaskId }),
96 update: (taskId, subtaskId, text) => invoke('update_subtask', { subtaskId, input: { text } }),
97 delete: (taskId, subtaskId) => invoke('delete_subtask', { subtaskId }),
98 },
99
100 // Events
101 events: {
102 list: () => invoke('list_events'),
103 listByProject: (projectId) => invoke('list_events_for_project', { projectId }),
104 listUpcoming: () => invoke('list_upcoming_events'),
105 get: (id) => invoke('get_event', { id }),
106 create: (input) => invoke('create_event', { input }),
107 update: (id, input) => invoke('update_event', { id, input }),
108 delete: (id) => invoke('delete_event', { id }),
109 bulkDelete: (ids) => invoke('bulk_delete_events', { ids }),
110 listBetween: (start, end) => invoke('list_events_between', { start, end }),
111 getStatusIndicator: (leadMinutes) => invoke('get_event_status_indicator', { leadMinutes }),
112 listSnoozed: () => invoke('list_snoozed_events'),
113 snooze: (id, until) => invoke('snooze_event', { id, input: { until } }),
114 unsnooze: (id) => invoke('unsnooze_event', { id }),
115 },
116
117 // Emails — CRUD + threading + status (read/archive/snooze/waiting)
118 emails: {
119 list: (includeArchived = false) => invoke('list_emails', { includeArchived }),
120 listThreaded: (params = {}) => invoke('list_emails_threaded', { params }), // Groups by thread_id, paginated
121 listByProject: (projectId) => invoke('list_emails_for_project', { projectId }),
122 listUnlinked: () => invoke('list_unlinked_emails'), // Emails not linked to any project
123 get: (id) => invoke('get_email', { id }),
124 create: (input) => invoke('create_email', { input }),
125 send: (input) => invoke('send_email', { input }), // SMTP send + save copy to local DB
126 delete: (id) => invoke('delete_email', { id }),
127 markRead: (id) => invoke('mark_email_read', { id }),
128 markUnread: (id) => invoke('mark_email_unread', { id }),
129 archive: (id) => invoke('archive_email', { id }), // Also moves on IMAP server if available
130 unarchive: (id) => invoke('unarchive_email', { id }),
131 markAllRead: () => invoke('mark_all_emails_read'),
132 linkToProject: (id, projectId) => invoke('link_email_to_project', { id, input: { projectId } }),
133 getUnreadCount: () => invoke('get_unread_email_count'),
134 listSnoozed: () => invoke('list_snoozed_emails'),
135 snooze: (id, until) => invoke('snooze_email', { id, input: { until } }),
136 unsnooze: (id) => invoke('unsnooze_email', { id }),
137 listWaiting: () => invoke('list_waiting_emails'),
138 markWaiting: (id, expectedResponse) => invoke('mark_email_waiting', { id, input: { expectedResponseDate: expectedResponse } }),
139 clearWaiting: (id) => invoke('clear_email_waiting', { id }),
140 listByThread: (threadId) => invoke('list_emails_by_thread', { threadId }),
141 saveDraft: (input) => invoke('save_email_draft', { input }),
142 listDrafts: () => invoke('list_email_drafts'),
143 sendDraft: (id) => invoke('send_email_draft', { id }),
144 setLabels: (id, labels) => invoke('set_email_labels', { id, labels }),
145 listFolders: () => invoke('list_email_folders'),
146 listLabels: () => invoke('list_email_labels'),
147 moveToFolder: (id, folder) => invoke('move_email_to_folder', { id, folder }),
148 },
149
150 // Contacts — CRUD + multi-value fields (emails, phones, social, custom)
151 contacts: {
152 list: () => invoke('list_contacts'),
153 get: (id) => invoke('get_contact', { id }),
154 create: (input) => invoke('create_contact', { input }),
155 update: (id, input) => invoke('update_contact', { id, input }),
156 delete: (id) => invoke('delete_contact', { id }),
157 bulkDelete: (ids) => invoke('bulk_delete_contacts', { ids }),
158 bulkTag: (ids, tag) => invoke('bulk_tag_contacts', { ids, tag }),
159 addEmail: (contactId, input) => invoke('add_contact_email', { contactId, input }),
160 removeEmail: (emailId) => invoke('remove_contact_email', { emailId }),
161 updateEmail: (emailId, input) => invoke('update_contact_email', { emailId, input }),
162 addPhone: (contactId, input) => invoke('add_contact_phone', { contactId, input }),
163 removePhone: (phoneId) => invoke('remove_contact_phone', { phoneId }),
164 updatePhone: (phoneId, input) => invoke('update_contact_phone', { phoneId, input }),
165 addSocialHandle: (contactId, input) => invoke('add_contact_social_handle', { contactId, input }),
166 removeSocialHandle: (handleId) => invoke('remove_contact_social_handle', { handleId }),
167 updateSocialHandle: (handleId, input) => invoke('update_contact_social_handle', { handleId, input }),
168 addCustomField: (contactId, input) => invoke('add_contact_custom_field', { contactId, input }),
169 removeCustomField: (fieldId) => invoke('remove_contact_custom_field', { fieldId }),
170 updateCustomField: (fieldId, input) => invoke('update_contact_custom_field', { fieldId, input }),
171 findByEmail: (email) => invoke('find_contact_by_email', { email }), // Reverse lookup for email sender → contact
172 validateAddresses: (addresses) => invoke('validate_email_addresses', { addresses }),
173 promoteContact: (id) => invoke('promote_contact', { id }),
174 listTasksForContact: (contactId) => invoke('list_tasks_for_contact', { contactId }),
175 listEventsForContact: (contactId) => invoke('list_events_for_contact', { contactId }),
176 listEmailsForContact: (contactId) => invoke('list_emails_for_contact', { contactId }),
177 listFiltered: (search, tag, includeImplicit) => invoke('list_contacts_filtered', { search: search || null, tag: tag || null, includeImplicit: includeImplicit ?? false }),
178 },
179
180 // Email Accounts — IMAP/JMAP account configuration and sync triggers
181 emailAccounts: {
182 list: () => invoke('list_email_accounts'),
183 get: (id) => invoke('get_email_account', { id }),
184 create: (input) => invoke('create_email_account', { input }),
185 update: (id, input) => invoke('update_email_account', { id, input }),
186 updateSyncInterval: (id, syncIntervalMinutes) => invoke('update_email_sync_interval', { id, input: { syncIntervalMinutes } }),
187 updateSignature: (id, emailSignature) => invoke('update_email_signature', { id, input: { emailSignature } }),
188 updateNotify: (id, enabled) => invoke('update_email_notify', { id, enabled }),
189 delete: (id) => invoke('delete_email_account', { id }),
190 test: (id) => invoke('test_email_account', { id }), // Verify IMAP/SMTP credentials
191 sync: (id, fullSync = false) => invoke('sync_email_account', { id, fullSync }), // fullSync ignores last_sync_at
192 },
193
194 // Stats
195 stats: {
196 getDashboard: () => invoke('get_dashboard_stats'),
197 },
198
199 // App metadata
200 app: {
201 getChangelog: () => invoke('get_changelog'),
202 },
203
204 // Day Planning
205 dayPlanning: {
206 getDay: (date) => invoke('get_day_planning', { date }),
207 scheduleTask: (id, input) => invoke('schedule_task', { id, input }),
208 unscheduleTask: (id) => invoke('unschedule_task', { id }),
209 },
210
211 // Snooze
212 snooze: {
213 getOptions: () => invoke('get_snooze_options'),
214 },
215
216 // OAuth — provider-based auth flow (Google, Microsoft, Yahoo, Fastmail)
217 oauth: {
218 listProviders: () => invoke('list_oauth_providers'),
219 start: (providerId) => invoke('start_oauth', { providerId }), // Opens browser for consent
220 complete: (input) => invoke('complete_oauth', { input }), // Exchanges auth code for tokens
221 refreshTokens: (accountId) => invoke('refresh_oauth_tokens', { accountId }),
222 disconnect: (accountId) => invoke('disconnect_oauth', { accountId }),
223 reconnect: (accountId) => invoke('reconnect_oauth', { accountId }),
224 },
225
226 // Search — full-text search across all entity types
227 search: {
228 query: (input) => invoke('search', { input }),
229 },
230
231 // Export/Backup — data export (JSON/CSV/ICS) and automatic backup management
232 export: {
233 getSummary: () => invoke('get_export_summary'), // Counts per entity type for the export dialog
234 json: (filePath) => invoke('export_json', { filePath }),
235 tasksCSV: (filePath, projectId = null) => invoke('export_tasks_csv', { filePath, projectId }),
236 eventsICS: (filePath, includePast = true) => invoke('export_events_ics', { filePath, includePast }),
237 createBackup: () => invoke('create_backup'), // Full SQLite snapshot
238 listBackups: () => invoke('list_backups'),
239 restoreBackup: (filePath, options) => invoke('restore_backup', { filePath, options }),
240 deleteBackup: (filePath) => invoke('delete_backup', { filePath }),
241 getBackupSettings: () => invoke('get_backup_settings'),
242 saveBackupSettings: (settings) => invoke('save_backup_settings', { input: settings }),
243 },
244
245 // Daily Notes — per-day reflection (persisted to SQLite)
246 dailyNotes: {
247 get: (date) => invoke('get_daily_note', { date }),
248 upsert: (input) => invoke('upsert_daily_note', { input }),
249 },
250
251 // Weekly Review — GTD-style reflection: focus tasks, vacation, nudges
252 weeklyReview: {
253 get: (weekStart = null) => invoke('get_weekly_review', { input: { weekStart } }),
254 complete: (notes, weekStart = null) => invoke('complete_weekly_review', { input: { notes, weekStart } }),
255 setFocus: (id, isFocus) => invoke('set_task_focus', { id, input: { isFocus } }),
256 clearAllFocus: () => invoke('clear_all_focus'),
257 checkNudge: () => invoke('check_weekly_review_nudge'),
258 setVacationDays: (days, weekStart = null) => invoke('set_vacation_days', { input: { days, weekStart } }),
259 },
260
261 // Monthly Review — reflection, goal-setting, heat-map calendar
262 monthlyReview: {
263 get: (month = null) => invoke('get_monthly_review', { input: { month } }),
264 upsertGoal: (month, text, position) => invoke('upsert_monthly_goal', { input: { month, text, position } }),
265 updateGoalStatus: (id, status) => invoke('update_monthly_goal_status', { id, input: { status } }),
266 deleteGoal: (id) => invoke('delete_monthly_goal', { id }),
267 saveReflection: (month, highlight, change) => invoke('save_monthly_reflection', { input: { month, highlight, change } }),
268 },
269
270 // Milestones
271 milestones: {
272 list: (projectId) => invoke('list_milestones', { projectId }),
273 create: (input) => invoke('create_milestone', { input }),
274 update: (id, input) => invoke('update_milestone', { id, input }),
275 delete: (id) => invoke('delete_milestone', { id }),
276 reorder: (projectId, input) => invoke('reorder_milestones', { projectId, input }),
277 },
278
279 // Sync — cross-device sync with E2E encryption
280 sync: {
281 getTiers: () => invoke('sync_get_tiers'),
282 status: () => invoke('sync_status'),
283 startAuth: () => invoke('sync_start_auth'),
284 completeAuth: (input) => invoke('sync_complete_auth', { input }),
285 disconnect: () => invoke('sync_disconnect'),
286 syncNow: () => invoke('sync_now'),
287 setupEncryptionNew: (password) => invoke('sync_setup_encryption_new', { password }), // First device
288 setupEncryptionExisting: (password) => invoke('sync_setup_encryption_existing', { password }), // Join existing
289 updateSettings: (input) => invoke('sync_update_settings', { input }),
290 subscriptionStatus: () => invoke('sync_subscription_status'),
291 subscribe: (interval) => invoke('sync_subscribe', { interval }),
292 accountInfo: () => invoke('sync_account_info'),
293 },
294
295 // Preferences — small JSON file in the app config dir for settings read before the DB is up
296 preferences: {
297 get: () => invoke('get_preferences'),
298 setUpdateCheckOnLaunch: (enabled) => invoke('set_update_check_on_launch', { enabled }),
299 },
300
301 // Plugins — Rhai-based import plugin management
302 plugins: {
303 listImport: () => invoke('list_import_plugins'), // All available (enabled + disabled)
304 listEnabled: () => invoke('list_enabled_import_plugins'),
305 getForExtension: (extension) => invoke('get_plugins_for_extension', { extension }), // Match file → compatible plugins
306 preview: (pluginId, filePath, options = {}) => invoke('preview_import', { pluginId, filePath, options }), // Dry-run parse
307 execute: (pluginId, filePath, options = {}) => invoke('execute_import', { pluginId, filePath, options }), // Create entities in DB
308 enable: (pluginId) => invoke('enable_plugin', { pluginId }), // Symlink available/ → enabled/
309 disable: (pluginId) => invoke('disable_plugin', { pluginId }),
310 reload: (pluginId) => invoke('reload_plugin', { pluginId }), // Re-read + recompile from disk
311 },
312
313 // Attachments — file attachments on tasks and projects
314 attachments: {
315 list: (taskId, projectId) => invoke('list_attachments', { taskId: taskId || null, projectId: projectId || null }),
316 add: (taskId, projectId, filePath) => invoke('add_attachment', { taskId: taskId || null, projectId: projectId || null, filePath }),
317 delete: (id) => invoke('delete_attachment', { id }),
318 open: (id) => invoke('open_attachment', { id }),
319 save: (id, destination) => invoke('save_attachment', { id, destination }),
320 convertFromEmail: (emailId, taskId) => invoke('convert_email_attachments', { emailId, taskId }),
321 openEmailBlob: (blobHash, filename) => invoke('open_email_blob', { blobHash, filename }),
322 saveEmailBlob: (blobHash, destination) => invoke('save_email_blob', { blobHash, destination }),
323 fileSize: (filePath) => invoke('get_file_size', { filePath }),
324 },
325
326 // External Import — vCard contacts and iCalendar events
327 import: {
328 previewVcf: (filePath) => invoke('preview_vcf', { filePath }),
329 importVcf: (filePath) => invoke('import_vcf', { filePath }),
330 previewIcs: (filePath) => invoke('preview_ics', { filePath }),
331 importIcs: (filePath) => invoke('import_ics', { filePath }),
332 },
333
334 // Time Tracking — start/stop timer, sessions, summaries
335 timeTracking: {
336 startTimer: (taskId) => invoke('start_timer', { taskId }),
337 stopTimer: (taskId) => invoke('stop_timer', { taskId }),
338 discardTimer: (taskId) => invoke('discard_timer', { taskId }),
339 getActive: () => invoke('get_active_timer'),
340 listSessions: (taskId) => invoke('list_time_sessions', { taskId }),
341 logManual: (taskId, minutes, date) => invoke('log_manual_time', { input: { taskId, minutes, date } }),
342 getSummary: (start, end) => invoke('get_time_summary', { input: { start, end } }),
343 },
344
345 // Window — Tauri window management
346 window: {
347 setTitle: (title) => invoke('set_window_title', { title }),
348 openCompose: (context) => invoke('open_compose_window', { context: context || null }), // Separate compose window, optional reply context
349 openEmailInBrowser: (id) => invoke('open_email_in_browser', { id }), // Render HTML email to temp file → open
350 },
351 };
352
353 // ============ Populate GoingsOn.api Namespace ============
354
355 GoingsOn.api = api;
356
357 })();
358
359