/** * GoingsOn - Attachments Module * File attachment UI for tasks and projects. */ (function() { 'use strict'; const esc = GoingsOn.utils.escapeHtml; const escAttr = GoingsOn.utils.escapeAttr; // MIME type to icon mapping const MIME_ICONS = { 'application/pdf': '\uD83D\uDCC4', 'image/': '\uD83D\uDDBC\uFE0F', 'audio/': '\uD83C\uDFB5', 'video/': '\uD83C\uDFA5', 'text/': '\uD83D\uDCC3', 'application/zip': '\uD83D\uDCE6', 'application/gzip': '\uD83D\uDCE6', }; /** * Get the emoji icon for a MIME type. * @param {string} mimeType - MIME type string * @returns {string} Emoji character for the file type */ function getIcon(mimeType) { if (MIME_ICONS[mimeType]) return MIME_ICONS[mimeType]; for (const [prefix, icon] of Object.entries(MIME_ICONS)) { if (prefix.endsWith('/') && mimeType.startsWith(prefix)) return icon; } return '\uD83D\uDCCE'; } /** * Open the attachments panel modal for a task or project. * @param {string|null} taskId - Task ID, or null for project-only * @param {string|null} projectId - Project ID, or null for task-only */ async function openPanel(taskId, projectId) { GoingsOn.ui.closeModal(); try { const attachments = await GoingsOn.api.attachments.list(taskId, projectId); renderPanel(attachments, taskId, projectId); } catch (err) { GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to load attachments'), 'error'); } } function renderPanel(attachments, taskId, projectId) { const tid = taskId ? escAttr(taskId) : ''; const pid = projectId ? escAttr(projectId) : ''; let listHtml; if (attachments.length === 0) { listHtml = '

No attachments yet

'; } else { listHtml = attachments.map(a => `
${getIcon(a.mimeType)}
${esc(a.filename)}
${esc(a.fileSizeFormatted)}
${a.hasLocalBlob ? ` ` : ` Sync needed `}
`).join(''); } const content = `
${listHtml}
`; GoingsOn.ui.openModal('Attachments', content); } /** * Open the native file picker and attach the selected file. * @param {string|null} taskId - Task ID to attach to * @param {string|null} projectId - Project ID to attach to */ async function pickAndAttach(taskId, projectId) { try { const { open } = window.__TAURI__.dialog; const selected = await open({ multiple: false, title: 'Select file to attach', }); if (!selected) return; const filePath = typeof selected === 'string' ? selected : selected.path; if (!filePath) return; await GoingsOn.ui.apiCall( GoingsOn.api.attachments.add(taskId || null, projectId || null, filePath), { successMessage: 'File attached!', errorMessage: 'Failed to attach file', reload: () => openPanel(taskId || null, projectId || null), } ); } catch (err) { if (err && err.toString().includes('cancelled')) return; GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to attach file'), 'error'); } } /** * Open an attachment file using the system default application. * @param {string} id - Attachment ID */ async function openAttachment(id) { try { await GoingsOn.api.attachments.open(id); } catch (err) { GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to open file'), 'error'); } } /** * Save an attachment to a user-chosen location. * @param {string} id - Attachment ID * @param {string} filename - Default filename for the save dialog */ async function saveAs(id, filename) { try { const { save } = window.__TAURI__.dialog; const destination = await save({ defaultPath: filename, title: 'Save attachment as', }); if (!destination) return; await GoingsOn.api.attachments.save(id, destination); GoingsOn.ui.showToast('File saved!', 'success'); } catch (err) { if (err && err.toString().includes('cancelled')) return; GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to save file'), 'error'); } } /** * Delete an attachment after confirmation, then refresh the panel. * @param {string} id - Attachment ID to delete * @param {string} taskId - Parent task ID for panel refresh * @param {string} projectId - Parent project ID for panel refresh */ async function remove(id, taskId, projectId) { const ok = await GoingsOn.ui.showConfirmDialog( 'Delete attachment', 'Delete this attachment?', { confirmText: 'Delete', danger: true } ); if (!ok) return; await GoingsOn.ui.apiCall( GoingsOn.api.attachments.delete(id), { successMessage: 'Attachment deleted', errorMessage: 'Failed to delete attachment', reload: () => openPanel(taskId || null, projectId || null), } ); } // Render a compact attachment count badge for task rows /** * Render a compact attachment count badge for task rows. * @param {number} attachmentCount - Number of attachments * @returns {string} HTML string for the badge, or empty string if no attachments */ function renderBadge(attachmentCount) { if (!attachmentCount || attachmentCount === 0) return ''; return `Files: ${attachmentCount}`; } GoingsOn.attachments = { openPanel, pickAndAttach, open: openAttachment, saveAs, remove, renderBadge, getIcon, }; })();