diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..de056073 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +**/*.md diff --git a/src/readModeWidget.ts b/src/readModeWidget.ts index 203a7129..4e1eb66e 100644 --- a/src/readModeWidget.ts +++ b/src/readModeWidget.ts @@ -89,12 +89,17 @@ class ProgressBar extends Component { progressBarEl: HTMLSpanElement; progressBackGroundEl: HTMLDivElement; progressEl: HTMLDivElement; + inProgressEl: HTMLDivElement; + abandonedEl: HTMLDivElement; numberEl: HTMLDivElement; plugin: TaskProgressBarPlugin; completed: number; total: number; + inProgress: number; + abandoned: number; + notStarted: number; group: GroupElement; @@ -105,13 +110,23 @@ class ProgressBar extends Component { ) { super(); this.plugin = plugin; - this.group = group; - this.type === "dataview" && this.updateCompletedAndTotalDataview(); - this.type === "normal" && this.updateCompletedAndTotal(); + this.completed = 0; + this.total = 0; + this.inProgress = 0; + this.abandoned = 0; + this.notStarted = 0; + + if (type === "dataview") { + this.updateCompletedAndTotalDataview(); + } else { + this.updateCompletedAndTotal(); + } + + // Set up event handlers for (let el of this.group.childrenElement) { - this.type === "normal" && + if (this.type === "normal") { el.on("click", "input", () => { setTimeout(() => { this.updateCompletedAndTotal(); @@ -119,8 +134,7 @@ class ProgressBar extends Component { this.changeNumber(); }, 200); }); - - this.type === "dataview" && + } else if (this.type === "dataview") { this.registerDomEvent(el, "mousedown", (ev) => { if (!ev.target) return; if ((ev.target as HTMLElement).tagName === "INPUT") { @@ -131,131 +145,314 @@ class ProgressBar extends Component { }, 200); } }); + } } } + getTaskStatus( + text: string + ): "completed" | "inProgress" | "abandoned" | "notStarted" { + const markMatch = text.match(/\[(.)]/); + if (!markMatch || !markMatch[1]) { + return "notStarted"; + } + + const mark = markMatch[1]; + + // Priority 1: If useOnlyCountMarks is enabled + if (this.plugin?.settings.useOnlyCountMarks) { + const onlyCountMarks = + this.plugin?.settings.onlyCountTaskMarks.split("|"); + if (onlyCountMarks.includes(mark)) { + return "completed"; + } else { + // If using onlyCountMarks and the mark is not in the list, + // determine which other status it belongs to + return this.determineNonCompletedStatus(mark); + } + } + + // Priority 2: If the mark is in excludeTaskMarks + if ( + this.plugin?.settings.excludeTaskMarks && + this.plugin.settings.excludeTaskMarks.includes(mark) + ) { + // Excluded marks are considered not started + return "notStarted"; + } + + // Priority 3: Check against specific task statuses + return this.determineTaskStatus(mark); + } + + determineNonCompletedStatus( + mark: string + ): "inProgress" | "abandoned" | "notStarted" { + const inProgressMarks = + this.plugin?.settings.taskStatuses.inProgress.split("|"); + if (inProgressMarks.includes(mark)) { + return "inProgress"; + } + + const abandonedMarks = + this.plugin?.settings.taskStatuses.abandoned.split("|"); + if (abandonedMarks.includes(mark)) { + return "abandoned"; + } + + return "notStarted"; + } + + determineTaskStatus( + mark: string + ): "completed" | "inProgress" | "abandoned" | "notStarted" { + const completedMarks = + this.plugin?.settings.taskStatuses.completed.split("|"); + if (completedMarks.includes(mark)) { + return "completed"; + } + + const inProgressMarks = + this.plugin?.settings.taskStatuses.inProgress.split("|"); + if (inProgressMarks.includes(mark)) { + return "inProgress"; + } + + const abandonedMarks = + this.plugin?.settings.taskStatuses.abandoned.split("|"); + if (abandonedMarks.includes(mark)) { + return "abandoned"; + } + + // If not matching any specific status, check if it's a not-started mark + const notStartedMarks = + this.plugin?.settings.taskStatuses.notStarted.split("|"); + if (notStartedMarks.includes(mark)) { + return "notStarted"; + } + + // Default fallback - any unrecognized mark is considered not started + return "notStarted"; + } + + isCompletedTask(text: string): boolean { + const markMatch = text.match(/\[(.)]/); + if (!markMatch || !markMatch[1]) { + return false; + } + + const mark = markMatch[1]; + + // Priority 1: If useOnlyCountMarks is enabled, only count tasks with specified marks + if (this.plugin?.settings.useOnlyCountMarks) { + const onlyCountMarks = + this.plugin?.settings.onlyCountTaskMarks.split("|"); + return onlyCountMarks.includes(mark); + } + + // Priority 2: If the mark is in excludeTaskMarks, don't count it + if ( + this.plugin?.settings.excludeTaskMarks && + this.plugin.settings.excludeTaskMarks.includes(mark) + ) { + return false; + } + + // Priority 3: Check against the task statuses + // We consider a task "completed" if it has a mark from the "completed" status + const completedMarks = + this.plugin?.settings.taskStatuses.completed.split("|"); + + // Return true if the mark is in the completedMarks array + return completedMarks.includes(mark); + } + updateCompletedAndTotalDataview() { - // Get all task elements - const allTasks = this.group.childrenElement; - let checked = 0; + let completed = 0; + let inProgress = 0; + let abandoned = 0; + let notStarted = 0; let total = 0; - // Get settings for task markers - const excludeMarks = this.plugin?.settings.excludeTaskMarks - ? this.plugin.settings.excludeTaskMarks.split("") - : []; - const onlyCountMarks = this.plugin?.settings.onlyCountTaskMarks - ? this.plugin.settings.onlyCountTaskMarks.split("|") - : ["x", "X"]; - const useOnlyCountMarks = this.plugin?.settings.useOnlyCountMarks; + for (let element of this.group.childrenElement) { + const checkboxElement = element.querySelector( + ".task-list-item-checkbox" + ); + if (!checkboxElement) continue; - // Process each task element - for (const el of allTasks) { - const taskMark = el.getAttribute("data-task"); + // For dataview, we need to check the text content + const textContent = element.textContent?.trim() || ""; - // Skip if no task mark - if (!taskMark) continue; + total++; - // Count total tasks (excluding specified markers) - if (taskMark !== " " && !excludeMarks.includes(taskMark)) { - total++; - } + // Extract the task mark + const markMatch = textContent.match(/\[(.)]/); + if (markMatch && markMatch[1]) { + const status = this.getTaskStatus(textContent); - // Count completed tasks based on settings - if (useOnlyCountMarks) { - // Only count specific markers - if (onlyCountMarks.includes(taskMark)) { - checked++; + // Count based on status + if (this.isCompletedTask(textContent)) { + completed++; + } else if (status === "inProgress") { + inProgress++; + } else if (status === "abandoned") { + abandoned++; + } else if (status === "notStarted") { + notStarted++; } } else { - // Count all non-space markers (excluding specified markers) - if (taskMark !== " " && !excludeMarks.includes(taskMark)) { - checked++; + // Fallback to checking if the checkbox is checked + const checkbox = checkboxElement as HTMLInputElement; + if (checkbox.checked) { + completed++; + } else { + notStarted++; } } } - this.numberEl?.detach(); - - this.completed = checked; + this.completed = completed; + this.inProgress = inProgress; + this.abandoned = abandoned; + this.notStarted = notStarted; this.total = total; } updateCompletedAndTotal() { - // Get all task elements - const allTasks = this.group.childrenElement; - let checked = 0; - let total = allTasks.length; + let completed = 0; + let inProgress = 0; + let abandoned = 0; + let notStarted = 0; + let total = 0; - // Get settings for task markers - const excludeMarks = this.plugin?.settings.excludeTaskMarks - ? this.plugin.settings.excludeTaskMarks.split("") - : []; - const onlyCountMarks = this.plugin?.settings.onlyCountTaskMarks - ? this.plugin.settings.onlyCountTaskMarks.split("|") - : ["x", "X"]; - const useOnlyCountMarks = this.plugin?.settings.useOnlyCountMarks; + for (let element of this.group.childrenElement) { + const checkboxElement = element.querySelector( + ".task-list-item-checkbox" + ); + if (!checkboxElement) continue; - // In normal mode, we can only distinguish between checked and unchecked - // So we'll only implement the "only count specific markers" feature - // The exclude feature is not applicable here since we can't determine the specific marker + const checkbox = checkboxElement as HTMLInputElement; + const textContent = element.textContent?.trim() || ""; - if (useOnlyCountMarks) { - // For "only count specific markers" we need to check if the default checkbox is checked - // This is a limitation since we can't determine the specific marker in normal mode - checked = allTasks.filter((el) => el.hasClass("is-checked")).length; - } else { - checked = allTasks.filter((el) => el.hasClass("is-checked")).length; + // Extract the task mark + const markMatch = textContent.match(/\[(.)]/); + + total++; + + if (markMatch && markMatch[1]) { + const status = this.getTaskStatus(textContent); + + // Count based on status + if (this.isCompletedTask(textContent)) { + completed++; + } else if (status === "inProgress") { + inProgress++; + } else if (status === "abandoned") { + abandoned++; + } else if (status === "notStarted") { + notStarted++; + } + } else if (checkbox.checked) { + completed++; + } } - this.numberEl?.detach(); - - this.completed = checked; + this.completed = completed; + this.inProgress = inProgress; + this.abandoned = abandoned; + this.notStarted = notStarted; this.total = total; } changePercentage() { - const percentage = + if (this.total === 0) return; + + const completedPercentage = Math.round((this.completed / this.total) * 10000) / 100; - this.progressEl.style.width = percentage + "%"; + const inProgressPercentage = + Math.round((this.inProgress / this.total) * 10000) / 100; + const abandonedPercentage = + Math.round((this.abandoned / this.total) * 10000) / 100; + + // Set the completed part + this.progressEl.style.width = completedPercentage + "%"; + + // Set the in-progress part (if it exists) + if (this.inProgressEl) { + this.inProgressEl.style.width = inProgressPercentage + "%"; + this.inProgressEl.style.left = completedPercentage + "%"; + } + + // Set the abandoned part (if it exists) + if (this.abandonedEl) { + this.abandonedEl.style.width = abandonedPercentage + "%"; + this.abandonedEl.style.left = + completedPercentage + inProgressPercentage + "%"; + } + + // Update the class based on progress percentage + let progressClass = "progress-bar-inline"; + switch (true) { - case percentage >= 0 && percentage < 25: - this.progressEl.className = - "progress-bar-inline progress-bar-inline-0"; + case completedPercentage === 0: + progressClass += " progress-bar-inline-empty"; break; - case percentage >= 25 && percentage < 50: - this.progressEl.className = - "progress-bar-inline progress-bar-inline-1"; + case completedPercentage > 0 && completedPercentage < 25: + progressClass += " progress-bar-inline-0"; break; - case percentage >= 50 && percentage < 75: - this.progressEl.className = - "progress-bar-inline progress-bar-inline-2"; + case completedPercentage >= 25 && completedPercentage < 50: + progressClass += " progress-bar-inline-1"; break; - case percentage >= 75 && percentage < 100: - this.progressEl.className = - "progress-bar-inline progress-bar-inline-3"; + case completedPercentage >= 50 && completedPercentage < 75: + progressClass += " progress-bar-inline-2"; break; - case percentage >= 100: - this.progressEl.className = - "progress-bar-inline progress-bar-inline-4"; + case completedPercentage >= 75 && completedPercentage < 100: + progressClass += " progress-bar-inline-3"; + break; + case completedPercentage >= 100: + progressClass += " progress-bar-inline-complete"; break; } + + // Add classes for special states + if (inProgressPercentage > 0) { + progressClass += " has-in-progress"; + } + if (abandonedPercentage > 0) { + progressClass += " has-abandoned"; + } + + this.progressEl.className = progressClass; } changeNumber() { if (this.plugin?.settings.addNumberToProgressBar) { - const text = this.plugin?.settings.showPercentage - ? `${Math.round((this.completed / this.total) * 10000) / 100}%` - : `[${this.completed}/${this.total}]`; + let text; + if (this.plugin?.settings.showPercentage) { + // Calculate percentage of completed tasks + const percentage = + Math.round((this.completed / this.total) * 10000) / 100; + text = `${percentage}%`; + } else { + // Show detailed counts if we have in-progress or abandoned tasks + if (this.inProgress > 0 || this.abandoned > 0) { + text = `[${this.completed}✓ ${this.inProgress}⟳ ${this.abandoned}✗ / ${this.total}]`; + } else { + text = `[${this.completed}/${this.total}]`; + } + } - this.numberEl = this.progressBarEl.createEl("div", { - cls: "progress-status", - text: text, - }); - - return; + if (!this.numberEl) { + this.numberEl = this.progressBarEl.createEl("div", { + cls: "progress-status", + text: text, + }); + } else { + this.numberEl.innerText = text; + } + } else if (this.numberEl) { + this.numberEl.innerText = `[${this.completed}/${this.total}]`; } - this.numberEl.innerText = `[${this.completed}/${this.total}]`; } onload() { @@ -267,12 +464,39 @@ class ProgressBar extends Component { this.progressBackGroundEl = this.progressBarEl.createEl("div", { cls: "progress-bar-inline-background", }); - this.progressEl = this.progressBackGroundEl.createEl("div"); + + // Create elements for each status type + this.progressEl = this.progressBackGroundEl.createEl("div", { + cls: "progress-bar-inline progress-completed", + }); + + // Only create these elements if we have tasks of these types + if (this.inProgress > 0) { + this.inProgressEl = this.progressBackGroundEl.createEl("div", { + cls: "progress-bar-inline progress-in-progress", + }); + } + + if (this.abandoned > 0) { + this.abandonedEl = this.progressBackGroundEl.createEl("div", { + cls: "progress-bar-inline progress-abandoned", + }); + } if (this.plugin?.settings.addNumberToProgressBar && this.total) { - const text = this.plugin?.settings.showPercentage - ? `${Math.round((this.completed / this.total) * 10000) / 100}%` - : `[${this.completed}/${this.total}]`; + let text; + if (this.plugin?.settings.showPercentage) { + const percentage = + Math.round((this.completed / this.total) * 10000) / 100; + text = `${percentage}%`; + } else { + // Show detailed counts if we have in-progress or abandoned tasks + if (this.inProgress > 0 || this.abandoned > 0) { + text = `[${this.completed}✓ ${this.inProgress}⟳ ${this.abandoned}✗ / ${this.total}]`; + } else { + text = `[${this.completed}/${this.total}]`; + } + } this.numberEl = this.progressBarEl.createEl("div", { cls: "progress-status", diff --git a/src/task-status/AnuPpuccinThemeCollection.ts b/src/task-status/AnuPpuccinThemeCollection.ts new file mode 100644 index 00000000..d830e0ba --- /dev/null +++ b/src/task-status/AnuPpuccinThemeCollection.ts @@ -0,0 +1,42 @@ +// Code from https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/src/Config/Themes +// Original code is licensed under the MIT License. + +import type { StatusCollection } from "./StatusCollections"; + +/** + * Status supported by the AnuPpuccin theme. {@link https://github.com/AnubisNekhet/AnuPpuccin} + * @see {@link StatusSettings.bulkAddStatusCollection} + */ +export function anuppuccinSupportedStatuses() { + const zzz: StatusCollection = [ + [" ", "Unchecked", "notStarted"], + ["x", "Checked", "completed"], + [">", "Rescheduled", "planned"], + ["<", "Scheduled", "planned"], + ["!", "Important", "notStarted"], + ["-", "Cancelled", "abandoned"], + ["/", "In Progress", "inProgress"], + ["?", "Question", "notStarted"], + ["*", "Star", "notStarted"], + ["n", "Note", "notStarted"], + ["l", "Location", "notStarted"], + ["i", "Information", "notStarted"], + ["I", "Idea", "notStarted"], + ["S", "Amount", "notStarted"], + ["p", "Pro", "notStarted"], + ["c", "Con", "notStarted"], + ["b", "Bookmark", "notStarted"], + ['"', "Quote", "notStarted"], + ["0", "Speech bubble 0", "notStarted"], + ["1", "Speech bubble 1", "notStarted"], + ["2", "Speech bubble 2", "notStarted"], + ["3", "Speech bubble 3", "notStarted"], + ["4", "Speech bubble 4", "notStarted"], + ["5", "Speech bubble 5", "notStarted"], + ["6", "Speech bubble 6", "notStarted"], + ["7", "Speech bubble 7", "notStarted"], + ["8", "Speech bubble 8", "notStarted"], + ["9", "Speech bubble 9", "notStarted"], + ]; + return zzz; +} diff --git a/src/task-status/AuraThemeCollection.ts b/src/task-status/AuraThemeCollection.ts new file mode 100644 index 00000000..ce50bebb --- /dev/null +++ b/src/task-status/AuraThemeCollection.ts @@ -0,0 +1,33 @@ +// Code from https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/src/Config/Themes +// Original code is licensed under the MIT License. + +import type { StatusCollection } from "./StatusCollections"; + +/** + * Status supported by the Aura theme. {@link https://github.com/ashwinjadhav818/obsidian-aura} + * @see {@link StatusSettings.bulkAddStatusCollection} + */ +export function auraSupportedStatuses() { + const zzz: StatusCollection = [ + [" ", "incomplete", "notStarted"], + ["x", "complete / done", "completed"], + ["-", "cancelled", "abandoned"], + [">", "deferred", "planned"], + ["/", "in progress, or half-done", "inProgress"], + ["!", "Important", "notStarted"], + ["?", "question", "notStarted"], + ["R", "review", "notStarted"], + ["+", "Inbox / task that should be processed later", "notStarted"], + ["b", "bookmark", "notStarted"], + ["B", "brainstorm", "notStarted"], + ["D", "deferred or scheduled", "planned"], + ["I", "Info", "notStarted"], + ["i", "idea", "notStarted"], + ["N", "note", "notStarted"], + ["Q", "quote", "notStarted"], + ["W", "win / success / reward", "notStarted"], + ["P", "pro", "notStarted"], + ["C", "con", "notStarted"], + ]; + return zzz; +} diff --git a/src/task-status/BorderThemeCollection.ts b/src/task-status/BorderThemeCollection.ts new file mode 100644 index 00000000..380a446e --- /dev/null +++ b/src/task-status/BorderThemeCollection.ts @@ -0,0 +1,33 @@ +// Code from https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/src/Config/Themes +// Original code is licensed under the MIT License. +import type { StatusCollection } from "./StatusCollections"; + +/** + * Statuses supported by the Border theme. {@link https://github.com/Akifyss/obsidian-border?tab=readme-ov-file#alternate-checkboxes} + * @see {@link StatusSettings.bulkAddStatusCollection} + */ +export function borderSupportedStatuses() { + const zzz: StatusCollection = [ + [" ", "To Do", "notStarted"], + ["/", "In Progress", "inProgress"], + ["x", "Done", "completed"], + ["-", "Cancelled", "abandoned"], + [">", "Rescheduled", "planned"], + ["<", "Scheduled", "planned"], + ["!", "Important", "notStarted"], + ["?", "Question", "notStarted"], + ["i", "Infomation", "notStarted"], + ["S", "Amount", "notStarted"], + ["*", "Star", "notStarted"], + ["b", "Bookmark", "notStarted"], + ["“", "Quote", "notStarted"], + ["n", "Note", "notStarted"], + ["l", "Location", "notStarted"], + ["I", "Idea", "notStarted"], + ["p", "Pro", "notStarted"], + ["c", "Con", "notStarted"], + ["u", "Up", "notStarted"], + ["d", "Down", "notStarted"], + ]; + return zzz; +} diff --git a/src/task-status/EbullientworksThemeCollection.ts b/src/task-status/EbullientworksThemeCollection.ts new file mode 100644 index 00000000..5d170227 --- /dev/null +++ b/src/task-status/EbullientworksThemeCollection.ts @@ -0,0 +1,21 @@ +// Code from https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/src/Config/Themes +// Original code is licensed under the MIT License. +import type { StatusCollection } from "./StatusCollections"; + +/** + * Status supported by the Ebullientworks theme. {@link https://github.com/ebullient/obsidian-theme-ebullientworks} + * @see {@link StatusSettings.bulkAddStatusCollection} + */ +export function ebullientworksSupportedStatuses() { + const zzz: StatusCollection = [ + [" ", "Unchecked", "notStarted"], + ["x", "Checked", "completed"], + ["-", "Cancelled", "abandoned"], + ["/", "In Progress", "inProgress"], + [">", "Deferred", "planned"], + ["!", "Important", "notStarted"], + ["?", "Question", "planned"], + ["r", "Review", "notStarted"], + ]; + return zzz; +} diff --git a/src/task-status/ITSThemeCollection.ts b/src/task-status/ITSThemeCollection.ts new file mode 100644 index 00000000..b2c83958 --- /dev/null +++ b/src/task-status/ITSThemeCollection.ts @@ -0,0 +1,52 @@ +// Code from https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/src/Config/Themes +// Original code is licensed under the MIT License. + +import type { StatusCollection } from "./StatusCollections"; + +/** + * Status supported by the ITS theme. {@link https://github.com/SlRvb/Obsidian--ITS-Theme} + * Values recognised by Tasks are excluded. + * @see {@link StatusSettings.bulkAddStatusCollection} + */ +export function itsSupportedStatuses() { + const zzz: StatusCollection = [ + [" ", "Unchecked", "notStarted"], + ["x", "Regular", "completed"], + ["X", "Checked", "completed"], + ["-", "Dropped", "abandoned"], + [">", "Forward", "planned"], + ["D", "Date", "notStarted"], + ["?", "Question", "planned"], + ["/", "Half Done", "inProgress"], + ["+", "Add", "notStarted"], + ["R", "Research", "notStarted"], + ["!", "Important", "notStarted"], + ["i", "Idea", "notStarted"], + ["B", "Brainstorm", "notStarted"], + ["P", "Pro", "notStarted"], + ["C", "Con", "notStarted"], + ["Q", "Quote", "notStarted"], + ["N", "Note", "notStarted"], + ["b", "Bookmark", "notStarted"], + ["I", "Information", "notStarted"], + ["p", "Paraphrase", "notStarted"], + ["L", "Location", "notStarted"], + ["E", "Example", "notStarted"], + ["A", "Answer", "notStarted"], + ["r", "Reward", "notStarted"], + ["c", "Choice", "notStarted"], + ["d", "Doing", "inProgress"], + ["T", "Time", "notStarted"], + ["@", "Character / Person", "notStarted"], + ["t", "Talk", "notStarted"], + ["O", "Outline / Plot", "notStarted"], + ["~", "Conflict", "notStarted"], + ["W", "World", "notStarted"], + ["f", "Clue / Find", "notStarted"], + ["F", "Foreshadow", "notStarted"], + ["H", "Favorite / Health", "notStarted"], + ["&", "Symbolism", "notStarted"], + ["s", "Secret", "notStarted"], + ]; + return zzz; +} diff --git a/src/task-status/LYTModeThemeCollection.ts b/src/task-status/LYTModeThemeCollection.ts new file mode 100644 index 00000000..692073c9 --- /dev/null +++ b/src/task-status/LYTModeThemeCollection.ts @@ -0,0 +1,36 @@ +// Code from https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/src/Config/Themes +// Original code is licensed under the MIT License. + +import type { StatusCollection } from './StatusCollections'; + +/** + * Status supported by the LYT Mode theme. {@link https://github.com/nickmilo/LYT-Mode} + * @see {@link StatusSettings.bulkAddStatusCollection} + */ +export function lytModeSupportedStatuses() { + const zzz: StatusCollection = [ + [' ', 'Unchecked', 'notStarted'], + ['x', 'Checked', 'completed'], + ['>', 'Rescheduled', 'planned'], + ['<', 'Scheduled', 'planned'], + ['!', 'Important', 'notStarted'], + ['-', 'Cancelled', 'abandoned'], + ['/', 'In Progress', 'inProgress'], + ['?', 'Question', 'notStarted'], + ['*', 'Star', 'notStarted'], + ['n', 'Note', 'notStarted'], + ['l', 'Location', 'notStarted'], + ['i', 'Information', 'notStarted'], + ['I', 'Idea', 'notStarted'], + ['S', 'Amount', 'notStarted'], + ['p', 'Pro', 'notStarted'], + ['c', 'Con', 'notStarted'], + ['b', 'Bookmark', 'notStarted'], + ['f', 'Fire', 'notStarted'], + ['k', 'Key', 'notStarted'], + ['w', 'Win', 'notStarted'], + ['u', 'Up', 'notStarted'], + ['d', 'Down', 'notStarted'], + ]; + return zzz; +} diff --git a/src/task-status/MinimalThemeCollection.ts b/src/task-status/MinimalThemeCollection.ts new file mode 100644 index 00000000..c5301840 --- /dev/null +++ b/src/task-status/MinimalThemeCollection.ts @@ -0,0 +1,37 @@ +// Code from https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/src/Config/Themes +// Original code is licensed under the MIT License. + +import type { StatusCollection } from './StatusCollections'; + +/** + * Status supported by the Minimal theme. {@link https://github.com/kepano/obsidian-minimal} + * Values recognised by Tasks are excluded. + * @see {@link StatusSettings.bulkAddStatusCollection} + */ +export function minimalSupportedStatuses() { + const zzz: StatusCollection = [ + [' ', 'to-do', 'notStarted'], + ['/', 'incomplete', 'inProgress'], + ['x', 'done', 'completed'], + ['-', 'canceled', 'abandoned'], + ['>', 'forwarded', 'planned'], + ['<', 'scheduling', 'planned'], + ['?', 'question', 'notStarted'], + ['!', 'important', 'notStarted'], + ['*', 'star', 'notStarted'], + ['"', 'quote', 'notStarted'], + ['l', 'location', 'notStarted'], + ['b', 'bookmark', 'notStarted'], + ['i', 'information', 'notStarted'], + ['S', 'savings', 'notStarted'], + ['I', 'idea', 'notStarted'], + ['p', 'pros', 'notStarted'], + ['c', 'cons', 'notStarted'], + ['f', 'fire', 'notStarted'], + ['k', 'key', 'notStarted'], + ['w', 'win', 'notStarted'], + ['u', 'up', 'notStarted'], + ['d', 'down', 'notStarted'], + ]; + return zzz; +} diff --git a/src/task-status/StatusCollections.d.ts b/src/task-status/StatusCollections.d.ts new file mode 100644 index 00000000..166036f6 --- /dev/null +++ b/src/task-status/StatusCollections.d.ts @@ -0,0 +1,14 @@ +// Code from https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/src/Statuses +// Original code is licensed under the MIT License. + +/** + * The type used for a single entry in bulk imports of pre-created sets of statuses, such as for Themes or CSS Snippets. + * The values are: symbol, name, status type (must be one of the values in {@link StatusType} + */ +export type StatusCollectionEntry = [string, string, string]; + +/** + * The type used for bulk imports of pre-created sets of statuses, such as for Themes or CSS Snippets. + * See {@link Status.createFromImportedValue} + */ +export type StatusCollection = Array; diff --git a/src/task-status/ThingsThemeCollection.ts b/src/task-status/ThingsThemeCollection.ts new file mode 100644 index 00000000..079f8ffa --- /dev/null +++ b/src/task-status/ThingsThemeCollection.ts @@ -0,0 +1,38 @@ +// Code from https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/src/Config/Themes +// Original code is licensed under the MIT License. + +import type { StatusCollection } from './StatusCollections'; + +/** + * Status supported by the Things theme. {@link https://github.com/colineckert/obsidian-things} + * @see {@link StatusSettings.bulkAddStatusCollection} + */ +export function thingsSupportedStatuses() { + const zzz: StatusCollection = [ + // Basic + [' ', 'to-do', 'notStarted'], + ['/', 'incomplete', 'inProgress'], + ['x', 'done', 'completed'], + ['-', 'canceled', 'abandoned'], + ['>', 'forwarded', 'planned'], + ['<', 'scheduling', 'planned'], + // Extras + ['?', 'question', 'notStarted'], + ['!', 'important', 'notStarted'], + ['*', 'star', 'notStarted'], + ['"', 'quote', 'notStarted'], + ['l', 'location', 'notStarted'], + ['b', 'bookmark', 'notStarted'], + ['i', 'information', 'notStarted'], + ['S', 'savings', 'notStarted'], + ['I', 'idea', 'notStarted'], + ['p', 'pros', 'notStarted'], + ['c', 'cons', 'notStarted'], + ['f', 'fire', 'notStarted'], + ['k', 'key', 'notStarted'], + ['w', 'win', 'notStarted'], + ['u', 'up', 'notStarted'], + ['d', 'down', 'notStarted'], + ]; + return zzz; +} diff --git a/src/task-status/index.ts b/src/task-status/index.ts new file mode 100644 index 00000000..d1a949c9 --- /dev/null +++ b/src/task-status/index.ts @@ -0,0 +1,22 @@ +// Code from https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/src/Config/Themes +// Original code is licensed under the MIT License. + +export * from "./AnuPpuccinThemeCollection"; +export * from "./AuraThemeCollection"; +export * from "./BorderThemeCollection"; +export * from "./EbullientworksThemeCollection"; +export * from "./ITSThemeCollection"; +export * from "./LYTModeThemeCollection"; +export * from "./MinimalThemeCollection"; +export * from "./ThingsThemeCollection"; + +export const allStatusCollections: string[] = [ + "AnuPpuccin", + "Aura", + "Border", + "Ebullientworks", + "ITS", + "LYTMode", + "Minimal", + "Things", +]; diff --git a/src/task-status/readme.md b/src/task-status/readme.md new file mode 100644 index 00000000..bd9cc38a --- /dev/null +++ b/src/task-status/readme.md @@ -0,0 +1,11 @@ +# Task Status + +This folder contains the code for the task statuses that are supported by the plugin. + +## Claim + +This code is originally from [obsidian-tasks](https://github.com/obsidian-tasks-group/obsidian-tasks) plugin. + +## License + +This code is licensed under the MIT License. diff --git a/src/taskProgressBarIndex.ts b/src/taskProgressBarIndex.ts index d25cb88a..b7579e48 100644 --- a/src/taskProgressBarIndex.ts +++ b/src/taskProgressBarIndex.ts @@ -1,11 +1,100 @@ -import { App, Editor, Plugin, PluginSettingTab, Setting } from "obsidian"; -import { taskProgressBarExtension } from "./widget"; +import { + App, + debounce, + Editor, + editorEditorField, + editorInfoField, + HoverParent, + HoverPopover, + MarkdownRenderer, + Plugin, + PluginSettingTab, + Setting, +} from "obsidian"; +import { HTMLElementWithView, taskProgressBarExtension } from "./widget"; import { updateProgressBarInElement } from "./readModeWidget"; import { DEFAULT_SETTINGS, TaskProgressBarSettings, TaskProgressBarSettingTab, } from "./taskProgressBarSetting"; +import { EditorView } from "@codemirror/view"; + +class TaskProgressBarPopover extends HoverPopover { + plugin: TaskProgressBarPlugin; + data: { + completed: string; + total: string; + inProgress: string; + abandoned: string; + notStarted: string; + planned: string; + }; + + constructor( + plugin: TaskProgressBarPlugin, + data: { + completed: string; + total: string; + inProgress: string; + abandoned: string; + notStarted: string; + planned: string; + }, + parent: HoverParent, + targetEl: HTMLElement, + waitTime: number = 1000 + ) { + super(parent, targetEl, waitTime); + + this.hoverEl.toggleClass("task-progress-bar-popover", true); + this.plugin = plugin; + this.data = data; + } + + onload(): void { + MarkdownRenderer.render( + this.plugin.app, + ` +| Status | Count | +| --- | --- | +| Total | ${this.data.total} | +| Completed | ${this.data.completed} | +| In Progress | ${this.data.inProgress} | +| Abandoned | ${this.data.abandoned} | +| Not Started | ${this.data.notStarted} | +| Planned | ${this.data.planned} | +`, + this.hoverEl, + "", + this.plugin + ); + } +} + +export const showPopoverWithProgressBar = ( + plugin: TaskProgressBarPlugin, + { + progressBar, + data, + view, + }: { + progressBar: HTMLElement; + data: { + completed: string; + total: string; + inProgress: string; + abandoned: string; + notStarted: string; + planned: string; + }; + view: EditorView; + } +) => { + const editor = view.state.field(editorInfoField); + if (!editor) return; + new TaskProgressBarPopover(plugin, data, editor, progressBar); +}; export default class TaskProgressBarPlugin extends Plugin { settings: TaskProgressBarSettings; diff --git a/src/taskProgressBarSetting.ts b/src/taskProgressBarSetting.ts index 0bd7b7a8..3e8ce656 100644 --- a/src/taskProgressBarSetting.ts +++ b/src/taskProgressBarSetting.ts @@ -1,39 +1,61 @@ -import { App, PluginSettingTab, Setting } from "obsidian"; +import { App, PluginSettingTab, Setting, Modal } from "obsidian"; import TaskProgressBarPlugin from "./taskProgressBarIndex"; +import { allStatusCollections } from "./task-status"; export interface TaskProgressBarSettings { addTaskProgressBarToHeading: boolean; addNumberToProgressBar: boolean; showPercentage: boolean; - allowAlternateTaskStatus: boolean; - alternativeMarks: string; countSubLevel: boolean; hideProgressBarBasedOnConditions: boolean; hideProgressBarTags: string; hideProgressBarFolders: string; hideProgressBarMetadata: string; + + // Task state settings + taskStatuses: { + completed: string; + inProgress: string; + abandoned: string; + notStarted: string; + planned: string; + }; + + countOtherStatusesAs: string; + + // Control which tasks to count excludeTaskMarks: string; - onlyCountTaskMarks: string; useOnlyCountMarks: boolean; + onlyCountTaskMarks: string; } export const DEFAULT_SETTINGS: TaskProgressBarSettings = { addTaskProgressBarToHeading: false, addNumberToProgressBar: false, showPercentage: false, - allowAlternateTaskStatus: false, - alternativeMarks: "(x|X|-)", countSubLevel: true, hideProgressBarBasedOnConditions: false, hideProgressBarTags: "no-progress-bar", hideProgressBarFolders: "", hideProgressBarMetadata: "hide-progress-bar", + + // Default task statuses + taskStatuses: { + completed: "x|X", + inProgress: ">|/", + abandoned: "-", + notStarted: " ", + planned: "?", + }, + + countOtherStatusesAs: "notStarted", + + // Control which tasks to count excludeTaskMarks: "", onlyCountTaskMarks: "x|X", useOnlyCountMarks: false, }; - export class TaskProgressBarSettingTab extends PluginSettingTab { plugin: TaskProgressBarPlugin; private applyDebounceTimer: number = 0; @@ -87,43 +109,203 @@ export class TaskProgressBarSettingTab extends PluginSettingTab { }) ); + // Task Status Settings new Setting(containerEl) - .setName("Allow alternate task status") + .setName("Task Status Settings") .setDesc( - "Toggle this to allow this plugin to treat different tasks mark as completed or uncompleted tasks." + "Select a predefined task status collection or customize your own" ) - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.allowAlternateTaskStatus) - .onChange(async (value) => { - this.plugin.settings.allowAlternateTaskStatus = value; - this.applySettingsUpdate(); + .setHeading() + .addDropdown((dropdown) => { + dropdown.addOption("custom", "Custom"); + for (const statusCollection of allStatusCollections) { + dropdown.addOption(statusCollection, statusCollection); + } - setTimeout(() => { - this.display(); - }, 200); + // Set default value to custom + dropdown.setValue("custom"); + + dropdown.onChange(async (value) => { + if (value === "custom") { + return; + } + + // Confirm before applying the theme + const modal = new Modal(this.app); + modal.titleEl.setText(`Apply ${value} Theme?`); + + const content = modal.contentEl.createDiv(); + content.setText( + `This will override your current task status settings with the ${value} theme. Do you want to continue?` + ); + + const buttonContainer = modal.contentEl.createDiv(); + buttonContainer.addClass("modal-button-container"); + + const cancelButton = buttonContainer.createEl("button"); + cancelButton.setText("Cancel"); + cancelButton.addEventListener("click", () => { + dropdown.setValue("custom"); + modal.close(); + }); + + const confirmButton = buttonContainer.createEl("button"); + confirmButton.setText("Apply Theme"); + confirmButton.addClass("mod-cta"); + confirmButton.addEventListener("click", async () => { + modal.close(); + + // Apply the selected theme's task statuses + try { + // Import the function dynamically based on the selected theme + const functionName = + value.toLowerCase() + "SupportedStatuses"; + const statusesModule = await import( + "./task-status" + ); + + // Use type assertion for the dynamic function access + const getStatuses = (statusesModule as any)[ + functionName + ]; + + if (typeof getStatuses === "function") { + const statuses = getStatuses(); + + // Create a map to collect all statuses of each type + const statusMap: Record = { + completed: [], + inProgress: [], + abandoned: [], + notStarted: [], + planned: [], + }; + + // Group statuses by their type + for (const [symbol, _, type] of statuses) { + if (type in statusMap) { + statusMap[ + type as keyof typeof statusMap + ].push(symbol); + } + } + + // Update the settings with the collected statuses + for (const type of Object.keys( + this.plugin.settings.taskStatuses + )) { + if ( + statusMap[type] && + statusMap[type].length > 0 + ) { + ( + this.plugin.settings + .taskStatuses as Record< + string, + string + > + )[type] = statusMap[type].join("|"); + } + } + + // Save settings and refresh the display + this.applySettingsUpdate(); + this.display(); + } + } catch (error) { + console.error( + "Failed to apply task status theme:", + error + ); + } + }); + + modal.open(); + }); + }); + + new Setting(containerEl) + .setName("Completed task markers") + .setDesc( + 'Characters in square brackets that represent completed tasks. Example: "x|X"' + ) + .addText((text) => + text + .setPlaceholder(DEFAULT_SETTINGS.taskStatuses.completed) + .setValue(this.plugin.settings.taskStatuses.completed) + .onChange(async (value) => { + this.plugin.settings.taskStatuses.completed = + value || DEFAULT_SETTINGS.taskStatuses.completed; + this.applySettingsUpdate(); }) ); - if (this.plugin.settings.allowAlternateTaskStatus) { - new Setting(containerEl) - .setName("Completed alternative marks") - .setDesc('Set completed alternative marks here. Like "x|X|-"') - .addText((text) => - text - .setPlaceholder(DEFAULT_SETTINGS.alternativeMarks) - .setValue(this.plugin.settings.alternativeMarks) - .onChange(async (value) => { - if (value.length === 0) { - this.plugin.settings.alternativeMarks = - DEFAULT_SETTINGS.alternativeMarks; - } else { - this.plugin.settings.alternativeMarks = value; - } - this.applySettingsUpdate(); - }) - ); - } + new Setting(containerEl) + .setName("In progress task markers") + .setDesc( + 'Characters in square brackets that represent tasks in progress. Example: ">|/"' + ) + .addText((text) => + text + .setPlaceholder(DEFAULT_SETTINGS.taskStatuses.inProgress) + .setValue(this.plugin.settings.taskStatuses.inProgress) + .onChange(async (value) => { + this.plugin.settings.taskStatuses.inProgress = + value || DEFAULT_SETTINGS.taskStatuses.inProgress; + this.applySettingsUpdate(); + }) + ); + + new Setting(containerEl) + .setName("Abandoned task markers") + .setDesc( + 'Characters in square brackets that represent abandoned tasks. Example: "-"' + ) + .addText((text) => + text + .setPlaceholder(DEFAULT_SETTINGS.taskStatuses.abandoned) + .setValue(this.plugin.settings.taskStatuses.abandoned) + .onChange(async (value) => { + this.plugin.settings.taskStatuses.abandoned = + value || DEFAULT_SETTINGS.taskStatuses.abandoned; + this.applySettingsUpdate(); + }) + ); + + new Setting(containerEl) + .setName("Not started task markers") + .setDesc( + 'Characters in square brackets that represent not started tasks. Default is space " "' + ) + .addText((text) => + text + .setPlaceholder(DEFAULT_SETTINGS.taskStatuses.notStarted) + .setValue(this.plugin.settings.taskStatuses.notStarted) + .onChange(async (value) => { + this.plugin.settings.taskStatuses.notStarted = + value || DEFAULT_SETTINGS.taskStatuses.notStarted; + this.applySettingsUpdate(); + }) + ); + + new Setting(containerEl) + .setName("Count other statuses as") + .setDesc( + 'Select the status to count other statuses as. Default is "Not Started".' + ) + .addDropdown((dropdown) => { + dropdown.addOption("notStarted", "Not Started"); + dropdown.addOption("abandoned", "Abandoned"); + dropdown.addOption("planned", "Planned"); + dropdown.addOption("completed", "Completed"); + dropdown.addOption("inProgress", "In Progress"); + }); + + // Task Counting Settings + new Setting(containerEl) + .setName("Task Counting Settings") + .setDesc("Toggle this to allow this plugin to count sub tasks.") + .setHeading(); new Setting(containerEl) .setName("Exclude specific task markers") diff --git a/src/widget.ts b/src/widget.ts index 1d69523f..dcf76433 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -12,12 +12,18 @@ import { EditorState, Range, Text } from "@codemirror/state"; // @ts-ignore - This import is necessary but TypeScript can't find it import { foldable, syntaxTree, tokenClassNodeProp } from "@codemirror/language"; import { RegExpCursor } from "./regexp-cursor"; -import TaskProgressBarPlugin from "./taskProgressBarIndex"; +import TaskProgressBarPlugin, { + showPopoverWithProgressBar, +} from "./taskProgressBarIndex"; import { shouldHideProgressBarInLivePriview } from "./utils"; interface Tasks { completed: number; total: number; + inProgress?: number; + abandoned?: number; + notStarted?: number; + planned?: number; } // Type to represent a text range for safe access @@ -26,10 +32,17 @@ interface TextRange { to: number; } +export interface HTMLElementWithView extends HTMLElement { + view: EditorView; +} + class TaskProgressBarWidget extends WidgetType { progressBarEl: HTMLSpanElement; progressBackGroundEl: HTMLDivElement; progressEl: HTMLDivElement; + inProgressEl: HTMLDivElement; + abandonedEl: HTMLDivElement; + plannedEl: HTMLDivElement; numberEl: HTMLDivElement; constructor( @@ -39,68 +52,135 @@ class TaskProgressBarWidget extends WidgetType { readonly from: number, readonly to: number, readonly completed: number, - readonly total: number + readonly total: number, + readonly inProgress: number = 0, + readonly abandoned: number = 0, + readonly notStarted: number = 0, + readonly planned: number = 0 ) { super(); } eq(other: TaskProgressBarWidget) { - const markdownView = app.workspace.getActiveViewOfType(MarkdownView); - if (!markdownView) { - return false; - } - if (this.completed === other.completed && this.total === other.total) { - return true; - } - const editor = markdownView.editor; - const offset = editor.offsetToPos(this.from); - const originalOffset = editor.offsetToPos(other.from); - if (this.completed !== other.completed || this.total !== other.total) { - return false; - } if ( - offset.line === originalOffset.line && + this.from === other.from && + this.to === other.to && + this.inProgress === other.inProgress && + this.abandoned === other.abandoned && + this.notStarted === other.notStarted && + this.planned === other.planned && this.completed === other.completed && this.total === other.total ) { return true; } - return other.completed === this.completed && other.total === this.total; + return ( + other.completed === this.completed && + other.total === this.total && + other.inProgress === this.inProgress && + other.abandoned === this.abandoned && + other.notStarted === this.notStarted && + other.planned === this.planned + ); } changePercentage() { - const percentage = + if (this.total === 0) return; + + const completedPercentage = Math.round((this.completed / this.total) * 10000) / 100; - this.progressEl.style.width = percentage + "%"; + const inProgressPercentage = + Math.round((this.inProgress / this.total) * 10000) / 100; + const abandonedPercentage = + Math.round((this.abandoned / this.total) * 10000) / 100; + const plannedPercentage = + Math.round((this.planned / this.total) * 10000) / 100; + + // Set the completed part + this.progressEl.style.width = completedPercentage + "%"; + + // Set the in-progress part (if it exists) + if (this.inProgressEl) { + this.inProgressEl.style.width = inProgressPercentage + "%"; + this.inProgressEl.style.left = completedPercentage + "%"; + } + + // Set the abandoned part (if it exists) + if (this.abandonedEl) { + this.abandonedEl.style.width = abandonedPercentage + "%"; + this.abandonedEl.style.left = + completedPercentage + inProgressPercentage + "%"; + } + + // Set the planned part (if it exists) + if (this.plannedEl) { + this.plannedEl.style.width = plannedPercentage + "%"; + this.plannedEl.style.left = + completedPercentage + + inProgressPercentage + + abandonedPercentage + + "%"; + } + + // Update the class based on progress percentage + // This allows for CSS styling based on progress level + let progressClass = "progress-bar-inline"; + switch (true) { - case percentage >= 0 && percentage < 25: - this.progressEl.className = - "progress-bar-inline progress-bar-inline-0"; + case completedPercentage === 0: + progressClass += " progress-bar-inline-empty"; break; - case percentage >= 25 && percentage < 50: - this.progressEl.className = - "progress-bar-inline progress-bar-inline-1"; + case completedPercentage > 0 && completedPercentage < 25: + progressClass += " progress-bar-inline-0"; break; - case percentage >= 50 && percentage < 75: - this.progressEl.className = - "progress-bar-inline progress-bar-inline-2"; + case completedPercentage >= 25 && completedPercentage < 50: + progressClass += " progress-bar-inline-1"; break; - case percentage >= 75 && percentage < 100: - this.progressEl.className = - "progress-bar-inline progress-bar-inline-3"; + case completedPercentage >= 50 && completedPercentage < 75: + progressClass += " progress-bar-inline-2"; break; - case percentage >= 100: - this.progressEl.className = - "progress-bar-inline progress-bar-inline-4"; + case completedPercentage >= 75 && completedPercentage < 100: + progressClass += " progress-bar-inline-3"; + break; + case completedPercentage >= 100: + progressClass += " progress-bar-inline-complete"; break; } + + // Add classes for special states + if (inProgressPercentage > 0) { + progressClass += " has-in-progress"; + } + if (abandonedPercentage > 0) { + progressClass += " has-abandoned"; + } + if (plannedPercentage > 0) { + progressClass += " has-planned"; + } + + this.progressEl.className = progressClass; } changeNumber() { if (this.plugin?.settings.addNumberToProgressBar) { - const text = this.plugin?.settings.showPercentage - ? `${Math.round((this.completed / this.total) * 10000) / 100}%` - : `[${this.completed}/${this.total}]`; + let text; + if (this.plugin?.settings.showPercentage) { + // Calculate percentage of completed tasks + const percentage = + Math.round((this.completed / this.total) * 10000) / 100; + text = `${percentage}%`; + } else { + // Show detailed counts if we have in-progress, abandoned, or planned tasks + if ( + this.inProgress > 0 || + this.abandoned > 0 || + this.planned > 0 + ) { + text = `[${this.completed}✓ ${this.inProgress}⟳ ${this.abandoned}✗ ${this.planned}? / ${this.total}]`; + } else { + text = `[${this.completed}/${this.total}]`; + } + } if (!this.numberEl) { this.numberEl = this.progressBarEl.createEl("div", { @@ -109,10 +189,8 @@ class TaskProgressBarWidget extends WidgetType { }); } else { this.numberEl.innerText = text; - return; } - } - if (this.numberEl) { + } else if (this.numberEl) { this.numberEl.innerText = `[${this.completed}/${this.total}]`; } } @@ -121,8 +199,9 @@ class TaskProgressBarWidget extends WidgetType { if ( !this.plugin?.settings.addNumberToProgressBar && this.numberEl !== undefined - ) + ) { this.numberEl.detach(); + } if (this.progressBarEl !== undefined) { this.changePercentage(); @@ -133,17 +212,73 @@ class TaskProgressBarWidget extends WidgetType { this.progressBarEl = createSpan( this.plugin?.settings.addNumberToProgressBar ? "cm-task-progress-bar with-number" - : "cm-task-progress-bar" + : "cm-task-progress-bar", + (el) => { + el.dataset.completed = this.completed.toString(); + el.dataset.total = this.total.toString(); + el.dataset.inProgress = this.inProgress.toString(); + el.dataset.abandoned = this.abandoned.toString(); + el.dataset.notStarted = this.notStarted.toString(); + el.dataset.planned = this.planned.toString(); + + el.onmouseover = () => { + showPopoverWithProgressBar(this.plugin, { + progressBar: el, + data: { + completed: this.completed.toString(), + total: this.total.toString(), + inProgress: this.inProgress.toString(), + abandoned: this.abandoned.toString(), + notStarted: this.notStarted.toString(), + planned: this.planned.toString(), + }, + view: this.view, + }); + }; + } ); this.progressBackGroundEl = this.progressBarEl.createEl("div", { cls: "progress-bar-inline-background", }); - this.progressEl = this.progressBackGroundEl.createEl("div"); + + // Create elements for each status type + this.progressEl = this.progressBackGroundEl.createEl("div", { + cls: "progress-bar-inline progress-completed", + }); + + // Only create these elements if we have tasks of these types + if (this.inProgress > 0) { + this.inProgressEl = this.progressBackGroundEl.createEl("div", { + cls: "progress-bar-inline progress-in-progress", + }); + } + + if (this.abandoned > 0) { + this.abandonedEl = this.progressBackGroundEl.createEl("div", { + cls: "progress-bar-inline progress-abandoned", + }); + } + + if (this.planned > 0) { + this.plannedEl = this.progressBackGroundEl.createEl("div", { + cls: "progress-bar-inline progress-planned", + }); + } if (this.plugin?.settings.addNumberToProgressBar && this.total) { - const text = this.plugin?.settings.showPercentage - ? `${Math.round((this.completed / this.total) * 10000) / 100}%` - : `[${this.completed}/${this.total}]`; + let text; + if (this.plugin?.settings.showPercentage) { + const percentage = + Math.round((this.completed / this.total) * 10000) / 100; + text = `${percentage}%`; + } else { + // Show detailed counts if we have in-progress or abandoned tasks + if (this.inProgress > 0 || this.abandoned > 0) { + text = `[${this.completed}✓ ${this.inProgress}⟳ ${this.abandoned}✗ / ${this.total}]`; + } else { + text = `[${this.completed}/${this.total}]`; + } + } this.numberEl = this.progressBarEl.createEl("div", { cls: "progress-status", @@ -279,7 +414,11 @@ export function taskProgressBarExtension( headingLine.to, headingLine.to, tasksNum.completed, - tasksNum.total + tasksNum.total, + tasksNum.inProgress || 0, + tasksNum.abandoned || 0, + tasksNum.notStarted || 0, + tasksNum.planned || 0 ), }); @@ -358,7 +497,11 @@ export function taskProgressBarExtension( line.to, line.to, tasksNum.completed, - tasksNum.total + tasksNum.total, + tasksNum.inProgress || 0, + tasksNum.abandoned || 0, + tasksNum.notStarted || 0, + tasksNum.planned || 0 ), }); @@ -436,15 +579,48 @@ export function taskProgressBarExtension( level: number = 0, tabSize: number = 4 ): RegExp { - // 获取排除的任务标记 + // Check if we're using only specific marks for counting + if (plugin?.settings.useOnlyCountMarks) { + const onlyCountMarks = + plugin?.settings.onlyCountTaskMarks || ""; + // If onlyCountMarks is empty, return a regex that won't match anything + if (!onlyCountMarks.trim()) { + return new RegExp("^$"); // This won't match any tasks + } + + // Include the specified marks and space (for not started tasks) + const markPattern = `\\[([ ${onlyCountMarks}])\\]`; + + if (isHeading) { + // For headings, we'll still match any task format, but filter by indentation level later + return new RegExp( + `^[\\t|\\s]*([-*+]|\\d+\\.)\\s${markPattern}` + ); + } else { + // If counting sublevels, use a more relaxed regex that matches any indentation + if (plugin?.settings.countSubLevel) { + return new RegExp( + `^[\\t|\\s]*?([-*+]|\\d+\\.)\\s${markPattern}` + ); + } else { + // When not counting sublevels, we'll check the actual indentation level separately + // So the regex should match tasks at any indentation level + return new RegExp( + `^[\\t|\\s]*([-*+]|\\d+\\.)\\s${markPattern}` + ); + } + } + } + + // Get excluded task marks const excludePattern = plugin?.settings.excludeTaskMarks || ""; - // 构建识别任务标记的正则表达式部分 + // Build the task marker pattern let markPattern = "\\[(.)\\]"; - // 如果存在排除的标记,修改标记匹配模式 + // If there are excluded marks, modify the pattern if (excludePattern && excludePattern.length > 0) { - // 构建一个不匹配被排除标记的模式 + // Build a pattern that doesn't match excluded marks const excludeChars = excludePattern .split("") .map((c) => "\\" + c) @@ -453,19 +629,21 @@ export function taskProgressBarExtension( } if (isHeading) { - return new RegExp(`^([-*+]|\\d+\\.)\\s${markPattern}`); + // For headings, we'll still match any task format, but filter by indentation level later + return new RegExp( + `^[\\t|\\s]*([-*+]|\\d+\\.)\\s${markPattern}` + ); } else { - // 如果是子级计数模式,使用更宽松的正则表达式匹配任何缩进级别的任务 + // If counting sublevels, use a more relaxed regex if (plugin?.settings.countSubLevel) { return new RegExp( `^[\\t|\\s]*?([-*+]|\\d+\\.)\\s${markPattern}` ); } else { - // 否则使用精确缩进级别匹配 + // When not counting sublevels, we'll check the actual indentation level separately + // So the regex should match tasks at any indentation level return new RegExp( - `^[\\t|\\s]{${ - tabSize * (level + 1) - }}([-*+]|\\d+\\.)\\s${markPattern}` + `^[\\t|\\s]*([-*+]|\\d+\\.)\\s${markPattern}` ); } } @@ -484,25 +662,30 @@ export function taskProgressBarExtension( const useOnlyCountMarks = plugin?.settings.useOnlyCountMarks; const onlyCountPattern = plugin?.settings.onlyCountTaskMarks || "x|X"; - const excludePattern = plugin?.settings.excludeTaskMarks || ""; - const alternativeMarks = plugin?.settings.alternativeMarks; - // Default patterns - 对子级计数的情况调整缩进匹配 + // If onlyCountMarks is enabled but the pattern is empty, return a regex that won't match anything + if (useOnlyCountMarks && !onlyCountPattern.trim()) { + return new RegExp("^$"); // This won't match any tasks + } + + const excludePattern = plugin?.settings.excludeTaskMarks || ""; + const completedMarks = + plugin?.settings.taskStatuses?.completed || "x|X"; + + // Default patterns - adjust for sublevel counting const basePattern = isHeading - ? "^([-*+]|\\d+\\.)\\s" + ? "^[\\t|\\s]*" // For headings, match any indentation (will be filtered later) : plugin?.settings.countSubLevel - ? "^[\\t|\\s]*?" // 如果是子级计数,使用非贪婪模式匹配任意缩进 - : "^[\\t|\\s]+"; + ? "^[\\t|\\s]*?" // For sublevel counting, use non-greedy match for any indentation + : "^[\\t|\\s]*"; // For no sublevel counting, still match any indentation level const bulletPrefix = isHeading - ? "" + ? "([-*+]|\\d+\\.)\\s" // For headings, just match the bullet : plugin?.settings.countSubLevel - ? "([-*+]|\\d+\\.)\\s" // 子级计数时简化前缀 - : level !== 0 - ? `{${tabSize * (level + 1)}}([-*+]|\\d+\\.)\\s` - : "([-*+]|\\d+\\.)\\s"; + ? "([-*+]|\\d+\\.)\\s" // Simplified prefix for sublevel counting + : "([-*+]|\\d+\\.)\\s"; // For no sublevel counting, just match the bullet - // 如果启用了仅统计特定标记功能 + // If "only count specific marks" is enabled if (useOnlyCountMarks) { return new RegExp( basePattern + @@ -513,53 +696,29 @@ export function taskProgressBarExtension( ); } - // Handle alternative marks option - 仅在未启用仅统计特定标记时才考虑替代标记 - if ( - !useOnlyCountMarks && - plugin?.settings.allowAlternateTaskStatus && - alternativeMarks && - alternativeMarks.length > 0 - ) { - if (excludePattern) { - // Filter alternative marks based on exclusions - const alternativeMarksArray = alternativeMarks - .replace(/[()]/g, "") - .split("|"); - const excludeMarksArray = excludePattern.split(""); - const filteredMarks = alternativeMarksArray - .filter((mark) => !excludeMarksArray.includes(mark)) - .join("|"); - - return new RegExp( - basePattern + - bulletPrefix + - "\\[(" + - filteredMarks + - ")\\]" - ); - } else { - return new RegExp( - basePattern + - bulletPrefix + - "\\[" + - alternativeMarks + - "\\]" - ); - } - } - - // Handle standard case - 如果没有启用特殊选项 + // When using the completed task marks if (excludePattern) { + // Filter completed marks based on exclusions + const completedMarksArray = completedMarks.split("|"); + const excludeMarksArray = excludePattern.split(""); + const filteredMarks = completedMarksArray + .filter((mark) => !excludeMarksArray.includes(mark)) + .join("|"); + return new RegExp( basePattern + bulletPrefix + - "\\[[^ " + - excludePattern + - "]\\]" + "\\[(" + + filteredMarks + + ")\\]" ); } else { return new RegExp( - basePattern + bulletPrefix + "\\[[^ ]\\]" + basePattern + + bulletPrefix + + "\\[(" + + completedMarks + + ")\\]" ); } } @@ -568,59 +727,193 @@ export function taskProgressBarExtension( * Check if a task should be counted as completed */ private isCompletedTask(text: string): boolean { - // 如果启用了仅统计特定标记 - if (plugin?.settings.useOnlyCountMarks) { - const onlyCountPattern = - plugin?.settings.onlyCountTaskMarks || "x|X"; - const markMatch = text.match(/\[(.)]/); - console.log(markMatch, text); - if (markMatch && markMatch[1]) { - const mark = markMatch[1]; - // 检查标记是否在仅统计列表中 - const onlyCountMarks = onlyCountPattern.split("|"); - return onlyCountMarks.includes(mark); - } + const markMatch = text.match(/\[(.)]/); + if (!markMatch || !markMatch[1]) { return false; } - // 如果启用了替代标记 + const mark = markMatch[1]; + + // Priority 1: If useOnlyCountMarks is enabled, only count tasks with specified marks + if (plugin?.settings.useOnlyCountMarks) { + const onlyCountMarks = + plugin?.settings.onlyCountTaskMarks.split("|"); + return onlyCountMarks.includes(mark); + } + + // Priority 2: If the mark is in excludeTaskMarks, don't count it if ( - plugin?.settings.allowAlternateTaskStatus && - plugin?.settings.alternativeMarks + plugin?.settings.excludeTaskMarks && + plugin.settings.excludeTaskMarks.includes(mark) ) { - const alternativeMarks = plugin?.settings.alternativeMarks - .replace(/[()]/g, "") - .split("|"); - const markMatch = text.match(/\[(.)]/); - if (markMatch && markMatch[1]) { - const mark = markMatch[1]; - return alternativeMarks.includes(mark); - } + return false; } - // 标准检查 - 非空格的字符 + // Priority 3: Check against the task statuses + // We consider a task "completed" if it has a mark from the "completed" status + const completedMarks = + plugin?.settings.taskStatuses?.completed?.split("|") || [ + "x", + "X", + ]; + + // Return true if the mark is in the completedMarks array + return completedMarks.includes(mark); + } + + /** + * Get the task status of a task + */ + private getTaskStatus( + text: string + ): + | "completed" + | "inProgress" + | "abandoned" + | "notStarted" + | "planned" { const markMatch = text.match(/\[(.)]/); - if (markMatch && markMatch[1]) { - const mark = markMatch[1]; - // 排除需要排除的标记 - if ( - plugin?.settings.excludeTaskMarks && - plugin.settings.excludeTaskMarks.includes(mark) - ) { - return false; - } - // 标准完成是非空格 - return mark !== " "; + console.log(markMatch, text); + if (!markMatch || !markMatch[1]) { + return "notStarted"; } - return false; + const mark = markMatch[1]; + // Priority 1: If useOnlyCountMarks is enabled + if (plugin?.settings.useOnlyCountMarks) { + const onlyCountMarks = + plugin?.settings.onlyCountTaskMarks.split("|"); + if (onlyCountMarks.includes(mark)) { + return "completed"; + } else { + // If using onlyCountMarks and the mark is not in the list, + // determine which other status it belongs to + return this.determineNonCompletedStatus(mark); + } + } + + // Priority 2: If the mark is in excludeTaskMarks + if ( + plugin?.settings.excludeTaskMarks && + plugin.settings.excludeTaskMarks.includes(mark) + ) { + // Excluded marks are considered not started + return "notStarted"; + } + + // Priority 3: Check against specific task statuses + return this.determineTaskStatus(mark); + } + + /** + * Helper to determine the non-completed status of a task mark + */ + private determineNonCompletedStatus( + mark: string + ): "inProgress" | "abandoned" | "notStarted" | "planned" { + const inProgressMarks = + plugin?.settings.taskStatuses?.inProgress?.split("|") || [ + "-", + "/", + ]; + + if (inProgressMarks.includes(mark)) { + return "inProgress"; + } + + const abandonedMarks = + plugin?.settings.taskStatuses?.abandoned?.split("|") || [ + ">", + ]; + if (abandonedMarks.includes(mark)) { + return "abandoned"; + } + + const plannedMarks = + plugin?.settings.taskStatuses?.planned?.split("|") || ["?"]; + if (plannedMarks.includes(mark)) { + return "planned"; + } + + // If the mark doesn't match any specific category, use the countOtherStatusesAs setting + return ( + (plugin?.settings.countOtherStatusesAs as + | "inProgress" + | "abandoned" + | "notStarted" + | "planned") || "notStarted" + ); + } + + /** + * Helper to determine the specific status of a task mark + */ + private determineTaskStatus( + mark: string + ): + | "completed" + | "inProgress" + | "abandoned" + | "notStarted" + | "planned" { + const completedMarks = + plugin?.settings.taskStatuses?.completed?.split("|") || [ + "x", + "X", + ]; + if (completedMarks.includes(mark)) { + return "completed"; + } + + const inProgressMarks = + plugin?.settings.taskStatuses?.inProgress?.split("|") || [ + "-", + "/", + ]; + if (inProgressMarks.includes(mark)) { + return "inProgress"; + } + + const abandonedMarks = + plugin?.settings.taskStatuses?.abandoned?.split("|") || [ + ">", + ]; + if (abandonedMarks.includes(mark)) { + return "abandoned"; + } + + const plannedMarks = + plugin?.settings.taskStatuses?.planned?.split("|") || ["?"]; + if (plannedMarks.includes(mark)) { + return "planned"; + } + + // If not matching any specific status, check if it's a not-started mark + const notStartedMarks = + plugin?.settings.taskStatuses?.notStarted?.split("|") || [ + " ", + ]; + if (notStartedMarks.includes(mark)) { + return "notStarted"; + } + + // If we get here, the mark doesn't match any of our defined categories + // Use the countOtherStatusesAs setting to determine how to count it + return ( + (plugin?.settings.countOtherStatusesAs as + | "completed" + | "inProgress" + | "abandoned" + | "notStarted" + | "planned") || "notStarted" + ); } /** * Check if a task marker should be excluded from counting */ private shouldExcludeTask(text: string): boolean { - // 如果没有排除设置,返回false + // If no exclusion settings, return false if ( !plugin?.settings.excludeTaskMarks || plugin.settings.excludeTaskMarks.length === 0 @@ -628,7 +921,7 @@ export function taskProgressBarExtension( return false; } - // 检查任务标记是否在排除列表中 + // Check if task mark is in the exclusion list const taskMarkMatch = text.match(/\[(.)]/); if (taskMarkMatch && taskMarkMatch[1]) { const taskMark = taskMarkMatch[1]; @@ -661,16 +954,34 @@ export function taskProgressBarExtension( bullet: boolean ): Tasks { if (!textArray || textArray.length === 0) { - return { completed: 0, total: 0 }; + return { + completed: 0, + total: 0, + inProgress: 0, + abandoned: 0, + notStarted: 0, + planned: 0, + }; } let completed: number = 0; + let inProgress: number = 0; + let abandoned: number = 0; + let notStarted: number = 0; + let planned: number = 0; let total: number = 0; let level: number = 0; // Get tab size from vault config const tabSize = this.getTabSize(); + // For debugging - collect task marks and their statuses + const taskDebug: { + mark: string; + status: string; + lineText: string; + }[] = []; + // Determine indentation level for bullets if (!plugin?.settings.countSubLevel && bullet && textArray[0]) { const indentMatch = textArray[0].match(/^[\s|\t]*/); @@ -685,6 +996,7 @@ export function taskProgressBarExtension( level, tabSize ); + const bulletCompleteRegex = this.createCompletedTaskRegex( plugin, false, @@ -702,41 +1014,150 @@ export function taskProgressBarExtension( if (i === 0) continue; // Skip the first line if (bullet) { - // 确保每个任务只被计算一次 - const lineText = textArray[i].trim(); - // 首先检查是否匹配任务格式,然后检查是否是应该被排除的任务类型 + const lineText = textArray[i]; + const lineTextTrimmed = lineText.trim(); + + // If countSubLevel is false, check the indentation level directly + if (!plugin?.settings.countSubLevel) { + const indentMatch = lineText.match(/^[\s|\t]*/); + const lineLevel = indentMatch + ? indentMatch[0].length / tabSize + : 0; + + // Only count this task if it's exactly one level deeper + if (lineLevel !== level + 1) { + continue; + } + } + + // First check if it matches task format, then check if it should be excluded if ( - lineText && + lineTextTrimmed && lineText.match(bulletTotalRegex) && - !this.shouldExcludeTask(lineText) + !this.shouldExcludeTask(lineTextTrimmed) ) { total++; - // 使用新方法判断是否为已完成任务 - if (this.isCompletedTask(lineText)) { + // Get the task status + const status = this.getTaskStatus(lineTextTrimmed); + + // Extract the mark for debugging + const markMatch = lineTextTrimmed.match(/\[(.)]/); + if (markMatch && markMatch[1]) { + taskDebug.push({ + mark: markMatch[1], + status: status, + lineText: lineTextTrimmed, + }); + } + + // Count based on status + if (status === "completed") { completed++; + } else if (status === "inProgress") { + inProgress++; + } else if (status === "abandoned") { + abandoned++; + } else if (status === "planned") { + planned++; + } else if (status === "notStarted") { + notStarted++; } } } else if (plugin?.settings.addTaskProgressBarToHeading) { - const lineText = textArray[i].trim(); - // 同样使用shouldExcludeTask函数进行额外的验证 + const lineText = textArray[i]; + const lineTextTrimmed = lineText.trim(); + + // For headings, if countSubLevel is false, only count top-level tasks (no indentation) + if (!plugin?.settings.countSubLevel) { + const indentMatch = lineText.match(/^[\s|\t]*/); + const lineLevel = indentMatch + ? indentMatch[0].length / tabSize + : 0; + + // For headings, only count tasks with no indentation when countSubLevel is false + if (lineLevel !== 0) { + continue; + } + } + + // Also use shouldExcludeTask for additional validation if ( - lineText && + lineTextTrimmed && lineText.match(headingTotalRegex) && - !this.shouldExcludeTask(lineText) + !this.shouldExcludeTask(lineTextTrimmed) ) { total++; - // 使用新方法判断是否为已完成任务 - if (this.isCompletedTask(lineText)) { + // Get the task status + const status = this.getTaskStatus(lineTextTrimmed); + + // Extract the mark for debugging + const markMatch = lineTextTrimmed.match(/\[(.)]/); + if (markMatch && markMatch[1]) { + taskDebug.push({ + mark: markMatch[1], + status: status, + lineText: lineTextTrimmed, + }); + } + + // Count based on status + if (status === "completed") { completed++; + } else if (status === "inProgress") { + inProgress++; + } else if (status === "abandoned") { + abandoned++; + } else if (status === "planned") { + planned++; + } else if (status === "notStarted") { + notStarted++; } } } } - // 确保完成数不超过总数 - completed = Math.min(completed, total); + // Log debug information + if (taskDebug.length > 0) { + console.log("Task counting debug:", { + taskDebug, + completedCount: completed, + inProgressCount: inProgress, + abandonedCount: abandoned, + notStartedCount: notStarted, + plannedCount: planned, + totalCount: total, + settings: { + useOnlyCountMarks: + plugin?.settings.useOnlyCountMarks, + onlyCountTaskMarks: + plugin?.settings.onlyCountTaskMarks, + excludeTaskMarks: plugin?.settings.excludeTaskMarks, + taskStatuses: plugin?.settings.taskStatuses, + countOtherStatusesAs: + plugin?.settings.countOtherStatusesAs, + }, + }); + } - return { completed, total }; + // Ensure counts don't exceed total + completed = Math.min(completed, total); + inProgress = Math.min(inProgress, total - completed); + abandoned = Math.min(abandoned, total - completed - inProgress); + planned = Math.min( + planned, + total - completed - inProgress - abandoned + ); + notStarted = + total - completed - inProgress - abandoned - planned; + + return { + completed, + total, + inProgress, + abandoned, + notStarted, + planned, + }; } }, { diff --git a/styles.css b/styles.css index c3b0d796..2d05547b 100644 --- a/styles.css +++ b/styles.css @@ -1,3 +1,33 @@ +/* Define theme variables */ +:root { + /* Task status colors - light theme */ + --task-completed-color: #4caf50; + --task-in-progress-color: #f9d923; + --task-abandoned-color: #eb5353; + --task-planned-color: #9c27b0; /* Added planned task color */ + + /* Progress bar gradient colors - light theme */ + --progress-0-color: #ae431e; + --progress-25-color: #e5890a; + --progress-50-color: #b4c6a6; + --progress-75-color: #6bcb77; + --progress-100-color: #4d96ff; +} + +/* Dark theme color adjustments */ +.theme-dark { + --task-completed-color: #4caf50; + --task-in-progress-color: #ffc107; + --task-abandoned-color: #f44336; + --task-planned-color: #ce93d8; /* Added planned task color for dark theme */ + + --progress-0-color: #ae431e; + --progress-25-color: #e5890a; + --progress-50-color: #b4c6a6; + --progress-75-color: #6bcb77; + --progress-100-color: #4d96ff; +} + /* Set Default Progress Bar For Plugin */ .cm-task-progress-bar { display: inline-block; @@ -14,28 +44,63 @@ } .progress-bar-inline { - border-radius: 10px; height: 8px; + position: relative; +} + +/* Progress bar colors for different percentages of completion */ +.progress-bar-inline-empty { + background-color: #f1f1f1; } .progress-bar-inline-0 { - background-color: #AE431E; + background-color: var(--progress-0-color); } .progress-bar-inline-1 { - background-color: #E5890A; + background-color: var(--progress-25-color); } .progress-bar-inline-2 { - background-color: #B4C6A6; + background-color: var(--progress-50-color); } .progress-bar-inline-3 { - background-color: #6BCB77; + background-color: var(--progress-75-color); } -.progress-bar-inline-4 { - background-color: #4D96FF; +.progress-bar-inline-complete { + background-color: var(--progress-100-color); +} + +/* Colors for different task statuses */ +.progress-completed { + background-color: var(--task-completed-color); + z-index: 3; +} + +.progress-in-progress { + background-color: var(--task-in-progress-color); + z-index: 2; + position: absolute; + top: 0; + height: 100%; +} + +.progress-abandoned { + background-color: var(--task-abandoned-color); + z-index: 1; + position: absolute; + top: 0; + height: 100%; +} + +.progress-planned { + background-color: var(--task-planned-color); + z-index: 1; + position: absolute; + top: 0; + height: 100%; } .progress-bar-inline-background { @@ -46,20 +111,45 @@ justify-content: flex-start; align-items: center; width: 85px; + position: relative; + overflow: hidden; +} + +/* Status indicators in the task number display */ +.cm-task-progress-bar .task-status-indicator { + display: inline-block; + margin-right: 2px; +} + +.cm-task-progress-bar .completed-indicator { + color: var(--task-completed-color); +} + +.cm-task-progress-bar .in-progress-indicator { + color: var(--task-in-progress-color); +} + +.cm-task-progress-bar .abandoned-indicator { + color: var(--task-abandoned-color); +} + +.cm-task-progress-bar .planned-indicator { + color: var(--task-planned-color); } /* Set Default Progress Bar With Number For Plugin */ - .cm-task-progress-bar.with-number { display: inline-flex; align-items: center; } -.HyperMD-header .cm-task-progress-bar.with-number .progress-bar-inline-background, .HyperMD-header .cm-task-progress-bar.with-number .progress-status { +.HyperMD-header + .cm-task-progress-bar.with-number + .progress-bar-inline-background, +.HyperMD-header .cm-task-progress-bar.with-number .progress-status { margin-bottom: 5px; } - .cm-task-progress-bar.with-number .progress-bar-inline-background { margin-bottom: -2px; width: 42px; @@ -69,3 +159,24 @@ font-size: 13px; margin-left: 3px; } + +/* Adaptations for dark theme */ +.theme-dark .progress-completed { + background-color: var(--task-completed-color); +} + +.theme-dark .progress-in-progress { + background-color: var(--task-in-progress-color); +} + +.theme-dark .progress-abandoned { + background-color: var(--task-abandoned-color); +} + +.theme-dark .progress-planned { + background-color: var(--task-planned-color); +} + +.task-progress-bar-popover { + width: 400px; +}