feat(quick-capture): add datetime-local support for task dates

This commit is contained in:
Quorafind 2025-11-29 21:09:07 +08:00
parent e89e3e59db
commit 359ea6dc15
4 changed files with 124 additions and 49 deletions

View file

@ -13,6 +13,7 @@ import {
processDateTemplates,
} from "@/utils/file/file-operations";
import { t } from "@/translations/helper";
import { formatDateSmart, isDateOnly } from "@/utils/date/date-utils";
import { SuggestManager } from "@/components/ui/suggest";
import { EmbeddableMarkdownEditor } from "@/editor-extensions/core/markdown-editor";
@ -138,6 +139,12 @@ export abstract class BaseQuickCaptureModal extends Modal {
if (typeof value === "string") {
const isoParsed = moment(value, moment.ISO_8601, true);
if (isoParsed.isValid()) return isoParsed.toDate();
const dateTimeParsed = moment(
value,
["YYYY-MM-DD HH:mm", "YYYY-MM-DDTHH:mm"],
true,
);
if (dateTimeParsed.isValid()) return dateTimeParsed.toDate();
const strictDate = moment(value, "YYYY-MM-DD", true);
if (strictDate.isValid()) return strictDate.toDate();
}
@ -809,18 +816,39 @@ export abstract class BaseQuickCaptureModal extends Modal {
* Format date
*/
protected formatDate(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
2,
"0",
)}-${String(date.getDate()).padStart(2, "0")}`;
return formatDateSmart(date, { includeSeconds: false });
}
/**
* Parse date
*/
protected parseDate(dateString: string): Date {
const [year, month, day] = dateString.split("-").map(Number);
return new Date(year, month - 1, day);
const trimmed = dateString?.trim();
if (!trimmed) return new Date("");
const parsed = moment(
trimmed,
["YYYY-MM-DD HH:mm", "YYYY-MM-DDTHH:mm", "YYYY-MM-DD"],
true,
);
if (parsed.isValid()) {
const date = parsed.toDate();
// Preserve date-only timestamps as midnight to keep smart formatting consistent
if (parsed.creationData().format === "YYYY-MM-DD") {
date.setHours(0, 0, 0, 0);
}
return date;
}
const fallback = new Date(trimmed);
if (isNaN(fallback.getTime())) {
return new Date("");
}
if (isDateOnly(fallback)) {
fallback.setHours(0, 0, 0, 0);
}
return fallback;
}
/**

View file

@ -387,10 +387,10 @@ export class QuickCaptureModal extends BaseQuickCaptureModal {
private createDateInputs(container: HTMLElement): void {
// Start Date
new Setting(container).setName(t("Start Date")).addText((text) => {
text.setPlaceholder("YYYY-MM-DD")
text.setPlaceholder("YYYY-MM-DD HH:mm")
.setValue(
this.taskMetadata.startDate
? this.formatDate(this.taskMetadata.startDate)
? this.formatDateInputValue(this.taskMetadata.startDate)
: "",
)
.onChange((value) => {
@ -405,16 +405,16 @@ export class QuickCaptureModal extends BaseQuickCaptureModal {
}
this.updatePreview();
});
text.inputEl.type = "date";
text.inputEl.type = "datetime-local";
this.startDateInput = text.inputEl;
});
// Due Date
new Setting(container).setName(t("Due Date")).addText((text) => {
text.setPlaceholder("YYYY-MM-DD")
text.setPlaceholder("YYYY-MM-DD HH:mm")
.setValue(
this.taskMetadata.dueDate
? this.formatDate(this.taskMetadata.dueDate)
? this.formatDateInputValue(this.taskMetadata.dueDate)
: "",
)
.onChange((value) => {
@ -429,16 +429,18 @@ export class QuickCaptureModal extends BaseQuickCaptureModal {
}
this.updatePreview();
});
text.inputEl.type = "date";
text.inputEl.type = "datetime-local";
this.dueDateInput = text.inputEl;
});
// Scheduled Date
new Setting(container).setName(t("Scheduled Date")).addText((text) => {
text.setPlaceholder("YYYY-MM-DD")
text.setPlaceholder("YYYY-MM-DD HH:mm")
.setValue(
this.taskMetadata.scheduledDate
? this.formatDate(this.taskMetadata.scheduledDate)
? this.formatDateInputValue(
this.taskMetadata.scheduledDate,
)
: "",
)
.onChange((value) => {
@ -453,7 +455,7 @@ export class QuickCaptureModal extends BaseQuickCaptureModal {
}
this.updatePreview();
});
text.inputEl.type = "date";
text.inputEl.type = "datetime-local";
this.scheduledDateInput = text.inputEl;
});
}
@ -863,24 +865,18 @@ export class QuickCaptureModal extends BaseQuickCaptureModal {
// Update metadata (only if not manually set)
if (aggregatedStartDate && !this.isManuallySet("startDate")) {
this.taskMetadata.startDate = aggregatedStartDate;
if (this.startDateInput) {
this.startDateInput.value =
this.formatDate(aggregatedStartDate);
}
this.setDateInputValue(this.startDateInput, aggregatedStartDate);
}
if (aggregatedDueDate && !this.isManuallySet("dueDate")) {
this.taskMetadata.dueDate = aggregatedDueDate;
if (this.dueDateInput) {
this.dueDateInput.value = this.formatDate(aggregatedDueDate);
}
this.setDateInputValue(this.dueDateInput, aggregatedDueDate);
}
if (aggregatedScheduledDate && !this.isManuallySet("scheduledDate")) {
this.taskMetadata.scheduledDate = aggregatedScheduledDate;
if (this.scheduledDateInput) {
this.scheduledDateInput.value = this.formatDate(
aggregatedScheduledDate,
);
}
this.setDateInputValue(
this.scheduledDateInput,
aggregatedScheduledDate,
);
}
}
@ -905,6 +901,19 @@ export class QuickCaptureModal extends BaseQuickCaptureModal {
this.taskMetadata.manuallySet[field] = true;
}
private setDateInputValue(
input: HTMLInputElement | undefined,
value?: Date,
): void {
if (!input) return;
input.value = this.formatDateInputValue(value);
}
private formatDateInputValue(value?: Date): string {
if (!value) return "";
return moment(value).format("YYYY-MM-DD[T]HH:mm");
}
/**
* Reset UI elements
*/

View file

@ -1,7 +1,7 @@
import { App, Notice } from "obsidian";
import { App, Notice, moment } from "obsidian";
import TaskProgressBarPlugin from "../index";
import { t } from "../translations/helper";
import { moment } from "obsidian";
import { formatDateSmart } from "@/utils/date/date-utils";
export class ElectronQuickCapture {
private captureWindow: any = null;
@ -396,6 +396,12 @@ export class ElectronQuickCapture {
private parseDueDate(dateStr: string): string | undefined {
if (!dateStr) return undefined;
const trimmed = dateStr.trim();
const normalize = (value: moment.Moment): string | undefined => {
if (!value || !value.isValid()) return undefined;
return formatDateSmart(value.toDate(), { includeSeconds: false });
};
try {
// Try natural language parsing first
const naturalParsers = [
@ -406,26 +412,33 @@ export class ElectronQuickCapture {
];
for (const parser of naturalParsers) {
const match = dateStr.match(parser.pattern);
const match = trimmed.match(parser.pattern);
if (match) {
const offset = parser.offsetMatch
? parseInt(match[parser.offsetMatch])
const offset = parser.offsetMatch
? parseInt(match[parser.offsetMatch])
: parser.offset;
const date = moment().add(offset, 'days');
return date.format('YYYY-MM-DD');
const date = moment().add(offset, "days");
const formatted = normalize(date);
if (formatted) return formatted;
}
}
// Try parsing as date
const parsed = moment(dateStr);
if (parsed.isValid()) {
return parsed.format('YYYY-MM-DD');
}
// Try parsing with strict formats (supporting date and datetime)
const parsed = moment(
trimmed,
[moment.ISO_8601, "YYYY-MM-DD HH:mm", "YYYY-MM-DDTHH:mm", "YYYY-MM-DD"],
true,
);
const strictFormatted = normalize(parsed);
if (strictFormatted) return strictFormatted;
// Fallback to loose parsing
const fallback = moment(trimmed);
return normalize(fallback);
} catch {
// Ignore parsing errors
return undefined;
}
return undefined;
}
private async getDataForWindow(type: string): Promise<any> {
@ -810,7 +823,7 @@ export class ElectronQuickCapture {
<div class="metadata-item">
<label>Start Date</label>
<div class="date-input-wrapper">
<input type="date" id="start-date-picker" class="date-picker">
<input type="datetime-local" id="start-date-picker" class="date-picker">
<input type="text" id="start-date-text" placeholder="today, tomorrow" class="date-text" style="display:none">
<button type="button" class="date-toggle" data-date-type="start" title="Toggle input type">🛫</button>
</div>
@ -819,7 +832,7 @@ export class ElectronQuickCapture {
<div class="metadata-item">
<label>Due Date</label>
<div class="date-input-wrapper">
<input type="date" id="due-date-picker" class="date-picker">
<input type="datetime-local" id="due-date-picker" class="date-picker">
<input type="text" id="due-date-text" placeholder="tomorrow, next week" class="date-text" style="display:none">
<button type="button" class="date-toggle" data-date-type="due" title="Toggle input type">📅</button>
</div>
@ -828,7 +841,7 @@ export class ElectronQuickCapture {
<div class="metadata-item">
<label>Scheduled Date</label>
<div class="date-input-wrapper">
<input type="date" id="scheduled-date-picker" class="date-picker">
<input type="datetime-local" id="scheduled-date-picker" class="date-picker">
<input type="text" id="scheduled-date-text" placeholder="next monday" class="date-text" style="display:none">
<button type="button" class="date-toggle" data-date-type="scheduled" title="Toggle input type"></button>
</div>
@ -1233,7 +1246,8 @@ export class ElectronQuickCapture {
function parseNaturalDate(input) {
if (!input) return '';
const lower = input.toLowerCase().trim();
const trimmed = input.trim();
const lower = trimmed.toLowerCase();
const today = new Date();
// Natural language patterns
@ -1309,21 +1323,41 @@ export class ElectronQuickCapture {
// Try to parse as a regular date
try {
const parsed = new Date(input);
const parsed = new Date(trimmed);
if (!isNaN(parsed.getTime())) {
return formatDate(parsed);
}
} catch {}
// If not a natural language pattern, return original
return input;
return trimmed;
}
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return year + '-' + month + '-' + day;
const hasTime =
date.getHours() !== 0 ||
date.getMinutes() !== 0 ||
date.getSeconds() !== 0 ||
date.getMilliseconds() !== 0;
if (!hasTime) {
return year + '-' + month + '-' + day;
}
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return (
year +
'-' +
month +
'-' +
day +
' ' +
hours +
':' +
minutes
);
}
</script>
</body>
@ -1334,4 +1368,4 @@ export class ElectronQuickCapture {
this.closeCaptureWindow();
this.cleanupIPCHandlers();
}
}
}

View file

@ -632,3 +632,7 @@
gap: var(--size-4-3);
}
.quick-capture-modal .setting-item-control input[type="datetime-local"] {
width: 10rem;
}