/**
* 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 = '
';
} else {
listHtml = attachments.map(a => `
${getIcon(a.mimeType)}
${esc(a.filename)}
${esc(a.fileSizeFormatted)}
${a.hasLocalBlob ? `
` : `
Sync needed
`}
`).join('');
}
const content = `
`;
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,
};
})();