From 410e5069505de4f8f129133fd7c3a0039b56e527 Mon Sep 17 00:00:00 2001 From: terryli710 Date: Wed, 4 Oct 2023 18:03:05 -0700 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20task=20duration=20-=20still=20b?= =?UTF-8?q?uggy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 18 ++ package.json | 3 + src/autoSuggestions/Suggester.ts | 51 ++++- src/components/icons/CalendarClock.svelte | 18 ++ src/components/icons/History.svelte | 14 ++ src/renderer/TaskCardRenderer.ts | 3 +- src/taskModule/task.ts | 14 ++ src/taskModule/taskParser.ts | 22 +- src/taskModule/taskSyncManager.ts | 7 +- src/ui/Due.svelte | 149 +++++++++---- src/ui/Duration.svelte | 245 ++++++++++++++++++++++ src/ui/LabelInput.svelte | 4 +- src/ui/Labels.svelte | 1 + src/ui/TaskCard.svelte | 11 +- styles.css | 8 +- 15 files changed, 513 insertions(+), 55 deletions(-) create mode 100644 src/components/icons/CalendarClock.svelte create mode 100644 src/components/icons/History.svelte create mode 100644 src/ui/Duration.svelte diff --git a/package-lock.json b/package-lock.json index 84ce5b7..94b6ecc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,13 @@ "version": "0.1.0", "license": "MIT", "dependencies": { + "humanize-duration": "^3.30.0", + "humanized-duration": "^0.0.1", "lucide-svelte": "^0.268.0", "marked": "^6.0.0", "obsidian": "latest", "obsidian-dataview": "^0.5.56", + "parse-duration": "^1.1.0", "runtypes": "^6.7.0", "sugar": "^2.0.6", "svelte": "^4.1.1", @@ -5064,6 +5067,16 @@ "node": ">=10.17.0" } }, + "node_modules/humanize-duration": { + "version": "3.30.0", + "resolved": "https://registry.npmmirror.com/humanize-duration/-/humanize-duration-3.30.0.tgz", + "integrity": "sha512-NxpT0fhQTFuMTLnuu1Xp+ozNpYirQnbV3NlOjEKBYlE3uvMRu3LDuq8EPc3gVXxVYnchQfqVM4/+T9iwHPLLeA==" + }, + "node_modules/humanized-duration": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/humanized-duration/-/humanized-duration-0.0.1.tgz", + "integrity": "sha512-Hfew+W2MDi9ezvdRH+2MUraxEb6grcV04mulYhkghxRHuCkt6tCWroL7X29aLGcEFjLjmD21M998Q3wNyUZNhg==" + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -7001,6 +7014,11 @@ "resolved": "https://registry.npmmirror.com/papaparse/-/papaparse-5.4.1.tgz", "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" }, + "node_modules/parse-duration": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/parse-duration/-/parse-duration-1.1.0.tgz", + "integrity": "sha512-z6t9dvSJYaPoQq7quMzdEagSFtpGu+utzHqqxmpVWNNZRIXnvqyCvn9XsTdh7c/w0Bqmdz3RB3YnRaKtpRtEXQ==" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz", diff --git a/package.json b/package.json index 0283d9b..c3c470a 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,13 @@ "ts-jest": "^29.1.1" }, "dependencies": { + "humanize-duration": "^3.30.0", + "humanized-duration": "^0.0.1", "lucide-svelte": "^0.268.0", "marked": "^6.0.0", "obsidian": "latest", "obsidian-dataview": "^0.5.56", + "parse-duration": "^1.1.0", "runtypes": "^6.7.0", "sugar": "^2.0.6", "svelte": "^4.1.1", diff --git a/src/autoSuggestions/Suggester.ts b/src/autoSuggestions/Suggester.ts index 1ae30ed..b690e40 100644 --- a/src/autoSuggestions/Suggester.ts +++ b/src/autoSuggestions/Suggester.ts @@ -38,8 +38,9 @@ export class AttributeSuggester { // ); this.inputtableAttributes = [ - 'priority', 'due', + 'duration', + 'priority', 'project', ] }); @@ -56,6 +57,9 @@ export class AttributeSuggester { suggestions = suggestions.concat( this.getDueSuggestions(lineText, cursorPos) ); + suggestions = suggestions.concat( + this.getDurationSuggestions(lineText, cursorPos) + ) suggestions = suggestions.concat( this.getProjectSuggestions(lineText, cursorPos) ); @@ -180,6 +184,51 @@ export class AttributeSuggester { return suggestions; } + getDurationSuggestions( + lineText: string, + cursorPos: number + ): SuggestInformation[] { + let suggestions: SuggestInformation[] = []; + + // Modify regex to capture the due date query + const durationRegexText = `${escapeRegExp(this.startingNotation)}\\s?duration:(.*?)${escapeRegExp(this.endingNotation)}`; + const durationRegex = new RegExp(durationRegexText, 'g'); + const durationMatch = matchByPositionAndGroup(lineText, durationRegex, cursorPos, 1); + if (!durationMatch) return suggestions; // No match + + // Get the due date query from the captured group + const durationQuery = (durationMatch[1] || '').trim(); + + const durationStringSelections = [ + '5 minutes', + '10 minutes', + '15 minutes', + '30 minutes', + '1 hour', + '2 hours', + '3 hours', + ]; + + // Use the dueQuery to filter the suggestions + const filteredDurationStrings = durationStringSelections.filter((durationString) => + durationString.toLowerCase().startsWith(durationQuery.toLowerCase()) + ); + + suggestions = filteredDurationStrings.map((durationString) => { + const replaceText = `${this.startingNotation}duration: ${durationString}${this.endingNotation} `; + return { + displayText: durationString, + replaceText: replaceText, + replaceFrom: durationMatch.index, + replaceTo: durationMatch.index + durationMatch[0].length, + cursorPosition: durationMatch.index + replaceText.length + }; + }); + + return suggestions; + + } + getProjectSuggestions( lineText: string, cursorPos: number diff --git a/src/components/icons/CalendarClock.svelte b/src/components/icons/CalendarClock.svelte new file mode 100644 index 0000000..eba5970 --- /dev/null +++ b/src/components/icons/CalendarClock.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/src/components/icons/History.svelte b/src/components/icons/History.svelte new file mode 100644 index 0000000..4ab2128 --- /dev/null +++ b/src/components/icons/History.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/src/renderer/TaskCardRenderer.ts b/src/renderer/TaskCardRenderer.ts index a51d7fb..1de2b8c 100644 --- a/src/renderer/TaskCardRenderer.ts +++ b/src/renderer/TaskCardRenderer.ts @@ -96,7 +96,8 @@ taskCardStatus: { descriptionStatus: 'done', projectStatus: 'done', - dueStatus: 'done' + dueStatus: 'done', + durationStatus: 'done' }, markdownTask: null, taskItemEl: taskItemEl, diff --git a/src/taskModule/task.ts b/src/taskModule/task.ts index 9ca02e9..1d85136 100644 --- a/src/taskModule/task.ts +++ b/src/taskModule/task.ts @@ -19,6 +19,11 @@ export type DueDate = { timezone?: string | null; }; +export type Duration = { + hours: number; + minutes: number; +} + export type SectionID = string; export type Priority = 1 | 2 | 3 | 4; export type Order = number; @@ -38,6 +43,7 @@ export interface TaskProperties { children: TaskProperties[] | ObsidianTask[]; due?: DueDate | null; + duration?: Duration | null; metadata?: { taskDisplayParams?: TaskDisplayParams | null; [key: string]: any; @@ -59,6 +65,7 @@ export class ObsidianTask implements TaskProperties { public children: TaskProperties[] | ObsidianTask[]; public due?: DueDate | null; + public duration?: Duration | null; public metadata?: { taskDisplayParams?: TaskDisplayParams | null; @@ -78,6 +85,7 @@ export class ObsidianTask implements TaskProperties { this.parent = props?.parent || null; this.children = props?.children || []; this.due = props?.due || null; + this.duration = props?.duration || null; this.metadata = props?.metadata || {}; } @@ -111,6 +119,12 @@ export class ObsidianTask implements TaskProperties { return !!this.due.string; } + hasDuration(): boolean { + if (!this.duration) return false; + // return if the duration string is not empty = hours and minutes all zero + return this.duration.hours > 0 || this.duration.minutes > 0; + } + setTaskDisplayParams(key: string, value: any): void { this.metadata.taskDisplayParams = { ...this.metadata.taskDisplayParams, diff --git a/src/taskModule/taskParser.ts b/src/taskModule/taskParser.ts index 33fc017..eb1dda4 100644 --- a/src/taskModule/taskParser.ts +++ b/src/taskModule/taskParser.ts @@ -2,11 +2,12 @@ import { logger } from '../utils/log'; import { escapeRegExp, extractTags } from '../utils/regexUtils'; import { kebabToCamel } from '../utils/stringCaseConverter'; import { toArray, toBoolean } from '../utils/typeConversion'; -import { DueDate, ObsidianTask, Order, Priority, TaskProperties, TextPosition } from './task'; +import { DueDate, Duration, ObsidianTask, Order, Priority, TaskProperties, TextPosition } from './task'; import { Project, ProjectModule } from './project'; import Sugar from 'sugar'; import { SettingStore } from '../settings'; import { DescriptionParser } from './description'; +import parse from 'parse-duration'; export class TaskParser { @@ -87,6 +88,7 @@ export class TaskParser { task.parent = attributes.parent || null; task.children = attributes.children || []; task.due = attributes.due || null; + task.duration = attributes.duration || null; task.metadata = attributes.metadata || {}; // Get labels from content @@ -241,6 +243,9 @@ export class TaskParser { case 'due': task.due = tryParseAttribute('due', this.parseDue.bind(this), attributeValue, 'other'); break; + case 'duration': + task.duration = tryParseAttribute('duration', this.parseDuration.bind(this), attributeValue, 'other'); + break; case 'project': task.project = tryParseAttribute('project', this.parseProject.bind(this), attributeValue, 'string'); break; @@ -328,6 +333,7 @@ export class TaskParser { task.order = parseJSONAttribute(metadata['order'], 'order', 0); task.project = parseJSONAttribute(metadata['project'], 'project', null); task.due = parseJSONAttribute(metadata['due'], 'due', null); + task.duration = parseJSONAttribute(metadata['duration'], 'duration', null); task.metadata = parseJSONAttribute(metadata['metadata'], 'metadata', {}); // Optional attributes @@ -422,6 +428,20 @@ export class TaskParser { } } + parseDuration(durationString: string): Duration | null { + const durationInMinutes = parse(durationString, 'm'); + logger.debug(`durationInMinutes: ${durationInMinutes}`); + // Convert the difference to hours and minutes + const hours = Math.floor(durationInMinutes / 60); + const minutes = durationInMinutes % 60; + + return { + hours: hours, + minutes: minutes + } as Duration; + } + + parseProject(projectString: string): Project | null { const project = this.projectModule.getProjectByName(projectString); if (!project) { diff --git a/src/taskModule/taskSyncManager.ts b/src/taskModule/taskSyncManager.ts index acaa34a..ed3a292 100644 --- a/src/taskModule/taskSyncManager.ts +++ b/src/taskModule/taskSyncManager.ts @@ -7,6 +7,7 @@ type TaskCardStatus = { descriptionStatus: 'editing' | 'done'; projectStatus: 'selecting' | 'done'; dueStatus: 'editing' | 'done'; + durationStatus: 'editing' | 'done'; }; export interface ObsidianTaskSyncProps { @@ -44,7 +45,8 @@ export class ObsidianTaskSyncManager implements ObsidianTaskSyncProps { this.taskCardStatus = props?.taskCardStatus || { descriptionStatus: 'done', projectStatus: 'done', - dueStatus: 'done' + dueStatus: 'done', + durationStatus: 'done', }; this.taskItemEl = props?.taskItemEl || null; this.taskMetadata = props?.taskMetadata || { @@ -118,7 +120,8 @@ export class ObsidianTaskSyncManager implements ObsidianTaskSyncProps { const allowedStatuses = { descriptionStatus: ['editing', 'done'], projectStatus: ['selecting', 'done'], - dueStatus: ['editing', 'done'] + dueStatus: ['editing', 'done'], + durationStatus: ['editing', 'done'] }; return allowedStatuses[key].includes(status); } diff --git a/src/ui/Due.svelte b/src/ui/Due.svelte index 26a0770..546dec3 100644 --- a/src/ui/Due.svelte +++ b/src/ui/Due.svelte @@ -1,26 +1,46 @@ -{#if taskSyncManager.obsidianTask.hasDue() || taskSyncManager.getTaskCardStatus('dueStatus') === 'editing'} - {#if taskSyncManager.getTaskCardStatus('dueStatus') === 'editing'} - adjustWidthForInput(inputElement)} - bind:value={dueString} - bind:this={inputElement} - class="task-card-due" - /> - {:else} -
-
- {dueDisplay} -
+{#if displayDue} +
+
+
- {/if} - {#if params.mode !== 'single-line'} -
|
- {/if} + {#if taskSyncManager.getTaskCardStatus('dueStatus') === 'editing'} + adjustWidthForInput(inputElement)} + bind:value={dueString} + bind:this={inputElement} + class="task-card-due" + /> + {:else} +
+
+ {dueDisplay} +
+
+ {/if} +
{/if} diff --git a/src/ui/LabelInput.svelte b/src/ui/LabelInput.svelte index 2f10b88..68bd80a 100644 --- a/src/ui/LabelInput.svelte +++ b/src/ui/LabelInput.svelte @@ -7,7 +7,6 @@ export let finishLabelEditing; export let inputElement: HTMLInputElement; - setTimeout(() => { if (inputElement) { inputElement.focus(); // Focus on the input element @@ -41,7 +40,7 @@ } .task-card-label-input-left-part { - background-color: var(--background-secondary); + background-color: var(--background-primary); border-top-left-radius: 2em; border-bottom-left-radius: 2em; border-top-right-radius: var(--radius-s); @@ -59,6 +58,7 @@ .task-card-label-input { display: inline-block; + background-color: var(--background-primary-alt); padding: var(--tag-padding-y) var(--tag-padding-x); font-size: var(--tag-size); color: var(--text-muted); diff --git a/src/ui/Labels.svelte b/src/ui/Labels.svelte index 18595c2..b776f37 100644 --- a/src/ui/Labels.svelte +++ b/src/ui/Labels.svelte @@ -152,6 +152,7 @@ button.label-plus-button { color: var(--tag-color); background-color: var(--tag-background); border-radius: 50%; + padding-top: 3px; } button.label-plus-button:hover { diff --git a/src/ui/TaskCard.svelte b/src/ui/TaskCard.svelte index 0949194..3f07b46 100644 --- a/src/ui/TaskCard.svelte +++ b/src/ui/TaskCard.svelte @@ -17,6 +17,7 @@ import { SettingStore } from '../settings'; import { DescriptionParser } from '../taskModule/description'; import CircularProgressBar from '../components/CircularProgressBar.svelte'; + import Duration from './Duration.svelte'; export let taskSyncManager: ObsidianTaskSyncManager; export let plugin: TaskCardPlugin; @@ -186,6 +187,8 @@ cardMenu.showAtPosition({ x: event.clientX, y: event.clientY }); } + let displayDue: boolean = taskSyncManager.obsidianTask.hasDue() || taskSyncManager.getTaskCardStatus('dueStatus') === 'editing'; + let displayDuration: boolean = taskSyncManager.obsidianTask.hasDuration() || taskSyncManager.getTaskCardStatus('durationStatus') === 'editing'; @@ -237,9 +240,15 @@
- {#if taskSyncManager.obsidianTask.hasDue() || taskSyncManager.getTaskCardStatus('dueStatus') === 'editing'} + {#if displayDue} {/if} + {#if displayDuration} + + {/if} + {#if displayDue || displayDuration} +
+ {/if}
diff --git a/styles.css b/styles.css index 0780a31..b277380 100644 --- a/styles.css +++ b/styles.css @@ -82,6 +82,7 @@ display: flex; flex-direction: row; align-items: center; /* Vertically center the children */ + align-self: center; } .task-card-checkbox-wrapper { @@ -140,8 +141,11 @@ .task-card-attribute-separator { flex-shrink: 0; /* Prevents shrinking */ margin: 0 4px; - color: var(--color-base-50); - font-size: var(--font-ui-medium); + margin-top: 3px; + border-left: 1px solid var(--interactive-hover); + height: 13px; + align-items: center; + align-self: center; } .task-card-icon { From 32c70f663ccccb13e95806ab014935fbce382ea5 Mon Sep 17 00:00:00 2001 From: terryli710 Date: Thu, 5 Oct 2023 10:53:22 -0700 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=90=9B=20=20debug=20duration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/taskModule/taskParser.ts | 1 - src/ui/Due.svelte | 1 - src/ui/Duration.svelte | 4 +--- src/ui/TaskCard.svelte | 24 ++++++++++++++++++++++-- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/taskModule/taskParser.ts b/src/taskModule/taskParser.ts index eb1dda4..f952946 100644 --- a/src/taskModule/taskParser.ts +++ b/src/taskModule/taskParser.ts @@ -430,7 +430,6 @@ export class TaskParser { parseDuration(durationString: string): Duration | null { const durationInMinutes = parse(durationString, 'm'); - logger.debug(`durationInMinutes: ${durationInMinutes}`); // Convert the difference to hours and minutes const hours = Math.floor(durationInMinutes / 60); const minutes = durationInMinutes % 60; diff --git a/src/ui/Due.svelte b/src/ui/Due.svelte index 546dec3..cbdb8f0 100644 --- a/src/ui/Due.svelte +++ b/src/ui/Due.svelte @@ -136,7 +136,6 @@ {#if taskSyncManager.getTaskCardStatus('dueStatus') === 'editing'} adjustWidthForInput(inputElement)} bind:value={dueString} bind:this={inputElement} diff --git a/src/ui/Duration.svelte b/src/ui/Duration.svelte index 99967e6..525ad75 100644 --- a/src/ui/Duration.svelte +++ b/src/ui/Duration.svelte @@ -11,7 +11,7 @@ export let taskSyncManager: ObsidianTaskSyncManager; - export let plugin: TaskCardPlugin; + // export let plugin: TaskCardPlugin; export let params: TaskDisplayParams; let duration: Duration | null; duration = taskSyncManager.obsidianTask.hasDuration() ? taskSyncManager.obsidianTask.duration : null; @@ -61,7 +61,6 @@ } async function toggleDurationEditMode(event: KeyboardEvent | MouseEvent) { - logger.debug(`duration status: ${taskSyncManager.taskCardStatus.durationStatus}`); if (taskSyncManager.taskCardStatus.durationStatus === 'done') { enableDurationEditMode(event); } else { @@ -142,7 +141,6 @@ {#if taskSyncManager.getTaskCardStatus('durationStatus') === 'editing'} { item.setTitle('Add Description'); @@ -124,6 +124,7 @@ item.setIcon('plus'); item.onClick((evt) => { taskSyncManager.taskCardStatus.dueStatus = 'editing'; + displayDue = taskSyncManager.obsidianTask.hasDue() || taskSyncManager.getTaskCardStatus('dueStatus') === 'editing'; }); }); } else { @@ -136,6 +137,25 @@ }); } + if (!taskSyncManager.obsidianTask.hasDuration()) { + cardMenu.addItem((item) => { + item.setTitle('Add Duration'); + item.setIcon('plus'); + item.onClick((evt) => { + taskSyncManager.taskCardStatus.durationStatus = 'editing'; + displayDuration = taskSyncManager.obsidianTask.hasDue() || taskSyncManager.getTaskCardStatus('dueStatus') === 'editing'; + }); + }); + } else { + cardMenu.addItem((item) => { + item.setTitle('Delete Duration'); + item.setIcon('trash'); + item.onClick((evt) => { + taskSyncManager.updateObsidianTaskAttribute('duration', null); + }); + }); + } + // Separator cardMenu.addSeparator(); @@ -244,7 +264,7 @@ {/if} {#if displayDuration} - + {/if} {#if displayDue || displayDuration}