/** * GoingsOn - External File Import Module * Handles importing contacts from vCard (.vcf) and events from iCalendar (.ics) files. */ (function() { 'use strict'; const esc = GoingsOn.utils.escapeHtml; // ============ Import Type Selection ============ /** * Opens the external import dialog with type selection. */ function openImportDialog() { const content = `
`; GoingsOn.ui.openModal('Import External Data', content); } // ============ Contact Import ============ /** * Opens file picker for vCard import, then shows preview. */ async function startContactImport() { const filePath = await pickFile('vCard Files', ['vcf']); if (!filePath) return; GoingsOn.ui.openModal('Import Contacts', `
Parsing ${esc(getFileName(filePath))}...
`); try { const preview = await GoingsOn.api.import.previewVcf(filePath); if (preview.length === 0) { updatePreviewContent('

No contacts found in file.

', null); return; } const maxPreview = 25; const display = preview.slice(0, maxPreview); const html = `

${preview.length} contact${preview.length !== 1 ? 's' : ''} found

${display.map(c => ` `).join('')}
NameCompanyEmailsPhones
${esc(c.displayName)} ${esc(c.company || '')} ${c.emailCount} ${c.phoneCount}
${preview.length > maxPreview ? `

...and ${preview.length - maxPreview} more

` : ''} `; updatePreviewContent(html, async () => { await executeContactImport(filePath, preview.length); }); } catch (err) { updatePreviewContent(`

Failed to parse file: ${esc(GoingsOn.utils.getErrorMessage(err))}

`, null); } } /** * Executes the vCard import. */ async function executeContactImport(filePath, expectedCount) { const btn = document.getElementById('import-external-confirm'); if (btn) { btn.disabled = true; btn.textContent = 'Importing...'; } try { const result = await GoingsOn.api.import.importVcf(filePath); GoingsOn.ui.closeModal(); const parts = []; if (result.imported > 0) parts.push(`${result.imported} imported`); if (result.skipped > 0) parts.push(`${result.skipped} skipped (duplicates)`); if (result.errors.length > 0) parts.push(`${result.errors.length} failed`); const hasErrors = result.errors.length > 0; GoingsOn.ui.showToast( `Contacts: ${parts.join(', ')}`, hasErrors ? 'warning' : 'success' ); if (GoingsOn.navigation && GoingsOn.navigation.reloadCurrentView) { GoingsOn.navigation.reloadCurrentView(); } } catch (err) { GoingsOn.ui.showToast('Import failed: ' + GoingsOn.utils.getErrorMessage(err), 'error'); if (btn) { btn.disabled = false; btn.textContent = 'Import'; } } } // ============ Calendar Import ============ /** * Opens file picker for ICS import, then shows preview. */ async function startCalendarImport() { const filePath = await pickFile('iCalendar Files', ['ics']); if (!filePath) return; GoingsOn.ui.openModal('Import Calendar', `
Parsing ${esc(getFileName(filePath))}...
`); try { const preview = await GoingsOn.api.import.previewIcs(filePath); if (preview.length === 0) { updatePreviewContent('

No events found in file.

', null); return; } const maxPreview = 25; const display = preview.slice(0, maxPreview); const html = `

${preview.length} event${preview.length !== 1 ? 's' : ''} found

${display.map(e => ` `).join('')}
TitleStartLocationRecurrence
${esc(e.title)} ${esc(formatImportDate(e.startTime))} ${esc(e.location || '')} ${esc(e.recurrence)}
${preview.length > maxPreview ? `

...and ${preview.length - maxPreview} more

` : ''} `; updatePreviewContent(html, async () => { await executeCalendarImport(filePath, preview.length); }); } catch (err) { updatePreviewContent(`

Failed to parse file: ${esc(GoingsOn.utils.getErrorMessage(err))}

`, null); } } /** * Executes the ICS import. */ async function executeCalendarImport(filePath, expectedCount) { const btn = document.getElementById('import-external-confirm'); if (btn) { btn.disabled = true; btn.textContent = 'Importing...'; } try { const result = await GoingsOn.api.import.importIcs(filePath); GoingsOn.ui.closeModal(); const parts = []; if (result.imported > 0) parts.push(`${result.imported} imported`); if (result.skipped > 0) parts.push(`${result.skipped} skipped (duplicates)`); if (result.errors.length > 0) parts.push(`${result.errors.length} failed`); const hasErrors = result.errors.length > 0; GoingsOn.ui.showToast( `Events: ${parts.join(', ')}`, hasErrors ? 'warning' : 'success' ); if (GoingsOn.navigation && GoingsOn.navigation.reloadCurrentView) { GoingsOn.navigation.reloadCurrentView(); } } catch (err) { GoingsOn.ui.showToast('Import failed: ' + GoingsOn.utils.getErrorMessage(err), 'error'); if (btn) { btn.disabled = false; btn.textContent = 'Import'; } } } // ============ Helpers ============ /** * Opens native file picker with given filter. * @param {string} filterName - Display name for the file filter * @param {string[]} extensions - Allowed file extensions * @returns {Promise} Selected file path, or null if cancelled */ async function pickFile(filterName, extensions) { try { const { open } = window.__TAURI__.dialog; return await open({ multiple: false, filters: [{ name: filterName, extensions }], }); } catch (err) { GoingsOn.ui.showToast('Failed to open file picker: ' + GoingsOn.utils.getErrorMessage(err), 'error'); return null; } } /** * Gets the filename from a full path. * @param {string} path - Full file path * @returns {string} Filename component */ function getFileName(path) { return path.split(/[/\\]/).pop() || path; } /** * Formats an ISO date string for display in preview table. * @param {string} isoString - ISO 8601 date string * @returns {string} Locale-formatted date string */ function formatImportDate(isoString) { if (!isoString) return ''; try { const d = new Date(isoString); return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } catch { return isoString; } } /** * Updates the modal content with preview HTML and optional confirm button. * @param {string} previewHtml - HTML to insert into the preview container * @param {Function|null} onConfirm - Callback for the Import button, or null for close-only */ function updatePreviewContent(previewHtml, onConfirm) { const container = document.querySelector('.import-preview-container'); if (!container) return; container.innerHTML = previewHtml; // Update or create form actions const modal = container.closest('.modal-body') || container.parentElement; let actions = modal.querySelector('.form-actions'); if (!actions) { actions = document.createElement('div'); actions.className = 'form-actions'; modal.appendChild(actions); } if (onConfirm) { actions.innerHTML = ` `; document.getElementById('import-external-confirm').addEventListener('click', onConfirm); } else { actions.innerHTML = ` `; } } // ============ Populate Namespace ============ GoingsOn.importExternal = { openDialog: openImportDialog, startContactImport, startCalendarImport, }; })();