Skip to content

Commit 4899865

Browse files
authored
Merge pull request #690 from callumalpass:fix/multiday-tasks-in-calendar
Advanced calendar + Pomodoro fixes for multi-day events and auto-start (#641, $647)
2 parents 0c36460 + aca1057 commit 4899865

File tree

4 files changed

+170
-19
lines changed

4 files changed

+170
-19
lines changed

src/modals/TimeblockInfoModal.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
getAllDailyNotes,
77
appHasDailyNotesPluginLoaded
88
} from 'obsidian-daily-notes-interface';
9+
import { formatDateForStorage } from '../utils/dateUtils';
910

1011
export interface TimeBlock {
1112
title: string;
@@ -23,6 +24,7 @@ export interface TimeBlock {
2324
export class TimeblockInfoModal extends Modal {
2425
private timeblock: TimeBlock;
2526
private eventDate: Date;
27+
private timeblockDate: string;
2628
private plugin: TaskNotesPlugin;
2729
private originalTimeblock: TimeBlock;
2830

@@ -35,12 +37,13 @@ export class TimeblockInfoModal extends Modal {
3537
private selectedAttachments: TAbstractFile[] = [];
3638
private attachmentsList: HTMLElement;
3739

38-
constructor(app: App, plugin: TaskNotesPlugin, timeblock: TimeBlock, eventDate: Date) {
40+
constructor(app: App, plugin: TaskNotesPlugin, timeblock: TimeBlock, eventDate: Date, timeblockDate?: string) {
3941
super(app);
4042
this.plugin = plugin;
4143
this.timeblock = { ...timeblock }; // Create a copy for editing
4244
this.originalTimeblock = timeblock; // Keep original for comparison
4345
this.eventDate = eventDate;
46+
this.timeblockDate = timeblockDate || formatDateForStorage(eventDate);
4447
}
4548

4649
async onOpen() {
@@ -280,8 +283,8 @@ export class TimeblockInfoModal extends Modal {
280283
}
281284

282285
// Get daily note for the date
283-
const dateStr = this.eventDate.toISOString().split('T')[0]; // YYYY-MM-DD
284-
const moment = (window as any).moment(dateStr);
286+
const dateStr = this.timeblockDate;
287+
const moment = (window as any).moment(dateStr, 'YYYY-MM-DD');
285288
const allDailyNotes = getAllDailyNotes();
286289
const dailyNote = getDailyNote(moment, allDailyNotes);
287290

@@ -406,8 +409,8 @@ export class TimeblockInfoModal extends Modal {
406409
}
407410

408411
// Get daily note for the date
409-
const dateStr = this.eventDate.toISOString().split('T')[0]; // YYYY-MM-DD
410-
const moment = (window as any).moment(dateStr);
412+
const dateStr = this.timeblockDate;
413+
const moment = (window as any).moment(dateStr, 'YYYY-MM-DD');
411414
const allDailyNotes = getAllDailyNotes();
412415
const dailyNote = getDailyNote(moment, allDailyNotes);
413416

@@ -474,4 +477,4 @@ export class TimeblockInfoModal extends Modal {
474477
const { contentEl } = this;
475478
contentEl.empty();
476479
}
477-
}
480+
}

src/services/PomodoroService.ts

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export class PomodoroService {
2828
private activeAudioContexts: Set<AudioContext> = new Set();
2929
private cleanupTimeouts: Set<number> = new Set();
3030
private webhookNotifier?: IWebhookNotifier;
31+
private lastSelectedTaskPath?: string;
32+
private lastSelectedTaskPathLoaded = false;
33+
private lastWorkSessionTaskPath?: string;
3134

3235
constructor(plugin: TaskNotesPlugin) {
3336
this.plugin = plugin;
@@ -136,6 +139,8 @@ export class PomodoroService {
136139
}
137140

138141
async saveLastSelectedTask(taskPath: string | undefined) {
142+
this.lastSelectedTaskPath = taskPath;
143+
this.lastSelectedTaskPathLoaded = true;
139144
try {
140145
const data = await this.plugin.loadData() || {};
141146
data.lastSelectedTaskPath = taskPath;
@@ -146,9 +151,19 @@ export class PomodoroService {
146151
}
147152

148153
async getLastSelectedTaskPath(): Promise<string | undefined> {
154+
if (this.lastSelectedTaskPathLoaded) {
155+
return this.lastSelectedTaskPath;
156+
}
149157
try {
150158
const data = await this.plugin.loadData();
151-
return data?.lastSelectedTaskPath;
159+
const path = data?.lastSelectedTaskPath;
160+
if (typeof path === 'string' && path.trim().length > 0) {
161+
this.lastSelectedTaskPath = path;
162+
} else {
163+
this.lastSelectedTaskPath = undefined;
164+
}
165+
this.lastSelectedTaskPathLoaded = true;
166+
return this.lastSelectedTaskPath;
152167
} catch (error) {
153168
console.error('Failed to load last selected task:', error);
154169
return undefined;
@@ -190,7 +205,13 @@ export class PomodoroService {
190205
// endTime will be set when paused or completed
191206
}]
192207
};
193-
208+
209+
if (task?.path) {
210+
this.lastWorkSessionTaskPath = task.path;
211+
this.lastSelectedTaskPath = task.path;
212+
this.lastSelectedTaskPathLoaded = true;
213+
}
214+
194215
this.state.currentSession = session;
195216
this.state.isRunning = true;
196217
this.state.timeRemaining = durationSeconds;
@@ -481,6 +502,72 @@ export class PomodoroService {
481502
}
482503
}
483504

505+
private async autoStartWorkSession(): Promise<void> {
506+
if (this.state.isRunning) {
507+
return;
508+
}
509+
510+
try {
511+
const task = await this.getAutoStartTask();
512+
if (task) {
513+
await this.startPomodoro(task);
514+
} else {
515+
await this.startPomodoro();
516+
}
517+
} catch (error) {
518+
console.error('Failed to auto-start work session:', error);
519+
}
520+
}
521+
522+
private async getAutoStartTask(): Promise<TaskInfo | undefined> {
523+
const candidatePaths: string[] = [];
524+
525+
if (this.lastWorkSessionTaskPath) {
526+
candidatePaths.push(this.lastWorkSessionTaskPath);
527+
}
528+
529+
if (this.state.currentSession?.taskPath) {
530+
candidatePaths.push(this.state.currentSession.taskPath);
531+
}
532+
533+
const persistedPath = await this.getLastSelectedTaskPath();
534+
if (persistedPath) {
535+
candidatePaths.push(persistedPath);
536+
}
537+
538+
const uniquePaths = Array.from(new Set(candidatePaths.filter(path => typeof path === 'string' && path.length > 0)));
539+
540+
for (const path of uniquePaths) {
541+
try {
542+
const task = await this.plugin.cacheManager.getTaskInfo(path);
543+
if (!task) {
544+
this.clearCachedTaskPath(path);
545+
continue;
546+
}
547+
if (task.archived || this.plugin.statusManager.isCompletedStatus(task.status)) {
548+
this.clearCachedTaskPath(path);
549+
continue;
550+
}
551+
return task;
552+
} catch (error) {
553+
console.warn(`Failed to load task for auto-start (${path}):`, error);
554+
}
555+
}
556+
557+
return undefined;
558+
}
559+
560+
private clearCachedTaskPath(path: string): void {
561+
if (this.lastWorkSessionTaskPath === path) {
562+
this.lastWorkSessionTaskPath = undefined;
563+
}
564+
565+
if (this.lastSelectedTaskPath === path) {
566+
this.lastSelectedTaskPath = undefined;
567+
this.lastSelectedTaskPathLoaded = true;
568+
}
569+
}
570+
484571
private async completePomodoro() {
485572
this.stopTimer();
486573

@@ -491,7 +578,11 @@ export class PomodoroService {
491578
const session = this.state.currentSession;
492579
session.completed = true;
493580
session.endTime = getCurrentTimestamp();
494-
581+
582+
if (session.type === 'work' && session.taskPath) {
583+
this.lastWorkSessionTaskPath = session.taskPath;
584+
}
585+
495586
// End the current active period if it's still running
496587
if (session.activePeriods.length > 0) {
497588
const currentPeriod = session.activePeriods[session.activePeriods.length - 1];
@@ -595,7 +686,9 @@ export class PomodoroService {
595686

596687
// Auto-start work if configured, otherwise just prepare the timer
597688
if (this.plugin.settings.pomodoroAutoStartWork) {
598-
const timeout = setTimeout(() => this.startPomodoro(), 1000) as unknown as number;
689+
const timeout = setTimeout(() => {
690+
this.autoStartWorkSession();
691+
}, 1000) as unknown as number;
599692
this.cleanupTimeouts.add(timeout);
600693
}
601694
}
@@ -756,6 +849,17 @@ export class PomodoroService {
756849

757850
// Update the current session's task
758851
this.state.currentSession.taskPath = task?.path;
852+
853+
if (task?.path) {
854+
this.lastWorkSessionTaskPath = task.path;
855+
this.lastSelectedTaskPath = task.path;
856+
this.lastSelectedTaskPathLoaded = true;
857+
} else {
858+
this.lastWorkSessionTaskPath = undefined;
859+
this.lastSelectedTaskPath = undefined;
860+
this.lastSelectedTaskPathLoaded = true;
861+
}
862+
759863
await this.saveState();
760864

761865
// Emit tick event to update UI
@@ -1123,4 +1227,4 @@ export class PomodoroService {
11231227
throw error;
11241228
}
11251229
}
1126-
}
1230+
}

src/utils/viewOptimizations.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ItemView } from 'obsidian';
22
import { ViewPerformanceService, ViewPerformanceConfig, ViewUpdateHandler } from '../services/ViewPerformanceService';
3-
import { TaskInfo } from '../types';
3+
import { TaskInfo, TimeEntry } from '../types';
44
import TaskNotesPlugin from '../main';
55

66
/**
@@ -67,13 +67,19 @@ export function shouldRefreshForDateBasedView(
6767
if (!originalTask) return true;
6868

6969
// For date-based views (calendars, agendas), check if date-related fields or visual properties changed
70+
const timeEntriesChanged = !areTimeEntriesEqual(originalTask.timeEntries, updatedTask.timeEntries);
7071
return originalTask.due !== updatedTask.due ||
7172
originalTask.scheduled !== updatedTask.scheduled ||
7273
originalTask.status !== updatedTask.status ||
7374
originalTask.completedDate !== updatedTask.completedDate ||
7475
originalTask.recurrence !== updatedTask.recurrence ||
7576
originalTask.priority !== updatedTask.priority || // Priority affects event visual appearance
76-
originalTask.title !== updatedTask.title; // Title changes should be reflected
77+
originalTask.title !== updatedTask.title || // Title changes should be reflected
78+
originalTask.archived !== updatedTask.archived || // Archived tasks should disappear from date-based views
79+
originalTask.path !== updatedTask.path || // Path changes indicate the file moved and need re-render
80+
originalTask.timeEstimate !== updatedTask.timeEstimate || // Duration changes affect multi-day rendering
81+
originalTask.totalTrackedTime !== updatedTask.totalTrackedTime || // Time tracking summary changes event info
82+
timeEntriesChanged; // Time entry adjustments affect rendered segments
7783
}
7884

7985
/**
@@ -219,4 +225,23 @@ export class ViewPerformanceMonitor {
219225
this.startTimer(operation);
220226
return fn().finally(() => this.endTimer(operation));
221227
}
222-
}
228+
}
229+
230+
function areTimeEntriesEqual(a?: TimeEntry[], b?: TimeEntry[]): boolean {
231+
if (a === b) return true;
232+
if (!a || !b) return a === b;
233+
if (a.length !== b.length) return false;
234+
235+
for (let i = 0; i < a.length; i++) {
236+
const entryA = a[i];
237+
const entryB = b[i];
238+
if (entryA.startTime !== entryB.startTime ||
239+
entryA.endTime !== entryB.endTime ||
240+
entryA.description !== entryB.description ||
241+
entryA.duration !== entryB.duration) {
242+
return false;
243+
}
244+
}
245+
246+
return true;
247+
}

src/views/AdvancedCalendarView.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ interface CalendarEvent {
8181
recurringTemplateTime?: string; // Original scheduled time
8282
subscriptionName?: string; // For ICS events
8383
attachments?: string[]; // For timeblocks
84+
originalDate?: string; // For timeblock daily note reference
8485
};
8586
}
8687

@@ -1015,6 +1016,8 @@ export class AdvancedCalendarView extends ItemView implements OptimizedView {
10151016
const start = parseDateToLocal(startDate);
10161017
const end = new Date(start.getTime() + (task.timeEstimate * 60 * 1000));
10171018
endDate = format(end, "yyyy-MM-dd'T'HH:mm");
1019+
} else if (!hasTime) {
1020+
endDate = this.calculateAllDayEndDate(startDate, task.timeEstimate);
10181021
}
10191022

10201023
// Get priority-based color for border
@@ -1231,6 +1234,8 @@ export class AdvancedCalendarView extends ItemView implements OptimizedView {
12311234
const start = parseDateToLocal(eventStart);
12321235
const end = new Date(start.getTime() + (task.timeEstimate * 60 * 1000));
12331236
endDate = format(end, "yyyy-MM-dd'T'HH:mm");
1237+
} else if (!hasTime) {
1238+
endDate = this.calculateAllDayEndDate(eventStart, task.timeEstimate);
12341239
}
12351240

12361241
// Get priority-based color for border
@@ -1273,6 +1278,8 @@ export class AdvancedCalendarView extends ItemView implements OptimizedView {
12731278
const start = parseDateToLocal(eventStart);
12741279
const end = new Date(start.getTime() + (task.timeEstimate * 60 * 1000));
12751280
endDate = format(end, "yyyy-MM-dd'T'HH:mm");
1281+
} else if (!hasTime) {
1282+
endDate = this.calculateAllDayEndDate(eventStart, task.timeEstimate);
12761283
}
12771284

12781285
// Get priority-based color for border
@@ -1307,6 +1314,18 @@ export class AdvancedCalendarView extends ItemView implements OptimizedView {
13071314
};
13081315
}
13091316

1317+
private calculateAllDayEndDate(startDate: string, timeEstimate?: number): string | undefined {
1318+
if (!timeEstimate || timeEstimate <= 0) {
1319+
return undefined;
1320+
}
1321+
1322+
const minutesPerDay = 60 * 24;
1323+
const start = parseDateToLocal(startDate);
1324+
const days = Math.max(1, Math.ceil(timeEstimate / minutesPerDay));
1325+
const end = new Date(start.getTime() + (days * minutesPerDay * 60 * 1000));
1326+
return format(end, 'yyyy-MM-dd');
1327+
}
1328+
13101329
// Event handlers
13111330
handleDateSelect(selectInfo: any) {
13121331
const { start, end, allDay, jsEvent } = selectInfo;
@@ -1496,7 +1515,7 @@ export class AdvancedCalendarView extends ItemView implements OptimizedView {
14961515
console.warn('[AdvancedCalendarView] Event clicked without extendedProps');
14971516
return;
14981517
}
1499-
const { taskInfo, icsEvent, timeblock, eventType, subscriptionName } = clickInfo.event.extendedProps;
1518+
const { taskInfo, icsEvent, timeblock, eventType, subscriptionName, originalDate } = clickInfo.event.extendedProps;
15001519
const jsEvent = clickInfo.jsEvent;
15011520

15021521
// Skip task events in list view - they have their own TaskCard-style handlers
@@ -1515,8 +1534,8 @@ export class AdvancedCalendarView extends ItemView implements OptimizedView {
15151534
}
15161535

15171536
if (eventType === 'timeblock') {
1518-
// Timeblocks are read-only for now, could add editing later
1519-
this.showTimeblockInfo(timeblock, clickInfo.event.start);
1537+
// Timeblocks can be edited via modal
1538+
this.showTimeblockInfo(timeblock, clickInfo.event.start, originalDate);
15201539
return;
15211540
}
15221541

@@ -2767,8 +2786,8 @@ export class AdvancedCalendarView extends ItemView implements OptimizedView {
27672786
modal.open();
27682787
}
27692788

2770-
private showTimeblockInfo(timeblock: TimeBlock, eventDate: Date): void {
2771-
const modal = new TimeblockInfoModal(this.app, this.plugin, timeblock, eventDate);
2789+
private showTimeblockInfo(timeblock: TimeBlock, eventDate: Date, originalDate?: string): void {
2790+
const modal = new TimeblockInfoModal(this.app, this.plugin, timeblock, eventDate, originalDate);
27722791
modal.open();
27732792
}
27742793

0 commit comments

Comments
 (0)