mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
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:
parent
93b37ed4c4
commit
4e3c860866
8 changed files with 344 additions and 13 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
3
packages/plugin-dev/brain-dump/icon.svg
Normal file
3
packages/plugin-dev/brain-dump/icon.svg
Normal 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 |
21
packages/plugin-dev/brain-dump/manifest.json
Normal file
21
packages/plugin-dev/brain-dump/manifest.json
Normal 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"
|
||||
}
|
||||
8
packages/plugin-dev/brain-dump/package.json
Normal file
8
packages/plugin-dev/brain-dump/package.json
Normal 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'"
|
||||
}
|
||||
}
|
||||
250
packages/plugin-dev/brain-dump/plugin.js
Normal file
250
packages/plugin-dev/brain-dump/plugin.js
Normal 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,
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue