Skip to content

Commit 88abdf9

Browse files
rendyhdclaude
andcommitted
Add notification system, Quick View redesign, and theme selector
Notifications: scheduled desktop notifications at configurable daily/secondary times that fetch tasks from the API or standalone cache and show Windows toasts for overdue, due-today, and upcoming tasks. Settings UI adds a Notifications tab with master toggle, time pickers, category filters, persistent/sound options, and a test button. Quick View: replaced multi-project checkbox list with single-list dropdown, added position-based sorting via project view API, added "include today from all projects" toggle, and view ID caching. Settings: added theme selector (system/light/dark) with live preview in the header bar. API: added fetchProjectViews() and fetchViewTasks() for position-based task sorting. Bumps version to v2.1.0-beta.1. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 76b936a commit 88abdf9

10 files changed

Lines changed: 831 additions & 91 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vikunja-quick-entry",
3-
"version": "2.0.1",
3+
"version": "2.1.0-beta.1",
44
"description": "A lightweight system tray app with a global hotkey for quick task entry to Vikunja",
55
"main": "src/main.js",
66
"scripts": {

src/api.js

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -669,4 +669,183 @@ function updateTask(taskId, taskData) {
669669
});
670670
}
671671

672-
module.exports = { createTask, fetchProjects, fetchTasks, markTaskDone, markTaskUndone, updateTaskDueDate, updateTask };
672+
function fetchProjectViews(projectId) {
673+
const config = getConfig();
674+
if (!config) {
675+
return Promise.resolve({ success: false, error: 'Configuration not loaded' });
676+
}
677+
678+
const url = `${config.vikunja_url}/api/v1/projects/${projectId}/views`;
679+
680+
const validation = validateHttpUrl(url);
681+
if (!validation.valid) {
682+
return Promise.resolve({ success: false, error: validation.error });
683+
}
684+
685+
return new Promise((resolve) => {
686+
const timeout = setTimeout(() => {
687+
resolve({ success: false, error: 'Request timed out (5s)' });
688+
}, 5000);
689+
690+
try {
691+
const request = net.request({
692+
method: 'GET',
693+
url,
694+
});
695+
696+
request.setHeader('Authorization', `Bearer ${config.api_token}`);
697+
request.setHeader('Content-Type', 'application/json');
698+
699+
let responseBody = '';
700+
let statusCode = 0;
701+
702+
request.on('response', (response) => {
703+
statusCode = response.statusCode;
704+
705+
response.on('data', (chunk) => {
706+
responseBody += chunk.toString();
707+
});
708+
709+
response.on('end', () => {
710+
clearTimeout(timeout);
711+
712+
if (statusCode >= 200 && statusCode < 300) {
713+
try {
714+
const views = JSON.parse(responseBody);
715+
resolve({ success: true, data: views });
716+
} catch {
717+
resolve({ success: false, error: 'Invalid response' });
718+
}
719+
} else {
720+
resolve({ success: false, error: describeHttpError(statusCode, responseBody) });
721+
}
722+
});
723+
});
724+
725+
request.on('error', (err) => {
726+
clearTimeout(timeout);
727+
resolve({ success: false, error: err.message || 'Network error' });
728+
});
729+
730+
request.end();
731+
} catch (err) {
732+
clearTimeout(timeout);
733+
resolve({ success: false, error: err.message || 'Request failed' });
734+
}
735+
});
736+
}
737+
738+
function fetchViewTasks(projectId, viewId, filterParams) {
739+
const config = getConfig();
740+
if (!config) {
741+
return Promise.resolve({ success: false, error: 'Configuration not loaded' });
742+
}
743+
744+
const baseUrl = `${config.vikunja_url}/api/v1/projects/${projectId}/views/${viewId}/tasks`;
745+
746+
const validation = validateHttpUrl(baseUrl);
747+
if (!validation.valid) {
748+
return Promise.resolve({ success: false, error: validation.error });
749+
}
750+
751+
const params = new URLSearchParams();
752+
params.set('per_page', String(filterParams.per_page || 10));
753+
params.set('page', String(filterParams.page || 1));
754+
755+
// Build filter string — always filter for open tasks
756+
let filterString = 'done = false';
757+
758+
// Due date filter
759+
const now = new Date();
760+
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
761+
const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);
762+
763+
if (filterParams.due_date_filter && filterParams.due_date_filter !== 'all') {
764+
switch (filterParams.due_date_filter) {
765+
case 'overdue':
766+
filterString += ` && due_date < '${todayStart.toISOString()}' && due_date != '0001-01-01T00:00:00Z'`;
767+
break;
768+
case 'today':
769+
filterString += ` && due_date <= '${todayEnd.toISOString()}' && due_date != '0001-01-01T00:00:00Z'`;
770+
break;
771+
case 'this_week': {
772+
const weekEnd = new Date(todayStart);
773+
weekEnd.setDate(weekEnd.getDate() + (7 - weekEnd.getDay()));
774+
weekEnd.setHours(23, 59, 59);
775+
filterString += ` && due_date <= '${weekEnd.toISOString()}' && due_date != '0001-01-01T00:00:00Z'`;
776+
break;
777+
}
778+
case 'this_month': {
779+
const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
780+
filterString += ` && due_date <= '${monthEnd.toISOString()}' && due_date != '0001-01-01T00:00:00Z'`;
781+
break;
782+
}
783+
case 'has_due_date':
784+
filterString += ` && due_date != '0001-01-01T00:00:00Z'`;
785+
break;
786+
case 'no_due_date':
787+
filterString += ` && due_date = '0001-01-01T00:00:00Z'`;
788+
break;
789+
}
790+
}
791+
792+
params.set('filter', filterString);
793+
params.set('sort_by', 'position');
794+
params.set('order_by', filterParams.order_by || 'asc');
795+
796+
const fullUrl = `${baseUrl}?${params.toString()}`;
797+
798+
return new Promise((resolve) => {
799+
const timeout = setTimeout(() => {
800+
resolve({ success: false, error: 'Request timed out (5s)' });
801+
}, 5000);
802+
803+
try {
804+
const request = net.request({
805+
method: 'GET',
806+
url: fullUrl,
807+
});
808+
809+
request.setHeader('Authorization', `Bearer ${config.api_token}`);
810+
request.setHeader('Content-Type', 'application/json');
811+
812+
let responseBody = '';
813+
let statusCode = 0;
814+
815+
request.on('response', (response) => {
816+
statusCode = response.statusCode;
817+
818+
response.on('data', (chunk) => {
819+
responseBody += chunk.toString();
820+
});
821+
822+
response.on('end', () => {
823+
clearTimeout(timeout);
824+
825+
if (statusCode >= 200 && statusCode < 300) {
826+
try {
827+
const tasks = JSON.parse(responseBody);
828+
resolve({ success: true, data: tasks });
829+
} catch {
830+
resolve({ success: false, error: 'Invalid response' });
831+
}
832+
} else {
833+
resolve({ success: false, error: describeHttpError(statusCode, responseBody) });
834+
}
835+
});
836+
});
837+
838+
request.on('error', (err) => {
839+
clearTimeout(timeout);
840+
resolve({ success: false, error: err.message || 'Network error' });
841+
});
842+
843+
request.end();
844+
} catch (err) {
845+
clearTimeout(timeout);
846+
resolve({ success: false, error: err.message || 'Request failed' });
847+
}
848+
});
849+
}
850+
851+
module.exports = { createTask, fetchProjects, fetchTasks, markTaskDone, markTaskUndone, updateTaskDueDate, updateTask, fetchProjectViews, fetchViewTasks };

src/config.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,18 @@ function getConfig() {
8383
include_today_all_projects: (config.viewer_filter && config.viewer_filter.include_today_all_projects) === true,
8484
},
8585
secondary_projects: Array.isArray(config.secondary_projects) ? config.secondary_projects : [],
86+
theme: config.theme || 'system',
87+
// Notification settings
88+
notifications_enabled: config.notifications_enabled === true,
89+
notifications_persistent: config.notifications_persistent === true,
90+
notifications_daily_reminder_enabled: config.notifications_daily_reminder_enabled !== false,
91+
notifications_daily_reminder_time: config.notifications_daily_reminder_time || '08:00',
92+
notifications_secondary_reminder_enabled: config.notifications_secondary_reminder_enabled === true,
93+
notifications_secondary_reminder_time: config.notifications_secondary_reminder_time || '16:00',
94+
notifications_overdue_enabled: config.notifications_overdue_enabled !== false,
95+
notifications_due_today_enabled: config.notifications_due_today_enabled !== false,
96+
notifications_upcoming_enabled: config.notifications_upcoming_enabled === true,
97+
notifications_sound: config.notifications_sound !== false,
8698
};
8799
}
88100

0 commit comments

Comments
 (0)