feat: support custom status

This commit is contained in:
quorafind 2025-03-02 13:33:23 +08:00
parent dc35c6a635
commit 1c3df1fc12
17 changed files with 1673 additions and 306 deletions

1
.prettierignore Normal file
View file

@ -0,0 +1 @@
**/*.md

View file

@ -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",

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

14
src/task-status/StatusCollections.d.ts vendored Normal file
View file

@ -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<StatusCollectionEntry>;

View file

@ -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;
}

22
src/task-status/index.ts Normal file
View file

@ -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",
];

11
src/task-status/readme.md Normal file
View file

@ -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.

View file

@ -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;

View file

@ -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<string, string[]> = {
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")

View file

@ -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,
};
}
},
{

View file

@ -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;
}