From 4e3c860866e00c69cf72a76541bd95b51f8b7b14 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Sat, 7 Mar 2026 13:34:13 +0100 Subject: [PATCH] feat(plugins): add brain dump plugin for quick task capture Add a bundled plugin that opens a dialog with a textarea where each line becomes a task. Includes project selector (defaults to inbox), due date picker (defaults to today), draft auto-save via persistDataSynced, and project theme color indicator. Also extends the plugin API with dueDay support on PluginCreateTaskData, raised button option on DialogButtonCfg, and default form element theming in the plugin dialog component. --- packages/plugin-api/src/types.ts | 3 + packages/plugin-dev/brain-dump/icon.svg | 3 + packages/plugin-dev/brain-dump/manifest.json | 21 ++ packages/plugin-dev/brain-dump/package.json | 8 + packages/plugin-dev/brain-dump/plugin.js | 250 ++++++++++++++++++ src/app/plugins/plugin-bridge.service.ts | 1 + src/app/plugins/plugin.service.ts | 2 + .../plugin-dialog/plugin-dialog.component.ts | 69 ++++- 8 files changed, 344 insertions(+), 13 deletions(-) create mode 100644 packages/plugin-dev/brain-dump/icon.svg create mode 100644 packages/plugin-dev/brain-dump/manifest.json create mode 100644 packages/plugin-dev/brain-dump/package.json create mode 100644 packages/plugin-dev/brain-dump/plugin.js diff --git a/packages/plugin-api/src/types.ts b/packages/plugin-api/src/types.ts index fd77e93686..312525f216 100644 --- a/packages/plugin-api/src/types.ts +++ b/packages/plugin-api/src/types.ts @@ -45,6 +45,7 @@ export interface DialogButtonCfg { icon?: string; onClick: () => void | Promise; color?: 'primary' | 'warn'; + raised?: boolean; } export interface DialogCfg { @@ -454,6 +455,8 @@ export interface PluginCreateTaskData { timeEstimate?: number; parentId?: string | null; isDone?: boolean; + /** Due date as ISO date string (YYYY-MM-DD) */ + dueDay?: string | null; } export interface PluginShortcutCfg { diff --git a/packages/plugin-dev/brain-dump/icon.svg b/packages/plugin-dev/brain-dump/icon.svg new file mode 100644 index 0000000000..4b542e05c2 --- /dev/null +++ b/packages/plugin-dev/brain-dump/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/plugin-dev/brain-dump/manifest.json b/packages/plugin-dev/brain-dump/manifest.json new file mode 100644 index 0000000000..bb30f365af --- /dev/null +++ b/packages/plugin-dev/brain-dump/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "Brain Dump", + "id": "brain-dump", + "manifestVersion": 1, + "version": "1.0.0", + "minSupVersion": "13.0.0", + "description": "Quickly capture many tasks at once. Each line becomes a task.", + "author": "Super Productivity", + "permissions": [ + "getAllProjects", + "addTask", + "showSnack", + "openDialog", + "persistDataSynced", + "loadSyncedData" + ], + "hooks": [], + "iFrame": false, + "isSkipMenuEntry": true, + "icon": "icon.svg" +} diff --git a/packages/plugin-dev/brain-dump/package.json b/packages/plugin-dev/brain-dump/package.json new file mode 100644 index 0000000000..b97e92bd76 --- /dev/null +++ b/packages/plugin-dev/brain-dump/package.json @@ -0,0 +1,8 @@ +{ + "name": "brain-dump", + "version": "1.0.0", + "description": "Brain dump plugin for Super Productivity", + "scripts": { + "build": "echo 'No build needed for simple plugin'" + } +} diff --git a/packages/plugin-dev/brain-dump/plugin.js b/packages/plugin-dev/brain-dump/plugin.js new file mode 100644 index 0000000000..4ecb929450 --- /dev/null +++ b/packages/plugin-dev/brain-dump/plugin.js @@ -0,0 +1,250 @@ +// Brain Dump Plugin - Quickly capture many tasks at once + +var _bdIntervals = { save: null, check: null }; + +function escapeHtml(str) { + var div = document.createElement('div'); + div.appendChild(document.createTextNode(str)); + return div.innerHTML; +} + +function countTasks(textarea) { + if (!textarea || !textarea.value) return 0; + return textarea.value.split('\n').filter(function (line) { + return line.trim(); + }).length; +} + +function todayStr() { + var d = new Date(); + return ( + d.getFullYear() + + '-' + + String(d.getMonth() + 1).padStart(2, '0') + + '-' + + String(d.getDate()).padStart(2, '0') + ); +} + +function _bdCleanup() { + clearInterval(_bdIntervals.save); + clearInterval(_bdIntervals.check); + _bdIntervals.save = null; + _bdIntervals.check = null; +} + +async function openBrainDump() { + var projectsPromise = PluginAPI.getAllProjects(); + var dataPromise = PluginAPI.loadSyncedData(); + var results = await Promise.all([projectsPromise, dataPromise]); + var projects = results[0]; + var savedData = results[1]; + + var draft = {}; + if (savedData) { + try { + draft = JSON.parse(savedData); + } catch (e) { + /* ignore corrupt draft */ + } + } + var savedText = draft.text || ''; + var inboxProject = projects.find(function (p) { + return p.id === 'INBOX_PROJECT'; + }); + var savedProjectId = draft.projectId || (inboxProject ? inboxProject.id : ''); + var savedDueDay = draft.dueDay !== undefined ? draft.dueDay : todayStr(); + + var activeProjects = projects.filter(function (p) { + return !p.isArchived; + }); + + var projectOptions = activeProjects + .map(function (p) { + return ( + '' + ); + }) + .join(''); + + var html = + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '
' + + '
' + + '
'; + + var lastSavedText = savedText; + + PluginAPI.openDialog({ + title: 'Brain Dump', + htmlContent: html, + buttons: [ + { + label: 'Cancel', + onClick: function () { + _bdCleanup(); + return saveDraft(); + }, + }, + { + label: 'Add Tasks', + color: 'primary', + icon: 'add', + raised: true, + onClick: function () { + _bdCleanup(); + return submitTasks(); + }, + }, + ], + }); + + // Periodic draft save (only on change) + theme color update + status update + _bdIntervals.save = setInterval(function () { + var textarea = document.getElementById('bd-input'); + if (textarea && textarea.value !== lastSavedText) { + lastSavedText = textarea.value; + saveDraft(); + } + updateThemeColor(activeProjects); + updateStatus(); + }, 2000); + + _bdIntervals.check = setInterval(function () { + if (!document.getElementById('bd-input')) { + _bdCleanup(); + } + }, 1000); + + // Initial updates + setTimeout(function () { + updateThemeColor(activeProjects); + updateStatus(); + }, 100); +} + +function updateStatus() { + var textarea = document.getElementById('bd-input'); + var statusEl = document.getElementById('bd-status'); + if (!statusEl) return; + var count = countTasks(textarea); + if (count === 0) { + statusEl.textContent = 'One task per line. Empty lines are skipped.'; + } else { + statusEl.textContent = count + ' task' + (count !== 1 ? 's' : '') + ' to add'; + } +} + +function updateThemeColor(activeProjects) { + var select = document.getElementById('bd-project'); + var colorBar = document.getElementById('bd-color-bar'); + if (!select || !colorBar) return; + + var project = activeProjects.find(function (p) { + return p.id === select.value; + }); + + if (project && project.theme && project.theme.primary) { + colorBar.style.background = project.theme.primary; + } else { + colorBar.style.background = 'transparent'; + } +} + +async function saveDraft() { + var textarea = document.getElementById('bd-input'); + var select = document.getElementById('bd-project'); + var dueInput = document.getElementById('bd-due'); + if (textarea) { + await PluginAPI.persistDataSynced( + JSON.stringify({ + text: textarea.value, + projectId: select ? select.value : '', + dueDay: dueInput ? dueInput.value : '', + }), + ); + } +} + +async function submitTasks() { + var textarea = document.getElementById('bd-input'); + var select = document.getElementById('bd-project'); + var dueInput = document.getElementById('bd-due'); + if (!textarea) return; + + var lines = textarea.value.split('\n').filter(function (line) { + return line.trim(); + }); + + if (lines.length === 0) { + PluginAPI.showSnack({ + msg: 'Nothing to add — enter at least one task.', + type: 'WARNING', + }); + return; + } + + var projectId = select ? select.value : null; + var dueDay = dueInput ? dueInput.value : null; + + for (var i = 0; i < lines.length; i++) { + var taskData = { title: lines[i].trim() }; + if (projectId) { + taskData.projectId = projectId; + } + if (dueDay) { + taskData.dueDay = dueDay; + } + await PluginAPI.addTask(taskData); + } + + // Clear textarea and draft + textarea.value = ''; + await PluginAPI.persistDataSynced( + JSON.stringify({ text: '', projectId: '', dueDay: '' }), + ); + + PluginAPI.showSnack({ + msg: lines.length + ' task' + (lines.length !== 1 ? 's' : '') + ' added', + type: 'SUCCESS', + ico: 'check', + }); +} + +// Register menu entry and shortcut +PluginAPI.registerMenuEntry({ + label: 'Brain Dump', + icon: 'lightbulb', + onClick: openBrainDump, +}); + +PluginAPI.registerShortcut({ + id: 'open-brain-dump', + label: 'Open Brain Dump', + onExec: openBrainDump, +}); diff --git a/src/app/plugins/plugin-bridge.service.ts b/src/app/plugins/plugin-bridge.service.ts index b5be995831..9583356178 100644 --- a/src/app/plugins/plugin-bridge.service.ts +++ b/src/app/plugins/plugin-bridge.service.ts @@ -505,6 +505,7 @@ export class PluginBridgeService implements OnDestroy { notes: taskData.notes || '', timeEstimate: taskData.timeEstimate || 0, isDone: (taskData as { isDone?: boolean }).isDone || false, + dueDay: taskData.dueDay ?? undefined, }; // Add the task using TaskService diff --git a/src/app/plugins/plugin.service.ts b/src/app/plugins/plugin.service.ts index f4203fd029..dedb1b993d 100644 --- a/src/app/plugins/plugin.service.ts +++ b/src/app/plugins/plugin.service.ts @@ -108,6 +108,7 @@ export class PluginService implements OnDestroy { 'assets/bundled-plugins/ai-productivity-prompts', 'assets/bundled-plugins/automations', 'assets/bundled-plugins/github-issue-provider', + 'assets/bundled-plugins/brain-dump', ]; // Only load manifests for discovery @@ -197,6 +198,7 @@ export class PluginService implements OnDestroy { 'assets/bundled-plugins/procrastination-buster', 'assets/bundled-plugins/automations', 'assets/bundled-plugins/github-issue-provider', + 'assets/bundled-plugins/brain-dump', ]; // KISS: No preloading - just load plugins directly diff --git a/src/app/plugins/ui/plugin-dialog/plugin-dialog.component.ts b/src/app/plugins/ui/plugin-dialog/plugin-dialog.component.ts index 4a1e98b8bc..9a9cae4f07 100644 --- a/src/app/plugins/ui/plugin-dialog/plugin-dialog.component.ts +++ b/src/app/plugins/ui/plugin-dialog/plugin-dialog.component.ts @@ -35,17 +35,30 @@ import { PluginLog } from '../../../core/log'; - @for (button of dialogData.buttons || defaultButtons; track button.label) { - + @for (button of dialogData.buttons || defaultButtons; track $index) { + @if (button.raised) { + + } @else { + + } } `, @@ -53,8 +66,9 @@ import { PluginLog } from '../../../core/log'; ` mat-dialog-content { min-width: 300px; - max-width: 600px; - max-height: 400px; + max-width: 900px; + width: 70vw; + max-height: 70vh; overflow-y: auto; } @@ -65,6 +79,35 @@ import { PluginLog } from '../../../core/log'; mat-icon { margin-right: 8px; } + + /* Default styles for native form elements injected by plugins */ + :host ::ng-deep select, + :host ::ng-deep textarea, + :host ::ng-deep input { + background: var(--bg-darker); + color: var(--text-color); + border: 1px solid var(--divider-color); + border-radius: 4px; + padding: 8px; + font-family: var(--font-primary-stack); + font-size: 14px; + } + + :host ::ng-deep select:focus, + :host ::ng-deep textarea:focus, + :host ::ng-deep input:focus { + outline: none; + border-color: var(--c-primary); + } + + :host ::ng-deep select option { + background: var(--bg-darker); + color: var(--text-color); + } + + :host ::ng-deep textarea { + resize: vertical; + } `, ], changeDetection: ChangeDetectionStrategy.OnPush,