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.
This commit is contained in:
Johannes Millan 2026-03-07 13:34:13 +01:00
parent 93b37ed4c4
commit 4e3c860866
8 changed files with 344 additions and 13 deletions

View file

@ -45,6 +45,7 @@ export interface DialogButtonCfg {
icon?: string;
onClick: () => void | Promise<void>;
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 {

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M9 21c0 .5.4 1 1 1h4c.6 0 1-.5 1-1v-1H9v1zm3-19C8.1 2 5 5.1 5 9c0 2.4 1.2 4.5 3 5.7V17c0 .5.4 1 1 1h6c.6 0 1-.5 1-1v-2.3c1.8-1.3 3-3.4 3-5.7 0-3.9-3.1-7-7-7z"/>
</svg>

After

Width:  |  Height:  |  Size: 260 B

View file

@ -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"
}

View file

@ -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'"
}
}

View file

@ -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 (
'<option value="' +
p.id +
'"' +
(p.id === savedProjectId ? ' selected' : '') +
'>' +
escapeHtml(p.title) +
'</option>'
);
})
.join('');
var html =
'<div id="bd-container" style="padding:4px 0">' +
'<div style="display:flex;gap:12px;margin-bottom:12px">' +
'<div style="flex:1">' +
'<label for="bd-project" style="display:block;margin-bottom:4px;font-size:12px;opacity:0.7">Project</label>' +
'<select id="bd-project" style="width:100%">' +
'<option value="">No project</option>' +
projectOptions +
'</select>' +
'<div id="bd-color-bar" style="height:3px;margin-top:4px;border-radius:2px"></div>' +
'</div>' +
'<div>' +
'<label for="bd-due" style="display:block;margin-bottom:4px;font-size:12px;opacity:0.7">Due date</label>' +
'<input type="date" id="bd-due" value="' +
escapeHtml(savedDueDay) +
'" style="width:100%">' +
'</div>' +
'</div>' +
'<textarea id="bd-input" rows="10" placeholder="One task per line..." style="width:100%;box-sizing:border-box">' +
escapeHtml(savedText) +
'</textarea>' +
'<div id="bd-status" style="margin-top:4px;font-size:12px;opacity:0.5">' +
'</div>' +
'</div>';
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,
});

View file

@ -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

View file

@ -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

View file

@ -35,17 +35,30 @@ import { PluginLog } from '../../../core/log';
</mat-dialog-content>
<mat-dialog-actions align="end">
@for (button of dialogData.buttons || defaultButtons; track button.label) {
<button
mat-button
[color]="button.color || 'primary'"
(click)="onButtonClick(button)"
>
@if (button.icon) {
<mat-icon>{{ button.icon }}</mat-icon>
}
{{ button.label }}
</button>
@for (button of dialogData.buttons || defaultButtons; track $index) {
@if (button.raised) {
<button
mat-raised-button
[color]="button.color || 'primary'"
(click)="onButtonClick(button)"
>
@if (button.icon) {
<mat-icon>{{ button.icon }}</mat-icon>
}
{{ button.label }}
</button>
} @else {
<button
mat-button
[color]="button.color || 'primary'"
(click)="onButtonClick(button)"
>
@if (button.icon) {
<mat-icon>{{ button.icon }}</mat-icon>
}
{{ button.label }}
</button>
}
}
</mat-dialog-actions>
`,
@ -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,