diff --git a/.gitignore b/.gitignore index 12afdff..6953e01 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,6 @@ data.json # Development/Documentation (not for distribution) RELEASE-GUIDE.md +ROADMAP.md +deploy-test.bat .claude/ \ No newline at end of file diff --git a/main.js b/main.js index 412990e..aafc247 100644 --- a/main.js +++ b/main.js @@ -46,7 +46,10 @@ var DEFAULT_SETTINGS = { autoStartBreak: true, tickSoundEnabled: false, // Daily note logging - logToDaily: false + logToDaily: false, + // Task reminders + enableReminders: true, + defaultReminderMinutes: 15 }; var DEFAULT_DATA = { tasks: [], @@ -90,8 +93,12 @@ var QuickAddTaskModal = class extends import_obsidian.Modal { super(app); this.taskText = ""; this.selectedList = "work"; + this.scheduledDate = ""; + this.scheduledTime = ""; + this.reminderMinutes = 0; 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; } @@ -137,6 +144,26 @@ var QuickAddTaskModal = class extends import_obsidian.Modal { dropdown.setValue(this.selectedList); dropdown.onChange((value) => this.selectedList = value); }); + new import_obsidian.Setting(contentEl).setName("\u{1F4C5} 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"; + }); + new import_obsidian.Setting(contentEl).setName("\u23F0 Scheduled Time").setDesc("Optional: What time?").addText((text) => { + text.setPlaceholder("HH:mm").setValue(this.scheduledTime).onChange((value) => this.scheduledTime = value); + text.inputEl.type = "time"; + }); + if (this.plugin.settings.enableReminders) { + new import_obsidian.Setting(contentEl).setName("\u{1F514} 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)); + }); + } const buttonContainer = contentEl.createEl("div", { cls: "immerse-modal-buttons" }); const cancelBtn = buttonContainer.createEl("button", { text: "Cancel", cls: "immerse-btn" }); cancelBtn.addEventListener("click", () => this.close()); @@ -146,6 +173,15 @@ var QuickAddTaskModal = class extends import_obsidian.Modal { submitTask() { if (this.taskText.trim()) { const task = this.plugin.createTask(this.taskText, this.estimatedMinutes, this.selectedList); + 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 import_obsidian.Notice("\u2705 Task added!"); this.close(); @@ -201,6 +237,29 @@ var EditTaskModal = class extends import_obsidian.Modal { textarea.setValue(this.task.notes).onChange((value) => this.task.notes = value); textarea.inputEl.rows = 4; }); + new import_obsidian.Setting(contentEl).setName("\u{1F4C5} 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 || void 0); + text.inputEl.type = "date"; + }); + new import_obsidian.Setting(contentEl).setName("\u23F0 Scheduled Time").setDesc("Optional: What time?").addText((text) => { + text.setPlaceholder("HH:mm").setValue(this.task.scheduledTime || "").onChange((value) => this.task.scheduledTime = value || void 0); + text.inputEl.type = "time"; + }); + if (this.plugin.settings.enableReminders) { + new import_obsidian.Setting(contentEl).setName("\u{1F514} 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 : void 0; + }); + }); + } if (this.task.actualMinutes > 0) { new import_obsidian.Setting(contentEl).setName("Time Tracked").setDesc(`You've worked on this task for ${this.plugin.formatTimeHuman(this.task.actualMinutes)}`); } @@ -459,8 +518,9 @@ var ImmerseView = class extends import_obsidian2.ItemView { } renderTaskItem(container, task) { const list = this.plugin.settings.lists.find((l) => l.id === task.list); + 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" : ""}` + cls: `immerse-task-item ${task.completed ? "completed" : ""} ${task.isActive ? "active" : ""} ${isOverdue ? "overdue" : ""}` }); const checkbox = taskEl.createEl("div", { cls: "immerse-checkbox" }); checkbox.innerHTML = task.completed ? "\u2713" : ""; @@ -495,6 +555,21 @@ var ImmerseView = class extends import_obsidian2.ItemView { actualSpan.addClass("immerse-overtime-text"); } } + 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(`\u26A0\uFE0F OVERDUE: ${dateStr}${timeStr ? " " + timeStr : ""}`); + } else { + scheduleSpan.setText(`\u{1F4C5} ${dateStr}${timeStr ? " " + timeStr : ""}`); + } + if (task.reminderMinutes) { + scheduleSpan.title = `Reminder set for ${task.reminderMinutes} min before`; + } + } const actions = taskEl.createEl("div", { cls: "immerse-task-actions" }); if (!task.completed) { const startBtn = actions.createEl("button", { cls: "immerse-task-btn", attr: { "aria-label": "Start Pomodoro" } }); @@ -544,7 +619,11 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { this.secondsWorkedOnCurrentTask = 0; // Status bar element this.statusBarEl = null; + // Reminder system + this.reminderCheckInterval = null; + this.notifiedReminders = /* @__PURE__ */ new Set(); } + // Track which reminders have been shown async onload() { await this.loadAllData(); this.checkDailyReset(); @@ -587,9 +666,13 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { }); this.addSettingTab(new ImmerseSettingTab(this.app, this)); this.createStatusBar(); + if (this.settings.enableReminders) { + this.startReminderSystem(); + } } onunload() { this.stopTimer(); + this.stopReminderSystem(); } async loadAllData() { const loaded = await this.loadData(); @@ -935,6 +1018,59 @@ var ImmersePlugin = class extends import_obsidian3.Plugin { this.statusBarEl.removeClass("immerse-status-active"); } } + // ============ Reminder System ============ + startReminderSystem() { + this.reminderCheckInterval = window.setInterval(() => { + this.checkReminders(); + }, 3e4); + 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(); + this.data.tasks.filter((task) => !task.completed && task.scheduledDate && task.scheduledTime).forEach((task) => { + const reminderKey = `${task.id}-${task.scheduledDate}-${task.scheduledTime}`; + if (this.notifiedReminders.has(reminderKey)) + return; + const scheduledDateTime = new Date(`${task.scheduledDate}T${task.scheduledTime}`); + const scheduledTime = scheduledDateTime.getTime(); + if (currentTime > scheduledTime) { + this.showOverdueNotice(task); + this.notifiedReminders.add(reminderKey); + return; + } + if (task.reminderMinutes) { + const reminderTime = scheduledTime - task.reminderMinutes * 60 * 1e3; + if (currentTime >= reminderTime) { + this.showReminder(task); + this.notifiedReminders.add(reminderKey); + } + } + }); + } + showReminder(task) { + const timeStr = task.scheduledTime; + new import_obsidian3.Notice(`\u{1F514} Reminder: "${task.text}" is scheduled for ${timeStr}`, 8e3); + if (this.settings.enableSounds) { + this.playAlertSound(); + } + } + showOverdueNotice(task) { + const dateStr = task.scheduledDate; + const timeStr = task.scheduledTime; + new import_obsidian3.Notice(`\u26A0\uFE0F Overdue: "${task.text}" was scheduled for ${dateStr} ${timeStr}`, 1e4); + if (this.settings.enableSounds) { + this.playAlertSound(); + } + } // ============ Sounds & Celebrations ============ showCelebration(task) { let messages = CELEBRATION_MESSAGES; diff --git a/src/main.ts b/src/main.ts index 21eeee1..6be170f 100644 --- a/src/main.ts +++ b/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 = 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) { diff --git a/src/modals.ts b/src/modals.ts index 39bbbae..8a44c20 100644 --- a/src/modals.ts +++ b/src/modals.ts @@ -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) diff --git a/src/types.ts b/src/types.ts index d65d103..ce0e0ae 100644 --- a/src/types.ts +++ b/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 = { diff --git a/src/view.ts b/src/view.ts index 6f45df6..a191a12 100644 --- a/src/view.ts +++ b/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' }); diff --git a/styles.css b/styles.css index ac78dd3..88f24cc 100644 --- a/styles.css +++ b/styles.css @@ -464,6 +464,33 @@ color: var(--ft-warning); } +.immerse-schedule-badge { + padding: 2px 8px; + background: var(--ft-primary); + color: white; + border-radius: 4px; + font-size: 11px; + font-weight: 500; +} + +.immerse-schedule-badge.overdue { + background: var(--ft-danger); + animation: pulse-overdue 2s ease-in-out infinite; +} + +@keyframes pulse-overdue { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.7; + } +} + +.immerse-task-item.overdue { + border-left: 3px solid var(--ft-danger); +} + /* Task Actions */ .immerse-task-actions { display: flex;