feat(time-parsing): improve emoji-based date type detection and time preservation

This commit is contained in:
Quorafind 2025-11-30 15:45:10 +08:00
parent c35880eb6f
commit 82a20200a3
15 changed files with 2531 additions and 1318 deletions

@ -1 +1 @@
Subproject commit 3a269f99499732440f56573a382f6bc6a1dcd0aa
Subproject commit 65aaa0ee9840428dc14f96fc120c004e5cbaf413

File diff suppressed because it is too large Load diff

View file

@ -57,10 +57,6 @@ import "@/styles/calendar/event.css";
import "@/styles/calendar/badge.css";
import { t } from "@/translations/helper";
// Import original view implementations for agenda/year (legacy, kept for reference)
import { AgendaView } from "./views/agenda-view";
import { YearView } from "./views/year-view";
// Import new TG-based view implementations
import { TGAgendaView } from "./views/tg-agenda-view";
import { TGYearView } from "./views/tg-year-view";
@ -273,10 +269,6 @@ export class CalendarComponent extends Component {
// View registry for custom views (agenda, year, and user-defined)
private viewRegistry: ViewRegistry = new ViewRegistry();
// Original view components (legacy, kept for backward compatibility)
private agendaView: AgendaView | null = null;
private yearView: YearView | null = null;
// Badge events cache
private badgeEventsCache: Map<string, CalendarEvent[]> = new Map();
@ -381,16 +373,6 @@ export class CalendarComponent extends Component {
this.tgCalendar = null;
}
// Clean up original views
if (this.agendaView) {
this.removeChild(this.agendaView);
this.agendaView = null;
}
if (this.yearView) {
this.removeChild(this.yearView);
this.yearView = null;
}
this.containerEl.empty();
}
@ -462,10 +444,6 @@ export class CalendarComponent extends Component {
this.render();
}
public get currentViewComponent() {
return this.agendaView || this.yearView || null;
}
public getBadgeEventsForDate(date: Date): CalendarEvent[] {
const dateKey = this.formatDateKey(date);
@ -723,14 +701,6 @@ export class CalendarComponent extends Component {
this.tgCalendar.destroy();
this.tgCalendar = null;
}
if (this.agendaView) {
this.removeChild(this.agendaView);
this.agendaView = null;
}
if (this.yearView) {
this.removeChild(this.yearView);
this.yearView = null;
}
// Update view class
this.viewContainerEl.removeClass(
@ -928,7 +898,7 @@ export class CalendarComponent extends Component {
},
draggable: {
enabled: true,
snapMinutes: 1440,
snapMinutes: 15, // 15-minute snap for range selection support
ghostOpacity: 0.5,
},
theme: {
@ -955,6 +925,11 @@ export class CalendarComponent extends Component {
this.handleTimeSlotClick(dateTime),
onTimeSlotDoubleClick: (dateTime: Date) =>
this.handleTimeSlotDoubleClick(dateTime),
// Range selection (drag to select multiple cells)
onDateRangeSelect: (startDate: Date, endDate: Date) =>
this.handleDateRangeSelect(startDate, endDate),
onTimeRangeSelect: (startDateTime: Date, endDateTime: Date) =>
this.handleTimeRangeSelect(startDateTime, endDateTime),
onRenderDateCell: (ctx: any) => this.handleTGRenderDateCell(ctx),
// Custom event rendering - adds checkbox for task completion
onRenderEvent: (ctx: EventRenderContext) =>
@ -1011,7 +986,7 @@ export class CalendarComponent extends Component {
},
draggable: {
enabled: true,
snapMinutes: 1440, // Date-only drag
snapMinutes: 15, // 15-minute snap for range selection support
ghostOpacity: 0.5,
},
theme: {
@ -1042,6 +1017,11 @@ export class CalendarComponent extends Component {
this.handleTimeSlotClick(dateTime),
onTimeSlotDoubleClick: (dateTime: Date) =>
this.handleTimeSlotDoubleClick(dateTime),
// Range selection (drag to select multiple cells)
onDateRangeSelect: (startDate: Date, endDate: Date) =>
this.handleDateRangeSelect(startDate, endDate),
onTimeRangeSelect: (startDateTime: Date, endDateTime: Date) =>
this.handleTimeRangeSelect(startDateTime, endDateTime),
// Custom cell rendering
onRenderDateCell: (ctx: any) => this.handleTGRenderDateCell(ctx),
// Custom event rendering - adds checkbox for task completion
@ -1102,46 +1082,6 @@ export class CalendarComponent extends Component {
this.tgCalendar.goToDate(this.currentDate.toDate());
}
private renderAgendaView() {
this.agendaView = new AgendaView(
this.app,
this.plugin,
this.viewContainerEl,
this.currentDate,
this.events,
{
onEventClick: this.onEventClick,
onEventHover: this.onEventHover,
onEventContextMenu: this.onEventContextMenu,
onEventComplete: this.onEventComplete,
},
);
this.addChild(this.agendaView);
this.agendaView.updateEvents(this.events);
}
private renderYearView() {
const config = this.getEffectiveCalendarConfig();
this.yearView = new YearView(
this.app,
this.plugin,
this.viewContainerEl,
this.currentDate,
this.events,
{
onEventClick: this.onEventClick,
onEventHover: this.onEventHover,
onDayClick: this.onDayClick,
onDayHover: this.onDayHover,
onMonthClick: this.onMonthClick,
onMonthHover: this.onMonthHover,
},
config,
);
this.addChild(this.yearView);
this.yearView.updateEvents(this.events);
}
// ============================================
// Event Handlers
// ============================================
@ -1790,6 +1730,38 @@ export class CalendarComponent extends Component {
).open();
}
/**
* Handle date range selection in month view
* Opens quick capture modal with start and due dates pre-filled
*/
private handleDateRangeSelect(startDate: Date, endDate: Date) {
new QuickCaptureModal(
this.app,
this.plugin,
{
startDate: startDate,
dueDate: endDate,
},
true,
).open();
}
/**
* Handle time range selection in week/day view
* Opens quick capture modal with start and due dates/times pre-filled
*/
private handleTimeRangeSelect(startDateTime: Date, endDateTime: Date) {
new QuickCaptureModal(
this.app,
this.plugin,
{
startDate: startDateTime,
dueDate: endDateTime,
},
true,
).open();
}
// ============================================
// Original Event Handlers (for Agenda/Year views)
// ============================================

View file

@ -5,7 +5,7 @@ import type { EnhancedTimeParsingConfig } from "@/types/time-parsing";
export function renderTimeParsingSettingsTab(
pluginSettingTab: TaskProgressBarSettingTab,
containerEl: HTMLElement
containerEl: HTMLElement,
) {
containerEl.createEl("h2", { text: t("Time Parsing Settings") });
@ -46,16 +46,17 @@ export function renderTimeParsingSettingsTab(
.setName(t("Enable Time Parsing"))
.setDesc(
t(
"Automatically parse natural language time expressions and specific times (12:00, 1:30 PM, 12:00-13:00)"
)
"Automatically parse natural language time expressions and specific times (12:00, 1:30 PM, 12:00-13:00)",
),
)
.addToggle((toggle) =>
toggle
.setValue(pluginSettingTab.plugin.settings.timeParsing.enabled)
.onChange(async (value) => {
pluginSettingTab.plugin.settings.timeParsing.enabled = value;
pluginSettingTab.plugin.settings.timeParsing.enabled =
value;
pluginSettingTab.applySettingsUpdate();
})
}),
);
// Remove Original Text
@ -66,27 +67,30 @@ export function renderTimeParsingSettingsTab(
toggle
.setValue(
pluginSettingTab.plugin.settings.timeParsing
?.removeOriginalText ?? true
?.removeOriginalText ?? true,
)
.onChange(async (value) => {
if (!pluginSettingTab.plugin.settings.timeParsing) return;
pluginSettingTab.plugin.settings.timeParsing.removeOriginalText =
value;
pluginSettingTab.applySettingsUpdate();
})
}),
);
// Supported Languages
containerEl.createEl("h3", { text: t("Supported Languages") });
containerEl.createEl("p", {
text: t(
"Currently supports English and Chinese time expressions. More languages may be added in future updates."
),
cls: "setting-item-description",
});
new Setting(containerEl)
.setName(t("Supported Languages"))
.setDesc(
t(
"Currently supports English and Chinese time expressions. More languages may be added in future updates.",
),
)
.setHeading();
// Date Keywords Configuration
containerEl.createEl("h3", { text: t("Date Keywords Configuration") });
new Setting(containerEl)
.setName(t("Date Keywords Configuration"))
.setDesc(t("Configure keywords for date parsing"))
.setHeading();
// Start Date Keywords
new Setting(containerEl)
@ -154,8 +158,10 @@ export function renderTimeParsingSettingsTab(
text.inputEl.rows = 2;
});
// Time Format Configuration
containerEl.createEl("h3", { text: t("Time Format Configuration") });
new Setting(containerEl)
.setName(t("Time Format Configuration"))
.setDesc(t("Configure the format of time expressions"))
.setHeading();
// Preferred Time Format
new Setting(containerEl)
@ -165,16 +171,24 @@ export function renderTimeParsingSettingsTab(
dropdown
.addOption("12h", t("12-hour format (1:30 PM)"))
.addOption("24h", t("24-hour format (13:30)"))
.setValue(pluginSettingTab.plugin.settings.timeParsing.timeDefaults?.preferredFormat || "24h")
.setValue(
pluginSettingTab.plugin.settings.timeParsing.timeDefaults
?.preferredFormat || "24h",
)
.onChange(async (value: "12h" | "24h") => {
if (!pluginSettingTab.plugin.settings.timeParsing.timeDefaults) {
pluginSettingTab.plugin.settings.timeParsing.timeDefaults = {
preferredFormat: value,
defaultPeriod: "AM",
midnightCrossing: "next-day",
};
if (
!pluginSettingTab.plugin.settings.timeParsing
.timeDefaults
) {
pluginSettingTab.plugin.settings.timeParsing.timeDefaults =
{
preferredFormat: value,
defaultPeriod: "AM",
midnightCrossing: "next-day",
};
} else {
pluginSettingTab.plugin.settings.timeParsing.timeDefaults.preferredFormat = value;
pluginSettingTab.plugin.settings.timeParsing.timeDefaults.preferredFormat =
value;
}
pluginSettingTab.applySettingsUpdate();
});
@ -188,16 +202,24 @@ export function renderTimeParsingSettingsTab(
dropdown
.addOption("AM", t("AM (Morning)"))
.addOption("PM", t("PM (Afternoon/Evening)"))
.setValue(pluginSettingTab.plugin.settings.timeParsing.timeDefaults?.defaultPeriod || "AM")
.setValue(
pluginSettingTab.plugin.settings.timeParsing.timeDefaults
?.defaultPeriod || "AM",
)
.onChange(async (value: "AM" | "PM") => {
if (!pluginSettingTab.plugin.settings.timeParsing.timeDefaults) {
pluginSettingTab.plugin.settings.timeParsing.timeDefaults = {
preferredFormat: "24h",
defaultPeriod: value,
midnightCrossing: "next-day",
};
if (
!pluginSettingTab.plugin.settings.timeParsing
.timeDefaults
) {
pluginSettingTab.plugin.settings.timeParsing.timeDefaults =
{
preferredFormat: "24h",
defaultPeriod: value,
midnightCrossing: "next-day",
};
} else {
pluginSettingTab.plugin.settings.timeParsing.timeDefaults.defaultPeriod = value;
pluginSettingTab.plugin.settings.timeParsing.timeDefaults.defaultPeriod =
value;
}
pluginSettingTab.applySettingsUpdate();
});
@ -206,22 +228,37 @@ export function renderTimeParsingSettingsTab(
// Midnight Crossing Behavior
new Setting(containerEl)
.setName(t("Midnight Crossing Behavior"))
.setDesc(t("How to handle time ranges that cross midnight (e.g., 23:00-01:00)"))
.setDesc(
t(
"How to handle time ranges that cross midnight (e.g., 23:00-01:00)",
),
)
.addDropdown((dropdown) => {
dropdown
.addOption("next-day", t("Next day (23:00 today - 01:00 tomorrow)"))
.addOption(
"next-day",
t("Next day (23:00 today - 01:00 tomorrow)"),
)
.addOption("same-day", t("Same day (treat as error)"))
.addOption("error", t("Show error"))
.setValue(pluginSettingTab.plugin.settings.timeParsing.timeDefaults?.midnightCrossing || "next-day")
.setValue(
pluginSettingTab.plugin.settings.timeParsing.timeDefaults
?.midnightCrossing || "next-day",
)
.onChange(async (value: "next-day" | "same-day" | "error") => {
if (!pluginSettingTab.plugin.settings.timeParsing.timeDefaults) {
pluginSettingTab.plugin.settings.timeParsing.timeDefaults = {
preferredFormat: "24h",
defaultPeriod: "AM",
midnightCrossing: value,
};
if (
!pluginSettingTab.plugin.settings.timeParsing
.timeDefaults
) {
pluginSettingTab.plugin.settings.timeParsing.timeDefaults =
{
preferredFormat: "24h",
defaultPeriod: "AM",
midnightCrossing: value,
};
} else {
pluginSettingTab.plugin.settings.timeParsing.timeDefaults.midnightCrossing = value;
pluginSettingTab.plugin.settings.timeParsing.timeDefaults.midnightCrossing =
value;
}
pluginSettingTab.applySettingsUpdate();
});
@ -232,19 +269,30 @@ export function renderTimeParsingSettingsTab(
.setName(t("Time Range Separators"))
.setDesc(t("Characters used to separate time ranges (comma-separated)"))
.addTextArea((text) => {
const separators = pluginSettingTab.plugin.settings.timeParsing.timePatterns?.rangeSeparators || ["-", "~", ""];
const separators = pluginSettingTab.plugin.settings.timeParsing
.timePatterns?.rangeSeparators || ["-", "~", ""];
text.setValue(separators.join(", "))
.setPlaceholder("-, ~, , ' - ', ' ~ '")
.onChange(async (value) => {
if (!pluginSettingTab.plugin.settings.timeParsing.timePatterns) {
pluginSettingTab.plugin.settings.timeParsing.timePatterns = {
singleTime: [],
timeRange: [],
rangeSeparators: value.split(",").map(s => s.trim()).filter(s => s.length > 0),
};
if (
!pluginSettingTab.plugin.settings.timeParsing
.timePatterns
) {
pluginSettingTab.plugin.settings.timeParsing.timePatterns =
{
singleTime: [],
timeRange: [],
rangeSeparators: value
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0),
};
} else {
pluginSettingTab.plugin.settings.timeParsing.timePatterns.rangeSeparators =
value.split(",").map(s => s.trim()).filter(s => s.length > 0);
pluginSettingTab.plugin.settings.timeParsing.timePatterns.rangeSeparators =
value
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
}
pluginSettingTab.applySettingsUpdate();
});
@ -252,7 +300,10 @@ export function renderTimeParsingSettingsTab(
});
// Examples
containerEl.createEl("h3", { text: t("Examples") });
new Setting(containerEl)
.setName(t("Examples"))
.setDesc(t("Examples of time expressions"))
.setHeading();
const examplesEl = containerEl.createEl("div", {
cls: "time-parsing-examples",
});
@ -265,10 +316,22 @@ export function renderTimeParsingSettingsTab(
{ input: "明天开会", output: "开会 📅 2025-01-05" },
{ input: "3天后完成", output: "完成 📅 2025-01-07" },
// Time examples
{ input: "meeting at 2:30 PM", output: "meeting 📅 2025-01-04 ⏰ 14:30" },
{ input: "workshop 9:00-17:00", output: "workshop 📅 2025-01-04 ⏰ 09:00-17:00" },
{ input: "call scheduled 12:00", output: "call 📅 2025-01-04 ⏰ 12:00" },
{ input: "lunch 12:0013:00", output: "lunch 📅 2025-01-04 ⏰ 12:00-13:00" },
{
input: "meeting at 2:30 PM",
output: "meeting 📅 2025-01-04 ⏰ 14:30",
},
{
input: "workshop 9:00-17:00",
output: "workshop 📅 2025-01-04 ⏰ 09:00-17:00",
},
{
input: "call scheduled 12:00",
output: "call 📅 2025-01-04 ⏰ 12:00",
},
{
input: "lunch 12:0013:00",
output: "lunch 📅 2025-01-04 ⏰ 12:00-13:00",
},
];
examples.forEach((example) => {

View file

@ -674,6 +674,22 @@ export class MarkdownTaskParser {
);
}
// Fallback: If start date exists but no start time, and we have a due time (default context)
// but no due date, assume the time belongs to the start date.
// This handles cases like "🛫 2025-11-29 18:00" where the time defaults to "due" context
// but should actually be associated with the start date.
if (
dates.startDate &&
!timeComponents.startTime &&
!dates.dueDate &&
timeComponents.dueTime
) {
enhancedDates.startDateTime = combineDateTime(
dates.startDate,
timeComponents.dueTime,
);
}
// Combine due date with due time
if (dates.dueDate && timeComponents.dueTime) {
enhancedDates.dueDateTime = combineDateTime(

View file

@ -82,7 +82,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
constructor(
private app: App,
private fileSourceConfig?: FileSourceConfiguration,
timeParsingService?: TimeParsingService
timeParsingService?: TimeParsingService,
) {
this.timeParsingService = timeParsingService;
}
@ -92,7 +92,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
*/
entryToFileTask(
entry: BasesEntry,
mapping: FileTaskPropertyMapping = DEFAULT_FILE_TASK_MAPPING
mapping: FileTaskPropertyMapping = DEFAULT_FILE_TASK_MAPPING,
): FileTask {
const properties = entry.properties || {};
@ -104,7 +104,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
// Log 10% of entries to avoid spam
console.log(
`[FileTaskManager] Available properties for ${entry.file.name}:`,
Object.keys(properties)
Object.keys(properties),
);
}
@ -132,29 +132,29 @@ export class FileTaskManagerImpl implements FileTaskManager {
// Extract dates
const createdDate = this.getDatePropertyValue(
entry,
mapping.createdDateProperty
mapping.createdDateProperty,
);
const startDate = this.getDatePropertyValue(
entry,
mapping.startDateProperty
mapping.startDateProperty,
);
const scheduledDate = this.getDatePropertyValue(
entry,
mapping.scheduledDateProperty
mapping.scheduledDateProperty,
);
const dueDate = this.getDatePropertyValue(
entry,
mapping.dueDateProperty
mapping.dueDateProperty,
);
const completedDate = this.getDatePropertyValue(
entry,
mapping.completedDateProperty
mapping.completedDateProperty,
);
// Extract other properties
const recurrence = this.getPropertyValue(
entry,
mapping.recurrenceProperty
mapping.recurrenceProperty,
);
const tags =
this.getArrayPropertyValue(entry, mapping.tagsProperty) || [];
@ -162,15 +162,15 @@ export class FileTaskManagerImpl implements FileTaskManager {
const context = this.getPropertyValue(entry, mapping.contextProperty);
const priority = this.getNumberPropertyValue(
entry,
mapping.priorityProperty
mapping.priorityProperty,
);
const estimatedTime = this.getNumberPropertyValue(
entry,
mapping.estimatedTimeProperty
mapping.estimatedTimeProperty,
);
const actualTime = this.getNumberPropertyValue(
entry,
mapping.actualTimeProperty
mapping.actualTimeProperty,
);
// Extract time components from content using enhanced time parsing
@ -178,8 +178,8 @@ export class FileTaskManagerImpl implements FileTaskManager {
// Combine dates with time components to create enhanced datetime objects
const enhancedDates = this.combineTimestampsWithTimeComponents(
{startDate, dueDate, scheduledDate, completedDate},
enhancedMetadata.timeComponents
{ startDate, dueDate, scheduledDate, completedDate },
enhancedMetadata.timeComponents,
);
const fileTask: FileTask = {
@ -193,21 +193,21 @@ export class FileTaskManagerImpl implements FileTaskManager {
children: [], // File tasks don't have children by default
// Optional properties
...(createdDate && {createdDate}),
...(startDate && {startDate}),
...(scheduledDate && {scheduledDate}),
...(dueDate && {dueDate}),
...(completedDate && {completedDate}),
...(recurrence && {recurrence}),
...(project && {project}),
...(context && {context}),
...(priority && {priority}),
...(estimatedTime && {estimatedTime}),
...(actualTime && {actualTime}),
...(createdDate && { createdDate }),
...(startDate && { startDate }),
...(scheduledDate && { scheduledDate }),
...(dueDate && { dueDate }),
...(completedDate && { completedDate }),
...(recurrence && { recurrence }),
...(project && { project }),
...(context && { context }),
...(priority && { priority }),
...(estimatedTime && { estimatedTime }),
...(actualTime && { actualTime }),
// Enhanced time components
...enhancedMetadata,
...(enhancedDates && {enhancedDates}),
...(enhancedDates && { enhancedDates }),
},
sourceEntry: entry,
isFileTask: true,
@ -222,7 +222,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
fileTaskToPropertyUpdates(
task: FileTask,
mapping: FileTaskPropertyMapping = DEFAULT_FILE_TASK_MAPPING,
excludeContent: boolean = false
excludeContent: boolean = false,
): Record<string, any> {
const updates: Record<string, any> = {};
@ -230,9 +230,10 @@ export class FileTaskManagerImpl implements FileTaskManager {
// Skip content if it was already handled separately (e.g., in handleContentUpdate)
if (!excludeContent) {
const config = this.fileSourceConfig?.fileTaskProperties;
if (config?.contentSource && config.contentSource !== 'filename') {
if (config?.contentSource && config.contentSource !== "filename") {
// Only update content property if it's not handled by file renaming
const shouldUpdateProperty = this.shouldUpdateContentProperty(config);
const shouldUpdateProperty =
this.shouldUpdateContentProperty(config);
if (shouldUpdateProperty) {
updates[mapping.contentProperty] = task.content;
}
@ -249,7 +250,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
mapping.createdDateProperty
) {
updates[mapping.createdDateProperty] = this.formatDateForProperty(
task.metadata.createdDate
task.metadata.createdDate,
);
}
if (
@ -257,7 +258,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
mapping.startDateProperty
) {
updates[mapping.startDateProperty] = this.formatDateForProperty(
task.metadata.startDate
task.metadata.startDate,
);
}
if (
@ -265,12 +266,12 @@ export class FileTaskManagerImpl implements FileTaskManager {
mapping.scheduledDateProperty
) {
updates[mapping.scheduledDateProperty] = this.formatDateForProperty(
task.metadata.scheduledDate
task.metadata.scheduledDate,
);
}
if (task.metadata.dueDate !== undefined && mapping.dueDateProperty) {
updates[mapping.dueDateProperty] = this.formatDateForProperty(
task.metadata.dueDate
task.metadata.dueDate,
);
}
if (
@ -278,7 +279,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
mapping.completedDateProperty
) {
updates[mapping.completedDateProperty] = this.formatDateForProperty(
task.metadata.completedDate
task.metadata.completedDate,
);
}
if (
@ -321,10 +322,10 @@ export class FileTaskManagerImpl implements FileTaskManager {
*/
async updateFileTask(
task: FileTask,
updates: Partial<FileTask>
updates: Partial<FileTask>,
): Promise<void> {
// Merge updates into the task
const updatedTask = {...task, ...updates};
const updatedTask = { ...task, ...updates };
let contentHandledSeparately = false;
// Handle content changes - re-extract time components if content changed
@ -333,7 +334,9 @@ export class FileTaskManagerImpl implements FileTaskManager {
contentHandledSeparately = true;
// Re-extract time components from updated content
const enhancedMetadata = this.extractTimeComponents(updates.content);
const enhancedMetadata = this.extractTimeComponents(
updates.content,
);
// Update the task's metadata with new time components
if (enhancedMetadata.timeComponents) {
@ -350,7 +353,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
scheduledDate: updatedTask.metadata.scheduledDate,
completedDate: updatedTask.metadata.completedDate,
},
enhancedMetadata.timeComponents
enhancedMetadata.timeComponents,
);
if (enhancedDates) {
@ -366,22 +369,26 @@ export class FileTaskManagerImpl implements FileTaskManager {
const propertyUpdates = this.fileTaskToPropertyUpdates(
updatedTask,
DEFAULT_FILE_TASK_MAPPING,
contentHandledSeparately
contentHandledSeparately,
);
console.log(
`[FileTaskManager] Updating file task ${task.content} with properties:`,
propertyUpdates
propertyUpdates,
);
// Update properties through the source entry
for (const [key, value] of Object.entries(propertyUpdates)) {
try {
// Note: updateProperty might be async, so we await it
if (typeof task.sourceEntry.updateProperty === 'function') {
await Promise.resolve(task.sourceEntry.updateProperty(key, value));
if (typeof task.sourceEntry.updateProperty === "function") {
await Promise.resolve(
task.sourceEntry.updateProperty(key, value),
);
} else {
console.error(`updateProperty method not available on source entry for key: ${key}`);
console.error(
`updateProperty method not available on source entry for key: ${key}`,
);
}
} catch (error) {
console.error(`Failed to update property ${key}:`, error);
@ -394,16 +401,16 @@ export class FileTaskManagerImpl implements FileTaskManager {
*/
private shouldUpdateContentProperty(config: any): boolean {
switch (config.contentSource) {
case 'title':
case "title":
// Only update property if preferFrontmatterTitle is enabled
return config.preferFrontmatterTitle === true;
case 'h1':
case "h1":
// H1 updates are handled by file content modification, not property updates
return false;
case 'custom':
case "custom":
// Custom fields are always updated via properties
return true;
case 'filename':
case "filename":
default:
// Filename updates are handled by file renaming
return false;
@ -415,17 +422,19 @@ export class FileTaskManagerImpl implements FileTaskManager {
*/
private async handleContentUpdate(
task: FileTask,
newContent: string
newContent: string,
): Promise<void> {
const config = this.fileSourceConfig?.fileTaskProperties;
if (!config) {
console.warn('[FileTaskManager] No file source config available, skipping content update');
console.warn(
"[FileTaskManager] No file source config available, skipping content update",
);
return;
}
switch (config.contentSource) {
case 'title':
case "title":
if (config.preferFrontmatterTitle) {
await this.updateFrontmatterTitle(task, newContent);
} else {
@ -433,21 +442,27 @@ export class FileTaskManagerImpl implements FileTaskManager {
}
break;
case 'h1':
case "h1":
// For H1 content source, we need to update the first heading in the file
await this.updateH1Heading(task, newContent);
break;
case 'custom':
case "custom":
// For custom content source, update the custom field in frontmatter
if (config.customContentField) {
await this.updateCustomContentField(task, newContent, config.customContentField);
await this.updateCustomContentField(
task,
newContent,
config.customContentField,
);
} else {
console.warn('[FileTaskManager] Custom content source specified but no customContentField configured');
console.warn(
"[FileTaskManager] Custom content source specified but no customContentField configured",
);
}
break;
case 'filename':
case "filename":
default:
// For filename content source, rename the file
await this.updateFileName(task, newContent);
@ -460,21 +475,28 @@ export class FileTaskManagerImpl implements FileTaskManager {
*/
private async updateFrontmatterTitle(
task: FileTask,
newTitle: string
newTitle: string,
): Promise<void> {
try {
// Update the title property in frontmatter through the source entry
// Note: updateProperty might be async, so we await it
if (typeof task.sourceEntry.updateProperty === 'function') {
await Promise.resolve(task.sourceEntry.updateProperty('title', newTitle));
if (typeof task.sourceEntry.updateProperty === "function") {
await Promise.resolve(
task.sourceEntry.updateProperty("title", newTitle),
);
console.log(
`[FileTaskManager] Updated frontmatter title for ${task.filePath} to: ${newTitle}`
`[FileTaskManager] Updated frontmatter title for ${task.filePath} to: ${newTitle}`,
);
} else {
throw new Error('updateProperty method not available on source entry');
throw new Error(
"updateProperty method not available on source entry",
);
}
} catch (error) {
console.error(`[FileTaskManager] Failed to update frontmatter title:`, error);
console.error(
`[FileTaskManager] Failed to update frontmatter title:`,
error,
);
// Fallback to file renaming if frontmatter update fails
await this.updateFileName(task, newTitle);
}
@ -485,22 +507,24 @@ export class FileTaskManagerImpl implements FileTaskManager {
*/
private async updateH1Heading(
task: FileTask,
newHeading: string
newHeading: string,
): Promise<void> {
try {
const file = this.app.vault.getFileByPath(task.filePath);
if (!file) {
console.error(`[FileTaskManager] File not found: ${task.filePath}`);
console.error(
`[FileTaskManager] File not found: ${task.filePath}`,
);
return;
}
const content = await this.app.vault.read(file);
const lines = content.split('\n');
const lines = content.split("\n");
// Find the first H1 heading
let h1LineIndex = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('# ')) {
if (lines[i].startsWith("# ")) {
h1LineIndex = i;
break;
}
@ -512,25 +536,30 @@ export class FileTaskManagerImpl implements FileTaskManager {
} else {
// Add new H1 at the beginning (after frontmatter if present)
let insertIndex = 0;
if (content.startsWith('---')) {
if (content.startsWith("---")) {
// Skip frontmatter
const frontmatterEnd = content.indexOf('\n---\n', 3);
const frontmatterEnd = content.indexOf("\n---\n", 3);
if (frontmatterEnd >= 0) {
const frontmatterLines = content.substring(0, frontmatterEnd + 5).split('\n').length - 1;
const frontmatterLines =
content.substring(0, frontmatterEnd + 5).split("\n")
.length - 1;
insertIndex = frontmatterLines;
}
}
lines.splice(insertIndex, 0, `# ${newHeading}`, '');
lines.splice(insertIndex, 0, `# ${newHeading}`, "");
}
const newContent = lines.join('\n');
const newContent = lines.join("\n");
await this.app.vault.modify(file, newContent);
console.log(
`[FileTaskManager] Updated H1 heading for ${task.filePath} to: ${newHeading}`
`[FileTaskManager] Updated H1 heading for ${task.filePath} to: ${newHeading}`,
);
} catch (error) {
console.error(`[FileTaskManager] Failed to update H1 heading:`, error);
console.error(
`[FileTaskManager] Failed to update H1 heading:`,
error,
);
}
}
@ -540,21 +569,28 @@ export class FileTaskManagerImpl implements FileTaskManager {
private async updateCustomContentField(
task: FileTask,
newContent: string,
fieldName: string
fieldName: string,
): Promise<void> {
try {
// Update the custom field in frontmatter through the source entry
// Note: updateProperty might be async, so we await it
if (typeof task.sourceEntry.updateProperty === 'function') {
await Promise.resolve(task.sourceEntry.updateProperty(fieldName, newContent));
if (typeof task.sourceEntry.updateProperty === "function") {
await Promise.resolve(
task.sourceEntry.updateProperty(fieldName, newContent),
);
console.log(
`[FileTaskManager] Updated custom field '${fieldName}' for ${task.filePath} to: ${newContent}`
`[FileTaskManager] Updated custom field '${fieldName}' for ${task.filePath} to: ${newContent}`,
);
} else {
throw new Error('updateProperty method not available on source entry');
throw new Error(
"updateProperty method not available on source entry",
);
}
} catch (error) {
console.error(`[FileTaskManager] Failed to update custom field '${fieldName}':`, error);
console.error(
`[FileTaskManager] Failed to update custom field '${fieldName}':`,
error,
);
}
}
@ -563,7 +599,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
*/
private async updateFileName(
task: FileTask,
newContent: string
newContent: string,
): Promise<void> {
try {
const file = this.app.vault.getFileByPath(task.filePath);
@ -575,7 +611,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
? currentPath.substring(0, lastSlashIndex)
: "";
const extension = currentPath.substring(
currentPath.lastIndexOf(".")
currentPath.lastIndexOf("."),
);
// Ensure newContent doesn't already have the extension
@ -583,7 +619,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
if (cleanContent.endsWith(extension)) {
cleanContent = cleanContent.substring(
0,
cleanContent.length - extension.length
cleanContent.length - extension.length,
);
}
@ -597,7 +633,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
// Update the task's filePath to reflect the new path
task.filePath = newPath;
console.log(
`[FileTaskManager] Renamed file from ${currentPath} to ${newPath}`
`[FileTaskManager] Renamed file from ${currentPath} to ${newPath}`,
);
}
}
@ -611,7 +647,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
*/
getFileTasksFromEntries(
entries: BasesEntry[],
mapping: FileTaskPropertyMapping = DEFAULT_FILE_TASK_MAPPING
mapping: FileTaskPropertyMapping = DEFAULT_FILE_TASK_MAPPING,
): FileTask[] {
// Filter out non-markdown files with robust extension detection
const markdownEntries = entries.filter((entry) => {
@ -642,11 +678,11 @@ export class FileTaskManagerImpl implements FileTaskManager {
});
console.log(
`[FileTaskManager] Filtered ${entries.length} entries to ${markdownEntries.length} markdown files`
`[FileTaskManager] Filtered ${entries.length} entries to ${markdownEntries.length} markdown files`,
);
return markdownEntries.map((entry) =>
this.entryToFileTask(entry, mapping)
this.entryToFileTask(entry, mapping),
);
}
@ -665,7 +701,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
private getPropertyValue(
entry: BasesEntry,
propertyName?: string
propertyName?: string,
): string | undefined {
if (!propertyName) return undefined;
// 1) Try Bases API
@ -675,8 +711,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
name: propertyName,
});
if (value !== null && value !== undefined) return String(value);
} catch {
}
} catch {}
// 2) Fallback: direct properties/frontmatter/note.data
try {
const anyEntry: any = entry as any;
@ -698,14 +733,13 @@ export class FileTaskManagerImpl implements FileTaskManager {
) {
return String(anyEntry.note.data[propertyName]);
}
} catch {
}
} catch {}
return undefined;
}
private getBooleanPropertyValue(
entry: BasesEntry,
propertyName?: string
propertyName?: string,
): boolean | undefined {
if (!propertyName) return undefined;
try {
@ -726,7 +760,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
private getNumberPropertyValue(
entry: BasesEntry,
propertyName?: string
propertyName?: string,
): number | undefined {
if (!propertyName) return undefined;
try {
@ -743,7 +777,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
private getDatePropertyValue(
entry: BasesEntry,
propertyName?: string
propertyName?: string,
): number | undefined {
if (!propertyName) return undefined;
try {
@ -813,7 +847,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
private getArrayPropertyValue(
entry: BasesEntry,
propertyName?: string
propertyName?: string,
): string[] | undefined {
if (!propertyName) return undefined;
try {
@ -872,23 +906,32 @@ export class FileTaskManagerImpl implements FileTaskManager {
/**
* Extract time components from task content using enhanced time parsing
*/
private extractTimeComponents(content: string): Partial<EnhancedStandardTaskMetadata> {
private extractTimeComponents(
content: string,
): Partial<EnhancedStandardTaskMetadata> {
if (!this.timeParsingService) {
return {};
}
try {
// Parse time components from content
const {timeComponents, errors, warnings} = this.timeParsingService.parseTimeComponents(content);
const { timeComponents, errors, warnings } =
this.timeParsingService.parseTimeComponents(content);
// Log warnings if any
if (warnings.length > 0) {
console.warn(`[FileTaskManager] Time parsing warnings for "${content}":`, warnings);
console.warn(
`[FileTaskManager] Time parsing warnings for "${content}":`,
warnings,
);
}
// Log errors if any (but don't fail)
if (errors.length > 0) {
console.warn(`[FileTaskManager] Time parsing errors for "${content}":`, errors);
console.warn(
`[FileTaskManager] Time parsing errors for "${content}":`,
errors,
);
}
// Return enhanced metadata with time components
@ -900,7 +943,10 @@ export class FileTaskManagerImpl implements FileTaskManager {
return enhancedMetadata;
} catch (error) {
console.error(`[FileTaskManager] Failed to extract time components from "${content}":`, error);
console.error(
`[FileTaskManager] Failed to extract time components from "${content}":`,
error,
);
return {};
}
}
@ -915,7 +961,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
scheduledDate?: number;
completedDate?: number;
},
timeComponents: EnhancedStandardTaskMetadata["timeComponents"]
timeComponents: EnhancedStandardTaskMetadata["timeComponents"],
): EnhancedStandardTaskMetadata["enhancedDates"] {
if (!timeComponents) {
return undefined;
@ -924,7 +970,10 @@ export class FileTaskManagerImpl implements FileTaskManager {
const enhancedDates: EnhancedStandardTaskMetadata["enhancedDates"] = {};
// Helper function to combine date and time component
const combineDateTime = (dateTimestamp: number | undefined, timeComponent: TimeComponent | undefined): Date | undefined => {
const combineDateTime = (
dateTimestamp: number | undefined,
timeComponent: TimeComponent | undefined,
): Date | undefined => {
if (!dateTimestamp || !timeComponent) {
return undefined;
}
@ -936,7 +985,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
date.getDate(),
timeComponent.hour,
timeComponent.minute,
timeComponent.second || 0
timeComponent.second || 0,
);
return combinedDate;
@ -944,46 +993,80 @@ export class FileTaskManagerImpl implements FileTaskManager {
// Combine start date with start time
if (dates.startDate && timeComponents.startTime) {
enhancedDates.startDateTime = combineDateTime(dates.startDate, timeComponents.startTime);
enhancedDates.startDateTime = combineDateTime(
dates.startDate,
timeComponents.startTime,
);
}
// Fallback: If start date exists but no start time, and we have a due time (default context)
// but no due date, assume the time belongs to the start date.
// This handles cases like "🛫 2025-11-29 18:00" where the time defaults to "due" context
// but should actually be associated with the start date.
if (
dates.startDate &&
!timeComponents.startTime &&
!dates.dueDate &&
timeComponents.dueTime
) {
enhancedDates.startDateTime = combineDateTime(
dates.startDate,
timeComponents.dueTime,
);
}
// Fallback for start datetime when explicit start date is missing
if (
!enhancedDates.startDateTime &&
timeComponents.startTime
) {
if (!enhancedDates.startDateTime && timeComponents.startTime) {
const fallbackDate =
dates.dueDate ??
dates.scheduledDate ??
dates.completedDate;
dates.dueDate ?? dates.scheduledDate ?? dates.completedDate;
if (fallbackDate) {
enhancedDates.startDateTime = combineDateTime(
fallbackDate,
timeComponents.startTime
timeComponents.startTime,
);
}
}
// Combine due date with due time
if (dates.dueDate && timeComponents.dueTime) {
enhancedDates.dueDateTime = combineDateTime(dates.dueDate, timeComponents.dueTime);
enhancedDates.dueDateTime = combineDateTime(
dates.dueDate,
timeComponents.dueTime,
);
}
// Combine scheduled date with scheduled time
if (dates.scheduledDate && timeComponents.scheduledTime) {
enhancedDates.scheduledDateTime = combineDateTime(dates.scheduledDate, timeComponents.scheduledTime);
enhancedDates.scheduledDateTime = combineDateTime(
dates.scheduledDate,
timeComponents.scheduledTime,
);
}
// If we have a due date but the time component is scheduledTime (common with "at" keyword),
// create dueDateTime using scheduledTime
if (dates.dueDate && !timeComponents.dueTime && timeComponents.scheduledTime) {
enhancedDates.dueDateTime = combineDateTime(dates.dueDate, timeComponents.scheduledTime);
if (
dates.dueDate &&
!timeComponents.dueTime &&
timeComponents.scheduledTime
) {
enhancedDates.dueDateTime = combineDateTime(
dates.dueDate,
timeComponents.scheduledTime,
);
}
// If we have a scheduled date but the time component is dueTime,
// create scheduledDateTime using dueTime
if (dates.scheduledDate && !timeComponents.scheduledTime && timeComponents.dueTime) {
enhancedDates.scheduledDateTime = combineDateTime(dates.scheduledDate, timeComponents.dueTime);
if (
dates.scheduledDate &&
!timeComponents.scheduledTime &&
timeComponents.dueTime
) {
enhancedDates.scheduledDateTime = combineDateTime(
dates.scheduledDate,
timeComponents.dueTime,
);
}
// Handle end time - if we have start date and end time, create end datetime
@ -994,11 +1077,16 @@ export class FileTaskManagerImpl implements FileTaskManager {
dates.scheduledDate ??
dates.completedDate;
if (endBaseDate) {
enhancedDates.endDateTime = combineDateTime(endBaseDate, timeComponents.endTime);
enhancedDates.endDateTime = combineDateTime(
endBaseDate,
timeComponents.endTime,
);
}
}
return Object.keys(enhancedDates).length > 0 ? enhancedDates : undefined;
return Object.keys(enhancedDates).length > 0
? enhancedDates
: undefined;
}
/**
@ -1006,7 +1094,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
*/
public validatePropertyMapping(
entries: BasesEntry[],
mapping: FileTaskPropertyMapping = DEFAULT_FILE_TASK_MAPPING
mapping: FileTaskPropertyMapping = DEFAULT_FILE_TASK_MAPPING,
): void {
if (entries.length === 0) return;
@ -1033,7 +1121,7 @@ export class FileTaskManagerImpl implements FileTaskManager {
Object.entries(mapping).forEach(([key, propName]) => {
if (propName && !propertyUsage[propName]) {
console.warn(
`[FileTaskManager] Property "${propName}" (${key}) not found in any entries`
`[FileTaskManager] Property "${propName}" (${key}) not found in any entries`,
);
}
});

View file

@ -439,7 +439,17 @@ export class TaskSpecificView extends ItemView {
this.toggleTaskCompletion(task);
},
onEventContextMenu: (ev: MouseEvent, event: CalendarEvent) => {
this.handleTaskContextMenu(ev, event);
// Extract original task from metadata to ensure all fields (like filePath) are present
const realTask = (event as any)?.metadata
?.originalTask as Task;
if (realTask) {
this.handleTaskContextMenu(ev, realTask);
} else {
this.handleTaskContextMenu(
ev,
event as unknown as Task,
);
}
},
},
);

View file

@ -109,7 +109,10 @@ export class TaskView extends ItemView {
this.applyCurrentFilter();
}, 400); // 增加延迟到 400ms 减少频繁更新
constructor(leaf: WorkspaceLeaf, private plugin: TaskProgressBarPlugin) {
constructor(
leaf: WorkspaceLeaf,
private plugin: TaskProgressBarPlugin,
) {
super(leaf);
this.tasks = this.plugin.preloadedTasks || [];
@ -137,7 +140,7 @@ export class TaskView extends ItemView {
getDisplayText(): string {
const currentViewConfig = getViewSettingOrDefault(
this.plugin,
this.currentViewId
this.currentViewId,
);
return currentViewConfig.name;
}
@ -145,7 +148,7 @@ export class TaskView extends ItemView {
getIcon(): string {
const currentViewConfig = getViewSettingOrDefault(
this.plugin,
this.currentViewId
this.currentViewId,
);
return currentViewConfig.icon;
}
@ -168,23 +171,22 @@ export class TaskView extends ItemView {
isDataflowEnabled(this.plugin) &&
this.plugin.dataflowOrchestrator
) {
this.registerEvent(
on(this.app, Events.CACHE_READY, async () => {
// 冷启动就绪,从快照加载,并更新视图
await this.loadTasksFast(false);
})
}),
);
this.registerEvent(
on(this.app, Events.TASK_CACHE_UPDATED, debouncedViewUpdate)
on(this.app, Events.TASK_CACHE_UPDATED, debouncedViewUpdate),
);
} else {
// Legacy: 兼容旧事件
this.registerEvent(
this.app.workspace.on(
"task-genius:task-cache-updated",
debouncedViewUpdate
)
debouncedViewUpdate,
),
);
}
@ -213,8 +215,8 @@ export class TaskView extends ItemView {
// 使用防抖函数应用过滤器,避免频繁更新
this.debouncedApplyFilter();
}
)
},
),
);
// 监听视图配置变更事件(仅刷新侧边栏与当前视图可见性)
@ -227,14 +229,14 @@ export class TaskView extends ItemView {
if (
this.sidebarComponent &&
typeof this.sidebarComponent.renderSidebarItems ===
"function"
"function"
) {
this.sidebarComponent.renderSidebarItems();
}
} catch (e) {
console.warn(
"Failed to render sidebar items on view-config-changed:",
e
e,
);
}
@ -249,12 +251,12 @@ export class TaskView extends ItemView {
// 若当前视图被设为不可见,则切换到第一个可见视图(不强制刷新内容)
const currentCfg =
this.plugin.settings.viewConfiguration.find(
(v) => v.id === this.currentViewId
(v) => v.id === this.currentViewId,
);
if (!currentCfg?.visible) {
const firstVisible =
this.plugin.settings.viewConfiguration.find(
(v) => v.visible
(v) => v.visible,
)?.id as ViewMode | undefined;
if (
firstVisible &&
@ -262,23 +264,23 @@ export class TaskView extends ItemView {
) {
this.currentViewId = firstVisible;
this.sidebarComponent?.setViewMode(
this.currentViewId
this.currentViewId,
);
// Ensure main content switches to the new visible view
this.switchView(
this.currentViewId,
undefined,
true
true,
);
}
}
}
)
},
),
);
// 2. 加载缓存的实时过滤状态
const savedFilterState = this.app.loadLocalStorage(
"task-genius-view-filter"
"task-genius-view-filter",
) as RootFilterState;
console.log("savedFilterState", savedFilterState);
@ -301,14 +303,14 @@ export class TaskView extends ItemView {
// 4. 获取初始视图ID
const savedViewId = this.app.loadLocalStorage(
"task-genius:view-mode"
"task-genius:view-mode",
) as ViewMode;
const initialViewId = this.plugin.settings.viewConfiguration.find(
(v) => v.id === savedViewId && v.visible
(v) => v.id === savedViewId && v.visible,
)
? savedViewId
: this.plugin.settings.viewConfiguration.find((v) => v.visible)
?.id || "inbox";
?.id || "inbox";
this.currentViewId = initialViewId;
this.sidebarComponent.setViewMode(this.currentViewId);
@ -322,7 +324,7 @@ export class TaskView extends ItemView {
} else {
// If no tasks loaded yet, wait for background sync before rendering
console.log(
"No cached tasks found, waiting for background sync..."
"No cached tasks found, waiting for background sync...",
);
await this.loadTasksWithSyncInBackground();
this.switchView(this.currentViewId);
@ -345,7 +347,7 @@ export class TaskView extends ItemView {
(this.leaf.tabHeaderEl as HTMLElement).toggleClass(
"task-genius-tab-header",
true
true,
);
this.tabActionButton = (
@ -364,11 +366,11 @@ export class TaskView extends ItemView {
this.plugin.app,
this.plugin,
{},
true
true,
);
modal.open();
});
}
},
);
this.register(() => {
@ -389,7 +391,7 @@ export class TaskView extends ItemView {
const existingLeaves =
this.plugin.app.workspace.getLeavesOfType(
TASK_VIEW_TYPE
TASK_VIEW_TYPE,
);
if (existingLeaves.length > 0) {
// Focus the existing view
@ -401,7 +403,7 @@ export class TaskView extends ItemView {
this.plugin.activateTaskView().then(() => {
const newView =
this.plugin.app.workspace.getActiveViewOfType(
TaskView
TaskView,
);
if (newView) {
newView.switchView(view.id);
@ -437,7 +439,7 @@ export class TaskView extends ItemView {
private initializeComponents() {
this.sidebarComponent = new SidebarComponent(
this.rootContainerEl,
this.plugin
this.plugin,
);
this.addChild(this.sidebarComponent);
this.sidebarComponent.load();
@ -460,14 +462,14 @@ export class TaskView extends ItemView {
console.log(
"TaskView onTaskUpdate",
originalTask.content,
updatedTask.content
updatedTask.content,
);
await this.handleTaskUpdate(originalTask, updatedTask);
},
onTaskContextMenu: (event: MouseEvent, task: Task) => {
this.handleTaskContextMenu(event, task);
},
}
},
);
this.addChild(this.contentComponent);
this.contentComponent.load();
@ -487,14 +489,14 @@ export class TaskView extends ItemView {
console.log(
"TaskView onTaskUpdate",
originalTask.content,
updatedTask.content
updatedTask.content,
);
await this.handleTaskUpdate(originalTask, updatedTask);
},
onTaskContextMenu: (event: MouseEvent, task: Task) => {
this.handleTaskContextMenu(event, task);
},
}
},
);
this.addChild(this.forecastComponent);
this.forecastComponent.load();
@ -517,7 +519,7 @@ export class TaskView extends ItemView {
onTaskContextMenu: (event: MouseEvent, task: Task) => {
this.handleTaskContextMenu(event, task);
},
}
},
);
this.addChild(this.tagsComponent);
this.tagsComponent.load();
@ -540,7 +542,7 @@ export class TaskView extends ItemView {
onTaskContextMenu: (event: MouseEvent, task: Task) => {
this.handleTaskContextMenu(event, task);
},
}
},
);
this.addChild(this.projectsComponent);
this.projectsComponent.load();
@ -563,7 +565,7 @@ export class TaskView extends ItemView {
onTaskContextMenu: (event: MouseEvent, task: Task) => {
this.handleTaskContextMenu(event, task);
},
}
},
);
this.addChild(this.reviewComponent);
this.reviewComponent.load();
@ -582,9 +584,19 @@ export class TaskView extends ItemView {
this.toggleTaskCompletion(task);
},
onEventContextMenu: (ev: MouseEvent, event: CalendarEvent) => {
this.handleTaskContextMenu(ev, event);
// Extract original task from metadata to ensure all fields (like filePath) are present
const realTask = (event as any)?.metadata
?.originalTask as Task;
if (realTask) {
this.handleTaskContextMenu(ev, realTask);
} else {
this.handleTaskContextMenu(
ev,
event as unknown as Task,
);
}
},
}
},
);
this.addChild(this.calendarComponent);
this.calendarComponent.load();
@ -602,7 +614,7 @@ export class TaskView extends ItemView {
onTaskSelected: this.handleTaskSelection.bind(this),
onTaskCompleted: this.toggleTaskCompletion.bind(this),
onTaskContextMenu: this.handleTaskContextMenu.bind(this),
}
},
);
this.addChild(this.kanbanComponent);
this.kanbanComponent.containerEl.hide();
@ -614,7 +626,7 @@ export class TaskView extends ItemView {
onTaskSelected: this.handleTaskSelection.bind(this),
onTaskCompleted: this.toggleTaskCompletion.bind(this),
onTaskContextMenu: this.handleTaskContextMenu.bind(this),
}
},
);
this.addChild(this.ganttComponent);
this.ganttComponent.containerEl.hide();
@ -626,7 +638,7 @@ export class TaskView extends ItemView {
this.detailsComponent = new TaskDetailsComponent(
this.rootContainerEl,
this.app,
this.plugin
this.plugin,
);
this.addChild(this.detailsComponent);
this.detailsComponent.load();
@ -645,7 +657,7 @@ export class TaskView extends ItemView {
this.handleKanbanTaskStatusUpdate.bind(this),
onEventContextMenu: this.handleTaskContextMenu.bind(this),
onTaskUpdate: this.handleTaskUpdate.bind(this),
}
},
);
this.addChild(this.viewComponentManager);
@ -662,7 +674,7 @@ export class TaskView extends ItemView {
if (!toggleContainer) {
console.error(
"Could not find .view-header-nav-buttons to add sidebar toggle."
"Could not find .view-header-nav-buttons to add sidebar toggle.",
);
return;
}
@ -681,7 +693,7 @@ export class TaskView extends ItemView {
private createTaskMark() {
// Check if task-mark feature is hidden in current workspace
if (this.plugin.workspaceManager?.isFeatureHidden('task-mark')) {
if (this.plugin.workspaceManager?.isFeatureHidden("task-mark")) {
return;
}
@ -690,40 +702,43 @@ export class TaskView extends ItemView {
interpolation: {
num: this.tasks.length,
},
})
}),
);
}
private createActionButtons() {
// Check if details-panel feature is hidden
if (!this.plugin.workspaceManager?.isFeatureHidden('details-panel')) {
if (!this.plugin.workspaceManager?.isFeatureHidden("details-panel")) {
this.detailsToggleBtn = this.addAction(
"panel-right-dashed",
t("Details"),
() => {
this.toggleDetailsVisibility(!this.isDetailsVisible);
}
},
);
this.detailsToggleBtn.toggleClass("panel-toggle-btn", true);
this.detailsToggleBtn.toggleClass("is-active", this.isDetailsVisible);
this.detailsToggleBtn.toggleClass(
"is-active",
this.isDetailsVisible,
);
}
// Check if quick-capture feature is hidden
if (!this.plugin.workspaceManager?.isFeatureHidden('quick-capture')) {
if (!this.plugin.workspaceManager?.isFeatureHidden("quick-capture")) {
this.addAction("notebook-pen", t("Capture"), () => {
const modal = new QuickCaptureModal(
this.plugin.app,
this.plugin,
{},
true
true,
);
modal.open();
});
}
// Check if filter feature is hidden
if (this.plugin.workspaceManager?.isFeatureHidden('filter')) {
if (this.plugin.workspaceManager?.isFeatureHidden("filter")) {
// Skip filter button creation
this.updateActionButtons();
return;
@ -734,7 +749,7 @@ export class TaskView extends ItemView {
const popover = new ViewTaskFilterPopover(
this.plugin.app,
undefined,
this.plugin
this.plugin,
);
// 设置关闭回调 - 现在主要用于处理取消操作
@ -754,18 +769,18 @@ export class TaskView extends ItemView {
const filterState = this
.liveFilterState as RootFilterState;
popover.taskFilterComponent.loadFilterState(
filterState
filterState,
);
}
}, 100);
});
popover.showAtPosition({x: e.clientX, y: e.clientY});
popover.showAtPosition({ x: e.clientX, y: e.clientY });
} else {
const modal = new ViewTaskFilterModal(
this.plugin.app,
this.leaf.id,
this.plugin
this.plugin,
);
// 设置关闭回调 - 现在主要用于处理取消操作
@ -797,7 +812,7 @@ export class TaskView extends ItemView {
console.log(
"应用当前过滤状态:",
this.liveFilterState ? "有实时筛选器" : "无实时筛选器",
this.currentFilterState ? "有过滤器" : "无过滤器"
this.currentFilterState ? "有过滤器" : "无过滤器",
);
// 通过triggerViewUpdate重新加载任务
this.triggerViewUpdate();
@ -838,7 +853,7 @@ export class TaskView extends ItemView {
undefined,
(config) => {
this.applySavedFilter(config);
}
},
);
modal.open();
});
@ -880,7 +895,7 @@ export class TaskView extends ItemView {
new ConfirmModal(this.plugin, {
title: t("Reindex"),
message: t(
"Are you sure you want to force reindex all tasks?"
"Are you sure you want to force reindex all tasks?",
),
confirmText: t("Reindex"),
cancelText: t("Cancel"),
@ -891,13 +906,13 @@ export class TaskView extends ItemView {
await this.plugin.dataflowOrchestrator.rebuild();
} else {
throw new Error(
"Dataflow orchestrator not available"
"Dataflow orchestrator not available",
);
}
} catch (error) {
console.error(
"Failed to force reindex tasks:",
error
error,
);
new Notice(t("Failed to force reindex tasks"));
}
@ -913,7 +928,7 @@ export class TaskView extends ItemView {
this.isSidebarCollapsed = !this.isSidebarCollapsed;
this.rootContainerEl.toggleClass(
"sidebar-collapsed",
this.isSidebarCollapsed
this.isSidebarCollapsed,
);
this.sidebarComponent.setCollapsed(this.isSidebarCollapsed);
@ -929,7 +944,7 @@ export class TaskView extends ItemView {
this.detailsToggleBtn.toggleClass("is-active", visible);
this.detailsToggleBtn.setAttribute(
"aria-label",
visible ? t("Hide Details") : t("Show Details")
visible ? t("Hide Details") : t("Show Details"),
);
}
@ -946,12 +961,12 @@ export class TaskView extends ItemView {
this.detailsComponent.onTaskEdit = (task: Task) => this.editTask(task);
this.detailsComponent.onTaskUpdate = async (
originalTask: Task,
updatedTask: Task
updatedTask: Task,
) => {
console.log(
"triggered by detailsComponent",
originalTask,
updatedTask
updatedTask,
);
await this.updateTask(originalTask, updatedTask);
};
@ -971,7 +986,7 @@ export class TaskView extends ItemView {
private switchView(
viewId: ViewMode,
project?: string | null,
forceRefresh: boolean = false
forceRefresh: boolean = false,
) {
// Exit selection mode when switching views
if (this.selectionManager.isSelectionMode) {
@ -985,7 +1000,7 @@ export class TaskView extends ItemView {
"Project:",
project,
"ForceRefresh:",
forceRefresh
forceRefresh,
);
// Update sidebar to reflect current view
@ -1026,7 +1041,7 @@ export class TaskView extends ItemView {
this.app,
this.plugin,
twoColumnConfig,
viewId
viewId,
);
this.addChild(twoColumnComponent);
@ -1088,7 +1103,7 @@ export class TaskView extends ItemView {
if (targetComponent) {
console.log(
`Activating component for view ${viewId}`,
targetComponent.constructor.name
targetComponent.constructor.name,
);
targetComponent.containerEl.show();
if (typeof targetComponent.setTasks === "function") {
@ -1112,13 +1127,13 @@ export class TaskView extends ItemView {
this.tasks,
viewId,
this.plugin,
filterOptions
filterOptions,
);
// Filter out badge tasks for forecast view - they should only appear in event view
if (viewId === "forecast") {
filteredTasks = filteredTasks.filter(
(task) => !(task as any).badge
(task) => !(task as any).badge,
);
}
@ -1126,12 +1141,12 @@ export class TaskView extends ItemView {
"[TaskView] Calling setTasks with",
filteredTasks.length,
"filtered tasks, forceRefresh:",
forceRefresh
forceRefresh,
);
targetComponent.setTasks(
filteredTasks,
this.tasks,
forceRefresh
forceRefresh,
);
}
@ -1151,13 +1166,13 @@ export class TaskView extends ItemView {
}
targetComponent.updateTasks(
filterTasks(this.tasks, viewId, this.plugin, filterOptions)
filterTasks(this.tasks, viewId, this.plugin, filterOptions),
);
}
if (typeof targetComponent.setViewMode === "function") {
console.log(
`Setting view mode for ${viewId} to ${modeForComponent} with project ${project}`
`Setting view mode for ${viewId} to ${modeForComponent} with project ${project}`,
);
targetComponent.setViewMode(modeForComponent, project);
}
@ -1184,13 +1199,13 @@ export class TaskView extends ItemView {
this.tasks,
component.getViewId(),
this.plugin,
filterOptions
filterOptions,
);
// Filter out badge tasks for forecast view - they should only appear in event view
if (component.getViewId() === "forecast") {
filteredTasks = filteredTasks.filter(
(task) => !(task as any).badge
(task) => !(task as any).badge,
);
}
@ -1215,7 +1230,7 @@ export class TaskView extends ItemView {
if (this.currentSelectedTaskId) {
// Re-select the current task to maintain details panel visibility
const currentTask = this.tasks.find(
(t) => t.id === this.currentSelectedTaskId
(t) => t.id === this.currentSelectedTaskId,
);
if (currentTask) {
this.detailsComponent.showTaskDetails(currentTask);
@ -1234,7 +1249,7 @@ export class TaskView extends ItemView {
private updateHeaderDisplay() {
const config = getViewSettingOrDefault(this.plugin, this.currentViewId);
this.leaf.setEphemeralState({title: config.name, icon: config.icon});
this.leaf.setEphemeralState({ title: config.name, icon: config.icon });
}
private handleTaskContextMenu(event: MouseEvent, task: Task) {
@ -1277,7 +1292,7 @@ export class TaskView extends ItemView {
},
(el) => {
createTaskCheckbox(mark, task, el);
}
},
);
item.titleEl.createEl("span", {
cls: "status-option",
@ -1357,28 +1372,28 @@ export class TaskView extends ItemView {
private async loadTasks(
forceSync: boolean = false,
skipViewUpdate: boolean = false
skipViewUpdate: boolean = false,
) {
// Only use dataflow - TaskManager is deprecated
if (!this.plugin.dataflowOrchestrator) {
console.warn(
"[TaskView] Dataflow orchestrator not available, waiting for initialization..."
"[TaskView] Dataflow orchestrator not available, waiting for initialization...",
);
this.tasks = [];
} else {
try {
console.log(
"[TaskView] Loading tasks from dataflow orchestrator..."
"[TaskView] Loading tasks from dataflow orchestrator...",
);
const queryAPI = this.plugin.dataflowOrchestrator.getQueryAPI();
this.tasks = await queryAPI.getAllTasks();
console.log(
`[TaskView] Loaded ${this.tasks.length} tasks from dataflow`
`[TaskView] Loaded ${this.tasks.length} tasks from dataflow`,
);
} catch (error) {
console.error(
"[TaskView] Error loading tasks from dataflow:",
error
error,
);
this.tasks = [];
}
@ -1396,24 +1411,24 @@ export class TaskView extends ItemView {
// Only use dataflow
if (!this.plugin.dataflowOrchestrator) {
console.warn(
"[TaskView] Dataflow orchestrator not available for fast load"
"[TaskView] Dataflow orchestrator not available for fast load",
);
this.tasks = [];
} else {
try {
console.log(
"[TaskView] Loading tasks fast from dataflow orchestrator..."
"[TaskView] Loading tasks fast from dataflow orchestrator...",
);
const queryAPI = this.plugin.dataflowOrchestrator.getQueryAPI();
// For fast loading, use regular getAllTasks (it should be cached)
this.tasks = await queryAPI.getAllTasks();
console.log(
`[TaskView] Loaded ${this.tasks.length} tasks (fast from dataflow)`
`[TaskView] Loaded ${this.tasks.length} tasks (fast from dataflow)`,
);
} catch (error) {
console.error(
"[TaskView] Error loading tasks fast from dataflow:",
error
error,
);
this.tasks = [];
}
@ -1439,7 +1454,7 @@ export class TaskView extends ItemView {
if (tasks.length !== this.tasks.length || tasks.length === 0) {
this.tasks = tasks;
console.log(
`TaskView updated with ${this.tasks.length} tasks (dataflow sync)`
`TaskView updated with ${this.tasks.length} tasks (dataflow sync)`,
);
// Don't trigger view update here as it will be handled by events
}
@ -1463,11 +1478,11 @@ export class TaskView extends ItemView {
// 如果当前视图已被设置为隐藏,则切换到第一个可见视图
const currentCfg = this.plugin.settings.viewConfiguration.find(
(v) => v.id === this.currentViewId
(v) => v.id === this.currentViewId,
);
if (!currentCfg?.visible) {
const firstVisible = this.plugin.settings.viewConfiguration.find(
(v) => v.visible
(v) => v.visible,
)?.id as ViewMode | undefined;
if (firstVisible && firstVisible !== this.currentViewId) {
this.currentViewId = firstVisible;
@ -1485,7 +1500,7 @@ export class TaskView extends ItemView {
private updateActionButtons() {
// 移除过滤器重置按钮(如果存在)
const resetButton = this.leaf.view.containerEl.querySelector(
".view-action.task-filter-reset"
".view-action.task-filter-reset",
);
if (resetButton) {
resetButton.remove();
@ -1508,7 +1523,7 @@ export class TaskView extends ItemView {
try {
const lower = mark.toLowerCase();
const completedCfg = String(
this.plugin.settings.taskStatuses?.completed || "x"
this.plugin.settings.taskStatuses?.completed || "x",
);
const completedSet = completedCfg
.split("|")
@ -1530,13 +1545,12 @@ export class TaskView extends ItemView {
}
}
}
} catch (_) {
}
} catch (_) {}
return false;
}
private async toggleTaskCompletion(task: Task) {
const updatedTask = {...task, completed: !task.completed};
const updatedTask = { ...task, completed: !task.completed };
if (updatedTask.completed) {
updatedTask.metadata.completedDate = Date.now();
@ -1564,7 +1578,7 @@ export class TaskView extends ItemView {
*/
private extractChangedFields(
originalTask: Task,
updatedTask: Task
updatedTask: Task,
): Partial<Task> {
const changes: Partial<Task> = {};
@ -1637,14 +1651,14 @@ export class TaskView extends ItemView {
originalTask.id,
updatedTask.id,
updatedTask,
originalTask
originalTask,
);
try {
// Extract only the changed fields
const updates = this.extractChangedFields(
originalTask,
updatedTask
updatedTask,
);
console.log("Extracted changes:", updates);
@ -1663,7 +1677,7 @@ export class TaskView extends ItemView {
}
console.log(
`Task ${updatedTask.id} updated successfully via handleTaskUpdate.`
`Task ${updatedTask.id} updated successfully via handleTaskUpdate.`,
);
// Update local task list immediately
@ -1674,7 +1688,7 @@ export class TaskView extends ItemView {
this.tasks[index] = updatedTask;
} else {
console.warn(
"Updated task not found in local list, might reload."
"Updated task not found in local list, might reload.",
);
}
@ -1701,7 +1715,7 @@ export class TaskView extends ItemView {
private async updateTask(
originalTask: Task,
updatedTask: Task
updatedTask: Task,
): Promise<Task> {
if (!this.plugin.writeAPI) {
console.error("WriteAPI not available for updateTask");
@ -1711,7 +1725,7 @@ export class TaskView extends ItemView {
// Extract only the changed fields
const updates = this.extractChangedFields(
originalTask,
updatedTask
updatedTask,
);
console.log("Extracted changes:", updates);
@ -1737,7 +1751,7 @@ export class TaskView extends ItemView {
this.tasks[index] = updatedTask;
} else {
console.warn(
"Updated task not found in local list, might reload."
"Updated task not found in local list, might reload.",
);
}
@ -1853,7 +1867,7 @@ export class TaskView extends ItemView {
if (deleteChildren && task.metadata?.children) {
for (const childId of task.metadata.children) {
const childIndex = this.tasks.findIndex(
(t) => t.id === childId
(t) => t.id === childId,
);
if (childIndex !== -1) {
this.tasks.splice(childIndex, 1);
@ -1872,8 +1886,8 @@ export class TaskView extends ItemView {
} else {
new Notice(
t("Failed to delete task") +
": " +
(result.error || "Unknown error")
": " +
(result.error || "Unknown error"),
);
}
} catch (error) {
@ -1903,18 +1917,18 @@ export class TaskView extends ItemView {
this.sidebarComponent.renderSidebarItems();
} else {
console.warn(
"TaskView: SidebarComponent does not have renderSidebarItems method."
"TaskView: SidebarComponent does not have renderSidebarItems method.",
);
}
// 检查当前视图的类型是否发生变化(比如从两列切换到单列)
const currentViewConfig = this.plugin.settings.viewConfiguration.find(
(v) => v.id === this.currentViewId
(v) => v.id === this.currentViewId,
);
// 如果当前是两列视图但配置已改为非两列,需要销毁两列组件
const currentTwoColumn = this.twoColumnViewComponents.get(
this.currentViewId
this.currentViewId,
);
if (
currentTwoColumn &&
@ -1933,10 +1947,10 @@ export class TaskView extends ItemView {
// Method to handle status updates originating from Kanban drag-and-drop
private handleKanbanTaskStatusUpdate = async (
taskId: string,
newStatusMark: string
newStatusMark: string,
) => {
console.log(
`TaskView handling Kanban status update request for ${taskId} to mark ${newStatusMark}`
`TaskView handling Kanban status update request for ${taskId} to mark ${newStatusMark}`,
);
const taskToUpdate = this.tasks.find((t) => t.id === taskId);
@ -1959,22 +1973,22 @@ export class TaskView extends ItemView {
},
});
console.log(
`Task ${taskId} status update processed by TaskView.`
`Task ${taskId} status update processed by TaskView.`,
);
} catch (error) {
console.error(
`TaskView failed to update task status from Kanban callback for task ${taskId}:`,
error
error,
);
}
} else {
console.log(
`Task ${taskId} status (${newStatusMark}) already matches, no update needed.`
`Task ${taskId} status (${newStatusMark}) already matches, no update needed.`,
);
}
} else {
console.warn(
`TaskView could not find task with ID ${taskId} for Kanban status update.`
`TaskView could not find task with ID ${taskId} for Kanban status update.`,
);
}
};
@ -1994,12 +2008,12 @@ export class TaskView extends ItemView {
console.log("应用保存的筛选器:", config.name);
this.liveFilterState = JSON.parse(JSON.stringify(config.filterState));
this.currentFilterState = JSON.parse(
JSON.stringify(config.filterState)
JSON.stringify(config.filterState),
);
console.log("applySavedFilter", this.liveFilterState);
this.app.saveLocalStorage(
"task-genius-view-filter",
this.liveFilterState
this.liveFilterState,
);
this.applyCurrentFilter();
this.updateActionButtons();

View file

@ -1135,7 +1135,19 @@ export class TaskBasesView extends BasesView {
onEventContextMenu: (
ev: MouseEvent,
event: CalendarEvent,
) => this.handleTaskContextMenu(ev, event),
) => {
// Extract original task from metadata to ensure all fields (like filePath) are present
const realTask = (event as any)?.metadata
?.originalTask as Task;
if (realTask) {
this.handleTaskContextMenu(ev, realTask);
} else {
this.handleTaskContextMenu(
ev,
event as unknown as Task,
);
}
},
},
);
this.addChild(calendarComp);

View file

@ -482,22 +482,28 @@ export class TimeParsingService {
expression: string,
index: number,
): "start" | "due" | "scheduled" {
// Get text before the expression (look back up to 20 characters)
const beforeText = text
.substring(Math.max(0, index - 20), index)
.toLowerCase();
// Get text before the expression (look back up to 200 characters to reliably capture emoji + date patterns)
// 30 chars was insufficient for cases like "Task 🛫 2025-11-29 18:00" where emoji takes 2 chars
const beforeTextRaw = text.substring(Math.max(0, index - 200), index);
const beforeText = beforeTextRaw.toLowerCase();
// Get text after the expression (look ahead up to 20 characters)
const afterText = text
.substring(
index + expression.length,
Math.min(text.length, index + expression.length + 20),
)
.toLowerCase();
const afterTextRaw = text.substring(
index + expression.length,
Math.min(text.length, index + expression.length + 20),
);
const afterText = afterTextRaw.toLowerCase();
// Combine surrounding context
const context = beforeText + " " + afterText;
// Check for explicit emoji indicators (highest priority)
// Use raw text (not lowercased) for emoji detection
// These emojis are commonly used in task management to denote date types
if (beforeTextRaw.includes("🛫")) return "start";
if (beforeTextRaw.includes("📅")) return "due";
if (beforeTextRaw.includes("⏳")) return "scheduled";
// Check for start keywords first (most specific)
for (const keyword of this.config.dateKeywords.start) {
if (context.includes(keyword.toLowerCase())) {
@ -981,22 +987,28 @@ export class TimeParsingService {
expression: string,
index: number,
): "start" | "due" | "scheduled" {
// Get text before the expression (look back up to 20 characters)
const beforeText = text
.substring(Math.max(0, index - 20), index)
.toLowerCase();
// Get text before the expression (look back up to 200 characters to reliably capture emoji + date patterns)
// 30 chars was insufficient for cases like "Task 🛫 2025-11-29 18:00" where emoji takes 2 chars
const beforeTextRaw = text.substring(Math.max(0, index - 200), index);
const beforeText = beforeTextRaw.toLowerCase();
// Get text after the expression (look ahead up to 20 characters)
const afterText = text
.substring(
index + expression.length,
Math.min(text.length, index + expression.length + 20),
)
.toLowerCase();
const afterTextRaw = text.substring(
index + expression.length,
Math.min(text.length, index + expression.length + 20),
);
const afterText = afterTextRaw.toLowerCase();
// Combine surrounding context
const context = beforeText + " " + afterText;
// Check for explicit emoji indicators (highest priority)
// Use raw text (not lowercased) for emoji detection
// These emojis are commonly used in task management to denote date types
if (beforeTextRaw.includes("🛫")) return "start";
if (beforeTextRaw.includes("📅")) return "due";
if (beforeTextRaw.includes("⏳")) return "scheduled";
// Check for start keywords
for (const keyword of this.config.dateKeywords.start) {
if (context.includes(keyword.toLowerCase())) {

View file

@ -14,7 +14,7 @@
justify-content: space-between;
align-items: center;
padding: var(--size-2-3) var(--size-4-4); /* Use Obsidian variables */
border-bottom: 1px solid var(--background-modifier-border);
/*border-bottom: 1px solid var(--background-modifier-border);*/
flex-shrink: 0; /* Prevent header from shrinking */
margin-bottom: 0;
}

File diff suppressed because it is too large Load diff

View file

@ -102,7 +102,78 @@ export function tasksToCalendarEvents(tasks: Task[]): CalendarEvent[] {
* @returns Object with start and optional end timestamp
*/
function calculateTaskDateRange(task: Task): TaskDateRange {
const { startDate, dueDate, scheduledDate } = task.metadata;
const enhancedDates = (task.metadata as any).enhancedDates;
// Use enhanced dates (with time) if available, otherwise fall back to standard dates
const startDate = enhancedDates?.startDateTime
? enhancedDates.startDateTime.getTime()
: task.metadata.startDate;
const dueDate = enhancedDates?.dueDateTime
? enhancedDates.dueDateTime.getTime()
: task.metadata.dueDate;
const scheduledDate = enhancedDates?.scheduledDateTime
? enhancedDates.scheduledDateTime.getTime()
: task.metadata.scheduledDate;
// Default duration for timed events without explicit end time
const DEFAULT_DURATION_MS = 30 * 60 * 1000; // 30 minutes
// Special case: explicit start and end time (e.g. from time range parsing like "10:00-12:00")
// This handles cases where start and end are explicitly defined in the enhanced metadata
if (enhancedDates?.startDateTime && enhancedDates?.endDateTime) {
return {
start: enhancedDates.startDateTime.getTime(),
end: enhancedDates.endDateTime.getTime(),
};
}
// Handle single datetime with time info: add default 30-minute duration
// This ensures tasks with only start time (e.g., "🛫 2025-11-29 23:35") still show on calendar
if (enhancedDates?.startDateTime && !enhancedDates?.endDateTime) {
const startTime = enhancedDates.startDateTime.getTime();
// If there's a due date, use it as end; otherwise add 30 minutes
if (dueDate && dueDate > startTime) {
return { start: startTime, end: dueDate };
}
return { start: startTime, end: startTime + DEFAULT_DURATION_MS };
}
if (enhancedDates?.dueDateTime && !enhancedDates?.startDateTime) {
const dueTime = enhancedDates.dueDateTime.getTime();
// If there's a start date, use it; otherwise subtract 30 minutes
if (startDate && startDate < dueTime) {
return { start: startDate, end: dueTime };
}
return { start: dueTime - DEFAULT_DURATION_MS, end: dueTime };
}
if (
enhancedDates?.scheduledDateTime &&
!enhancedDates?.startDateTime &&
!enhancedDates?.dueDateTime
) {
const scheduledTime = enhancedDates.scheduledDateTime.getTime();
return {
start: scheduledTime,
end: scheduledTime + DEFAULT_DURATION_MS,
};
}
// FALLBACK: Handle cases where date was parsed with time directly into metadata
// (without creating enhancedDates). Check if the timestamp has time component.
// This happens when emoji parser extracts "🛫 2025-11-29 18:00" directly.
if (startDate && !dueDate && !scheduledDate && !isDateOnly(startDate)) {
return { start: startDate, end: startDate + DEFAULT_DURATION_MS };
}
if (dueDate && !startDate && !scheduledDate && !isDateOnly(dueDate)) {
return { start: dueDate - DEFAULT_DURATION_MS, end: dueDate };
}
if (scheduledDate && !startDate && !dueDate && !isDateOnly(scheduledDate)) {
return {
start: scheduledDate,
end: scheduledDate + DEFAULT_DURATION_MS,
};
}
// Priority 1: startDate → dueDate (multi-day span)
if (startDate && dueDate && startDate !== dueDate && startDate < dueDate) {
@ -188,6 +259,7 @@ function formatDateForCalendar(timestamp: number, isAllDay: boolean): string {
if (isAllDay) {
return format(date, "yyyy-MM-dd");
}
// Use space separator for calendar adapter compatibility (NativeDateAdapter/DateFnsAdapter)
return format(date, "yyyy-MM-dd HH:mm");
}

View file

@ -38,7 +38,7 @@ export function formatDate(date: Date): string {
*/
export function parseLocalDate(
dateString: string,
customFormats?: string[]
customFormats?: string[],
): number | undefined {
if (!dateString) return undefined;
@ -50,8 +50,26 @@ export function parseLocalDate(
return undefined;
}
// Check if the date string contains time information
const hasTimeInfo = /\d{1,2}:\d{2}/.test(dateString);
// Define default format patterns to try with date-fns
const defaultFormats = [
// Date-time formats (with time) should be tried first when time info is present
const dateTimeFormats = [
"yyyy-MM-dd HH:mm", // ISO format with time
"yyyy-MM-dd H:mm", // ISO format with single-digit hour
"yyyy/MM/dd HH:mm", // YYYY/MM/DD with time
"yyyy/MM/dd H:mm",
"dd-MM-yyyy HH:mm", // DD-MM-YYYY with time
"dd/MM/yyyy HH:mm", // DD/MM/YYYY with time
"MM-dd-yyyy HH:mm", // MM-DD-YYYY with time
"MM/dd/yyyy HH:mm", // MM/DD/YYYY with time
"yyyy.MM.dd HH:mm", // YYYY.MM.DD with time
"yyyyMMddHHmmss",
"yyyyMMdd_HHmmss",
];
const dateOnlyFormats = [
"yyyy-MM-dd", // ISO format
"yyyy/MM/dd", // YYYY/MM/DD
"dd-MM-yyyy", // DD-MM-YYYY
@ -65,10 +83,13 @@ export function parseLocalDate(
"MMM dd, yyyy", // MMM DD, YYYY with leading zero
"d MMM yyyy", // DD MMM YYYY (e.g., 15 Jan 2025)
"dd MMM yyyy", // DD MMM YYYY with leading zero
"yyyyMMddHHmmss",
"yyyyMMdd_HHmmss",
];
// Try date-time formats first if time info is present, otherwise try date-only formats
const defaultFormats = hasTimeInfo
? [...dateTimeFormats, ...dateOnlyFormats]
: [...dateOnlyFormats, ...dateTimeFormats];
// Combine custom formats with default formats
const allFormats = customFormats
? [...customFormats, ...defaultFormats]
@ -83,9 +104,15 @@ export function parseLocalDate(
// Check if the parsed date is valid
if (isValid(parsedDate)) {
// Set to start of day to match original behavior
const normalizedDate = startOfDay(parsedDate);
return normalizedDate.getTime();
// Only normalize to start of day if the original string had no time info
// and the format pattern also has no time component
const formatHasTime = /[Hh]:mm/.test(formatString);
if (!hasTimeInfo && !formatHasTime) {
const normalizedDate = startOfDay(parsedDate);
return normalizedDate.getTime();
}
// Preserve the time information
return parsedDate.getTime();
}
} catch (e) {
// Silently continue to next format
@ -97,8 +124,12 @@ export function parseLocalDate(
try {
const isoDate = parseISO(dateString);
if (isValid(isoDate)) {
const normalizedDate = startOfDay(isoDate);
return normalizedDate.getTime();
// Only normalize to start of day if the original string had no time info
if (!hasTimeInfo) {
const normalizedDate = startOfDay(isoDate);
return normalizedDate.getTime();
}
return isoDate.getTime();
}
} catch (e) {
// Silently continue
@ -144,7 +175,7 @@ export function getLocalDateString(date: Date): string {
*/
export function getRelativeTimeString(
date: Date | number,
lang = navigator.language
lang = navigator.language,
): string {
// 允许传入日期对象或时间戳
const timeMs = typeof date === "number" ? date : date.getTime();
@ -159,7 +190,7 @@ export function getRelativeTimeString(
// 计算日期差(以天为单位)
const deltaDays = Math.round(
(targetDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)
(targetDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24),
);
// 创建相对时间格式化器

File diff suppressed because one or more lines are too long