feat: Add task scheduling and reminders system
Add comprehensive scheduling functionality with reminder notifications: - Add scheduledDate, scheduledTime, and reminderMinutes fields to ImmerseTask - Add enableReminders and defaultReminderMinutes to plugin settings - Implement reminder notification system with 30-second background checks - Add overdue task detection with visual indicators - Add native HTML5 date/time pickers in Quick Add and Edit modals - Add reminder dropdown with 5/10/15/30/60 minute options - Display scheduled date/time with blue badge (📅) in task list - Show pulsing red "OVERDUE" badge (⚠️) for past-due tasks - Add red left border highlight for overdue tasks - Implement startup check for due/overdue tasks when plugin loads - Show overdue notices when Obsidian opens if tasks are past due - Play alert sounds for reminders (if sounds enabled) - Track shown reminders to prevent duplicate notifications
This commit is contained in:
90
src/main.ts
90
src/main.ts
@@ -44,10 +44,14 @@ export default class ImmersePlugin extends Plugin {
|
||||
// Focus time tracking (in seconds for accuracy)
|
||||
private focusSecondsToday: number = 0;
|
||||
private secondsWorkedOnCurrentTask: number = 0;
|
||||
|
||||
|
||||
// Status bar element
|
||||
statusBarEl: HTMLElement | null = null;
|
||||
|
||||
// Reminder system
|
||||
private reminderCheckInterval: number | null = null;
|
||||
private notifiedReminders: Set<string> = new Set(); // Track which reminders have been shown
|
||||
|
||||
async onload() {
|
||||
await this.loadAllData();
|
||||
|
||||
@@ -108,10 +112,16 @@ export default class ImmersePlugin extends Plugin {
|
||||
|
||||
// Create status bar timer
|
||||
this.createStatusBar();
|
||||
|
||||
// Start reminder checking system
|
||||
if (this.settings.enableReminders) {
|
||||
this.startReminderSystem();
|
||||
}
|
||||
}
|
||||
|
||||
onunload() {
|
||||
this.stopTimer();
|
||||
this.stopReminderSystem();
|
||||
}
|
||||
|
||||
async loadAllData() {
|
||||
@@ -604,6 +614,84 @@ export default class ImmersePlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Reminder System ============
|
||||
|
||||
startReminderSystem() {
|
||||
// Check for reminders every 30 seconds
|
||||
this.reminderCheckInterval = window.setInterval(() => {
|
||||
this.checkReminders();
|
||||
}, 30000);
|
||||
// Also check immediately
|
||||
this.checkReminders();
|
||||
}
|
||||
|
||||
stopReminderSystem() {
|
||||
if (this.reminderCheckInterval) {
|
||||
window.clearInterval(this.reminderCheckInterval);
|
||||
this.reminderCheckInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
checkReminders() {
|
||||
if (!this.settings.enableReminders) return;
|
||||
|
||||
const now = new Date();
|
||||
const currentTime = now.getTime();
|
||||
|
||||
// Check each incomplete task with scheduling
|
||||
this.data.tasks
|
||||
.filter(task => !task.completed && task.scheduledDate && task.scheduledTime)
|
||||
.forEach(task => {
|
||||
const reminderKey = `${task.id}-${task.scheduledDate}-${task.scheduledTime}`;
|
||||
|
||||
// Skip if we already notified for this reminder
|
||||
if (this.notifiedReminders.has(reminderKey)) return;
|
||||
|
||||
// Parse the scheduled date and time
|
||||
const scheduledDateTime = new Date(`${task.scheduledDate}T${task.scheduledTime}`);
|
||||
const scheduledTime = scheduledDateTime.getTime();
|
||||
|
||||
// Check for overdue tasks (scheduled time has passed)
|
||||
if (currentTime > scheduledTime) {
|
||||
this.showOverdueNotice(task);
|
||||
this.notifiedReminders.add(reminderKey);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for tasks with reminders set
|
||||
if (task.reminderMinutes) {
|
||||
const reminderTime = scheduledTime - (task.reminderMinutes * 60 * 1000);
|
||||
|
||||
// Show reminder if it's past the reminder time but before the scheduled time
|
||||
if (currentTime >= reminderTime) {
|
||||
this.showReminder(task);
|
||||
this.notifiedReminders.add(reminderKey);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showReminder(task: ImmerseTask) {
|
||||
const timeStr = task.scheduledTime;
|
||||
new Notice(`🔔 Reminder: "${task.text}" is scheduled for ${timeStr}`, 8000);
|
||||
|
||||
// Play alert sound if enabled
|
||||
if (this.settings.enableSounds) {
|
||||
this.playAlertSound();
|
||||
}
|
||||
}
|
||||
|
||||
showOverdueNotice(task: ImmerseTask) {
|
||||
const dateStr = task.scheduledDate;
|
||||
const timeStr = task.scheduledTime;
|
||||
new Notice(`⚠️ Overdue: "${task.text}" was scheduled for ${dateStr} ${timeStr}`, 10000);
|
||||
|
||||
// Play alert sound if enabled
|
||||
if (this.settings.enableSounds) {
|
||||
this.playAlertSound();
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Sounds & Celebrations ============
|
||||
|
||||
showCelebration(task: ImmerseTask) {
|
||||
|
||||
@@ -15,11 +15,15 @@ export class QuickAddTaskModal extends Modal {
|
||||
taskText: string = '';
|
||||
estimatedMinutes: number;
|
||||
selectedList: string = 'work';
|
||||
scheduledDate: string = '';
|
||||
scheduledTime: string = '';
|
||||
reminderMinutes: number = 0;
|
||||
|
||||
constructor(app: App, plugin: ImmersePlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.estimatedMinutes = plugin.settings.defaultEstimateMinutes;
|
||||
this.reminderMinutes = plugin.settings.defaultReminderMinutes;
|
||||
if (plugin.settings.lists.length > 0) {
|
||||
this.selectedList = plugin.settings.lists[0].id;
|
||||
}
|
||||
@@ -82,6 +86,45 @@ export class QuickAddTaskModal extends Modal {
|
||||
dropdown.onChange(value => this.selectedList = value);
|
||||
});
|
||||
|
||||
// Scheduled date
|
||||
new Setting(contentEl)
|
||||
.setName('📅 Scheduled Date')
|
||||
.setDesc('Optional: When do you plan to work on this?')
|
||||
.addText(text => {
|
||||
text.setPlaceholder('YYYY-MM-DD')
|
||||
.setValue(this.scheduledDate)
|
||||
.onChange(value => this.scheduledDate = value);
|
||||
text.inputEl.type = 'date';
|
||||
});
|
||||
|
||||
// Scheduled time
|
||||
new Setting(contentEl)
|
||||
.setName('⏰ Scheduled Time')
|
||||
.setDesc('Optional: What time?')
|
||||
.addText(text => {
|
||||
text.setPlaceholder('HH:mm')
|
||||
.setValue(this.scheduledTime)
|
||||
.onChange(value => this.scheduledTime = value);
|
||||
text.inputEl.type = 'time';
|
||||
});
|
||||
|
||||
// Reminder
|
||||
if (this.plugin.settings.enableReminders) {
|
||||
new Setting(contentEl)
|
||||
.setName('🔔 Reminder')
|
||||
.setDesc('Remind me before the scheduled time')
|
||||
.addDropdown(dropdown => {
|
||||
dropdown.addOption('0', 'No reminder');
|
||||
dropdown.addOption('5', '5 minutes before');
|
||||
dropdown.addOption('10', '10 minutes before');
|
||||
dropdown.addOption('15', '15 minutes before');
|
||||
dropdown.addOption('30', '30 minutes before');
|
||||
dropdown.addOption('60', '1 hour before');
|
||||
dropdown.setValue(this.reminderMinutes.toString());
|
||||
dropdown.onChange(value => this.reminderMinutes = parseInt(value));
|
||||
});
|
||||
}
|
||||
|
||||
// Buttons
|
||||
const buttonContainer = contentEl.createEl('div', { cls: 'immerse-modal-buttons' });
|
||||
|
||||
@@ -95,6 +138,16 @@ export class QuickAddTaskModal extends Modal {
|
||||
submitTask() {
|
||||
if (this.taskText.trim()) {
|
||||
const task = this.plugin.createTask(this.taskText, this.estimatedMinutes, this.selectedList);
|
||||
// Add scheduling data if provided
|
||||
if (this.scheduledDate) {
|
||||
task.scheduledDate = this.scheduledDate;
|
||||
}
|
||||
if (this.scheduledTime) {
|
||||
task.scheduledTime = this.scheduledTime;
|
||||
}
|
||||
if (this.reminderMinutes > 0 && this.scheduledDate && this.scheduledTime) {
|
||||
task.reminderMinutes = this.reminderMinutes;
|
||||
}
|
||||
this.plugin.addTask(task);
|
||||
new Notice('✅ Task added!');
|
||||
this.close();
|
||||
@@ -177,6 +230,48 @@ export class EditTaskModal extends Modal {
|
||||
textarea.inputEl.rows = 4;
|
||||
});
|
||||
|
||||
// Scheduled date
|
||||
new Setting(contentEl)
|
||||
.setName('📅 Scheduled Date')
|
||||
.setDesc('Optional: When do you plan to work on this?')
|
||||
.addText(text => {
|
||||
text.setPlaceholder('YYYY-MM-DD')
|
||||
.setValue(this.task.scheduledDate || '')
|
||||
.onChange(value => this.task.scheduledDate = value || undefined);
|
||||
text.inputEl.type = 'date';
|
||||
});
|
||||
|
||||
// Scheduled time
|
||||
new Setting(contentEl)
|
||||
.setName('⏰ Scheduled Time')
|
||||
.setDesc('Optional: What time?')
|
||||
.addText(text => {
|
||||
text.setPlaceholder('HH:mm')
|
||||
.setValue(this.task.scheduledTime || '')
|
||||
.onChange(value => this.task.scheduledTime = value || undefined);
|
||||
text.inputEl.type = 'time';
|
||||
});
|
||||
|
||||
// Reminder
|
||||
if (this.plugin.settings.enableReminders) {
|
||||
new Setting(contentEl)
|
||||
.setName('🔔 Reminder')
|
||||
.setDesc('Remind me before the scheduled time')
|
||||
.addDropdown(dropdown => {
|
||||
dropdown.addOption('0', 'No reminder');
|
||||
dropdown.addOption('5', '5 minutes before');
|
||||
dropdown.addOption('10', '10 minutes before');
|
||||
dropdown.addOption('15', '15 minutes before');
|
||||
dropdown.addOption('30', '30 minutes before');
|
||||
dropdown.addOption('60', '1 hour before');
|
||||
dropdown.setValue((this.task.reminderMinutes || 0).toString());
|
||||
dropdown.onChange(value => {
|
||||
const minutes = parseInt(value);
|
||||
this.task.reminderMinutes = minutes > 0 ? minutes : undefined;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Show actual time if task has been worked on
|
||||
if (this.task.actualMinutes > 0) {
|
||||
new Setting(contentEl)
|
||||
|
||||
10
src/types.ts
10
src/types.ts
@@ -10,7 +10,9 @@ export interface ImmerseTask {
|
||||
completedAt?: number;
|
||||
list: string;
|
||||
notes: string;
|
||||
scheduledDate?: string;
|
||||
scheduledDate?: string; // Date in YYYY-MM-DD format
|
||||
scheduledTime?: string; // Time in HH:mm format (24-hour)
|
||||
reminderMinutes?: number; // Minutes before scheduled time to remind (0 = no reminder)
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
@@ -34,6 +36,9 @@ export interface ImmerseSettings {
|
||||
tickSoundEnabled: boolean;
|
||||
// Daily note logging
|
||||
logToDaily: boolean;
|
||||
// Task reminders
|
||||
enableReminders: boolean;
|
||||
defaultReminderMinutes: number; // Default minutes before task to remind
|
||||
}
|
||||
|
||||
export interface ImmerseData {
|
||||
@@ -62,6 +67,9 @@ export const DEFAULT_SETTINGS: ImmerseSettings = {
|
||||
tickSoundEnabled: false,
|
||||
// Daily note logging
|
||||
logToDaily: false,
|
||||
// Task reminders
|
||||
enableReminders: true,
|
||||
defaultReminderMinutes: 15,
|
||||
};
|
||||
|
||||
export const DEFAULT_DATA: ImmerseData = {
|
||||
|
||||
29
src/view.ts
29
src/view.ts
@@ -326,9 +326,13 @@ export class ImmerseView extends ItemView {
|
||||
|
||||
renderTaskItem(container: Element, task: ImmerseTask) {
|
||||
const list = this.plugin.settings.lists.find(l => l.id === task.list);
|
||||
|
||||
const taskEl = container.createEl('div', {
|
||||
cls: `immerse-task-item ${task.completed ? 'completed' : ''} ${task.isActive ? 'active' : ''}`
|
||||
|
||||
// Check if task is overdue
|
||||
const isOverdue = !task.completed && task.scheduledDate && task.scheduledTime &&
|
||||
new Date(`${task.scheduledDate}T${task.scheduledTime}`).getTime() < Date.now();
|
||||
|
||||
const taskEl = container.createEl('div', {
|
||||
cls: `immerse-task-item ${task.completed ? 'completed' : ''} ${task.isActive ? 'active' : ''} ${isOverdue ? 'overdue' : ''}`
|
||||
});
|
||||
|
||||
// Checkbox
|
||||
@@ -363,7 +367,7 @@ export class ImmerseView extends ItemView {
|
||||
|
||||
const taskMeta = content.createEl('div', { cls: 'immerse-task-meta' });
|
||||
taskMeta.createEl('span', { text: `Est: ${this.plugin.formatTimeHuman(task.estimatedMinutes)}` });
|
||||
|
||||
|
||||
if (task.actualMinutes > 0) {
|
||||
const actualSpan = taskMeta.createEl('span');
|
||||
actualSpan.setText(`Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}`);
|
||||
@@ -372,6 +376,23 @@ export class ImmerseView extends ItemView {
|
||||
}
|
||||
}
|
||||
|
||||
// Show scheduled date/time if set
|
||||
if (task.scheduledDate) {
|
||||
const scheduleSpan = taskMeta.createEl('span', {
|
||||
cls: `immerse-schedule-badge ${isOverdue ? 'overdue' : ''}`
|
||||
});
|
||||
const dateStr = task.scheduledDate;
|
||||
const timeStr = task.scheduledTime || '';
|
||||
if (isOverdue) {
|
||||
scheduleSpan.setText(`⚠️ OVERDUE: ${dateStr}${timeStr ? ' ' + timeStr : ''}`);
|
||||
} else {
|
||||
scheduleSpan.setText(`📅 ${dateStr}${timeStr ? ' ' + timeStr : ''}`);
|
||||
}
|
||||
if (task.reminderMinutes) {
|
||||
scheduleSpan.title = `Reminder set for ${task.reminderMinutes} min before`;
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
const actions = taskEl.createEl('div', { cls: 'immerse-task-actions' });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user