mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat: support only marks condition
This commit is contained in:
parent
4d034c94d2
commit
dc35c6a635
4 changed files with 906 additions and 378 deletions
|
|
@ -135,12 +135,45 @@ class ProgressBar extends Component {
|
|||
}
|
||||
|
||||
updateCompletedAndTotalDataview() {
|
||||
const checked = this.group.childrenElement.filter(
|
||||
(el) =>
|
||||
el.getAttribute("data-task") &&
|
||||
el.getAttribute("data-task") !== " "
|
||||
).length;
|
||||
const total = this.group.childrenElement.length;
|
||||
// Get all task elements
|
||||
const allTasks = this.group.childrenElement;
|
||||
let checked = 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;
|
||||
|
||||
// Process each task element
|
||||
for (const el of allTasks) {
|
||||
const taskMark = el.getAttribute("data-task");
|
||||
|
||||
// Skip if no task mark
|
||||
if (!taskMark) continue;
|
||||
|
||||
// Count total tasks (excluding specified markers)
|
||||
if (taskMark !== " " && !excludeMarks.includes(taskMark)) {
|
||||
total++;
|
||||
}
|
||||
|
||||
// Count completed tasks based on settings
|
||||
if (useOnlyCountMarks) {
|
||||
// Only count specific markers
|
||||
if (onlyCountMarks.includes(taskMark)) {
|
||||
checked++;
|
||||
}
|
||||
} else {
|
||||
// Count all non-space markers (excluding specified markers)
|
||||
if (taskMark !== " " && !excludeMarks.includes(taskMark)) {
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.numberEl?.detach();
|
||||
|
||||
|
|
@ -149,10 +182,31 @@ class ProgressBar extends Component {
|
|||
}
|
||||
|
||||
updateCompletedAndTotal() {
|
||||
const checked = this.group.childrenElement.filter((el) =>
|
||||
el.hasClass("is-checked")
|
||||
).length;
|
||||
const total = this.group.childrenElement.length;
|
||||
// Get all task elements
|
||||
const allTasks = this.group.childrenElement;
|
||||
let checked = 0;
|
||||
let total = allTasks.length;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
this.numberEl?.detach();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +1,11 @@
|
|||
import { App, Editor, Plugin, PluginSettingTab, Setting } from "obsidian";
|
||||
import { taskProgressBarExtension } from "./widget";
|
||||
import { updateProgressBarInElement } from "./readModeWidget";
|
||||
|
||||
interface TaskProgressBarSettings {
|
||||
addTaskProgressBarToHeading: boolean;
|
||||
addNumberToProgressBar: boolean;
|
||||
showPercentage: boolean;
|
||||
allowAlternateTaskStatus: boolean;
|
||||
alternativeMarks: string;
|
||||
countSubLevel: boolean;
|
||||
hideProgressBarBasedOnConditions: boolean;
|
||||
hideProgressBarTags: string;
|
||||
hideProgressBarFolders: string;
|
||||
hideProgressBarMetadata: string;
|
||||
}
|
||||
|
||||
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",
|
||||
};
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
TaskProgressBarSettings,
|
||||
TaskProgressBarSettingTab,
|
||||
} from "./taskProgressBarSetting";
|
||||
|
||||
export default class TaskProgressBarPlugin extends Plugin {
|
||||
settings: TaskProgressBarSettings;
|
||||
|
|
@ -59,212 +38,3 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
class TaskProgressBarSettingTab extends PluginSettingTab {
|
||||
plugin: TaskProgressBarPlugin;
|
||||
private applyDebounceTimer: number = 0;
|
||||
|
||||
constructor(app: App, plugin: TaskProgressBarPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
applySettingsUpdate() {
|
||||
clearTimeout(this.applyDebounceTimer);
|
||||
const plugin = this.plugin;
|
||||
this.applyDebounceTimer = window.setTimeout(() => {
|
||||
plugin.saveSettings();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl("h2", { text: "📍 Task Progress Bar" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Add progress bar to Heading")
|
||||
.setDesc(
|
||||
"Toggle this to allow this plugin to add progress bar for Task below the headings."
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.addTaskProgressBarToHeading)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.addTaskProgressBarToHeading =
|
||||
value;
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
this.showNumberToProgressbar();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Count sub children level of current Task")
|
||||
.setDesc("Toggle this to allow this plugin to count sub tasks.")
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.countSubLevel)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.countSubLevel = value;
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Allow alternate task status")
|
||||
.setDesc(
|
||||
"Toggle this to allow this plugin to treat different tasks mark as completed or uncompleted tasks."
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.allowAlternateTaskStatus)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.allowAlternateTaskStatus = value;
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
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("Conditional Progress Bar Display")
|
||||
.setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hide progress bars based on conditions")
|
||||
.setDesc(
|
||||
"Toggle this to enable hiding progress bars based on tags, folders, or metadata."
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(
|
||||
this.plugin.settings.hideProgressBarBasedOnConditions
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hideProgressBarBasedOnConditions =
|
||||
value;
|
||||
this.applySettingsUpdate();
|
||||
|
||||
setTimeout(() => {
|
||||
this.display();
|
||||
}, 200);
|
||||
})
|
||||
);
|
||||
|
||||
if (this.plugin.settings.hideProgressBarBasedOnConditions) {
|
||||
new Setting(containerEl)
|
||||
.setName("Hide by tags")
|
||||
.setDesc(
|
||||
'Specify tags that will hide progress bars (comma-separated, without #). Example: "no-progress-bar,hide-progress"'
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder(DEFAULT_SETTINGS.hideProgressBarTags)
|
||||
.setValue(this.plugin.settings.hideProgressBarTags)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hideProgressBarTags = value;
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hide by folders")
|
||||
.setDesc(
|
||||
'Specify folder paths that will hide progress bars (comma-separated). Example: "Daily Notes,Projects/Hidden"'
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("folder1,folder2/subfolder")
|
||||
.setValue(this.plugin.settings.hideProgressBarFolders)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hideProgressBarFolders = value;
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hide by metadata")
|
||||
.setDesc(
|
||||
'Specify frontmatter metadata that will hide progress bars. Example: "hide-progress-bar: true"'
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder(
|
||||
DEFAULT_SETTINGS.hideProgressBarMetadata
|
||||
)
|
||||
.setValue(this.plugin.settings.hideProgressBarMetadata)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hideProgressBarMetadata =
|
||||
value;
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.containerEl.createEl("h2", { text: "Say Thank You" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Donate")
|
||||
.setDesc(
|
||||
"If you like this plugin, consider donating to support continued development:"
|
||||
)
|
||||
.addButton((bt) => {
|
||||
bt.buttonEl.outerHTML = `<a href="https://www.buymeacoffee.com/boninall"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=boninall&button_colour=6495ED&font_colour=ffffff&font_family=Inter&outline_colour=000000&coffee_colour=FFDD00"></a>`;
|
||||
});
|
||||
}
|
||||
|
||||
showNumberToProgressbar() {
|
||||
new Setting(this.containerEl)
|
||||
.setName("Add number to the Progress Bar")
|
||||
.setDesc(
|
||||
"Toggle this to allow this plugin to add tasks number to progress bar."
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.addNumberToProgressBar)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.addNumberToProgressBar = value;
|
||||
this.applySettingsUpdate();
|
||||
|
||||
setTimeout(() => {
|
||||
this.display();
|
||||
}, 200);
|
||||
})
|
||||
);
|
||||
|
||||
if (this.plugin.settings.addNumberToProgressBar) {
|
||||
new Setting(this.containerEl)
|
||||
.setName("Show percentage")
|
||||
.setDesc(
|
||||
"Toggle this to allow this plugin to show percentage in the progress bar."
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.showPercentage)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.showPercentage = value;
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
303
src/taskProgressBarSetting.ts
Normal file
303
src/taskProgressBarSetting.ts
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
import { App, PluginSettingTab, Setting } from "obsidian";
|
||||
import TaskProgressBarPlugin from "./taskProgressBarIndex";
|
||||
|
||||
export interface TaskProgressBarSettings {
|
||||
addTaskProgressBarToHeading: boolean;
|
||||
addNumberToProgressBar: boolean;
|
||||
showPercentage: boolean;
|
||||
allowAlternateTaskStatus: boolean;
|
||||
alternativeMarks: string;
|
||||
countSubLevel: boolean;
|
||||
hideProgressBarBasedOnConditions: boolean;
|
||||
hideProgressBarTags: string;
|
||||
hideProgressBarFolders: string;
|
||||
hideProgressBarMetadata: string;
|
||||
excludeTaskMarks: string;
|
||||
onlyCountTaskMarks: string;
|
||||
useOnlyCountMarks: boolean;
|
||||
}
|
||||
|
||||
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",
|
||||
excludeTaskMarks: "",
|
||||
onlyCountTaskMarks: "x|X",
|
||||
useOnlyCountMarks: false,
|
||||
};
|
||||
|
||||
|
||||
export class TaskProgressBarSettingTab extends PluginSettingTab {
|
||||
plugin: TaskProgressBarPlugin;
|
||||
private applyDebounceTimer: number = 0;
|
||||
|
||||
constructor(app: App, plugin: TaskProgressBarPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
applySettingsUpdate() {
|
||||
clearTimeout(this.applyDebounceTimer);
|
||||
const plugin = this.plugin;
|
||||
this.applyDebounceTimer = window.setTimeout(() => {
|
||||
plugin.saveSettings();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl("h2", { text: "📍 Task Progress Bar" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Add progress bar to Heading")
|
||||
.setDesc(
|
||||
"Toggle this to allow this plugin to add progress bar for Task below the headings."
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.addTaskProgressBarToHeading)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.addTaskProgressBarToHeading =
|
||||
value;
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
this.showNumberToProgressbar();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Count sub children level of current Task")
|
||||
.setDesc("Toggle this to allow this plugin to count sub tasks.")
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.countSubLevel)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.countSubLevel = value;
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Allow alternate task status")
|
||||
.setDesc(
|
||||
"Toggle this to allow this plugin to treat different tasks mark as completed or uncompleted tasks."
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.allowAlternateTaskStatus)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.allowAlternateTaskStatus = value;
|
||||
this.applySettingsUpdate();
|
||||
|
||||
setTimeout(() => {
|
||||
this.display();
|
||||
}, 200);
|
||||
})
|
||||
);
|
||||
|
||||
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("Exclude specific task markers")
|
||||
.setDesc(
|
||||
'Specify task markers to exclude from counting. Example: "?|/"'
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.excludeTaskMarks)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.excludeTaskMarks = value;
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Only count specific task markers")
|
||||
.setDesc("Toggle this to only count specific task markers")
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.useOnlyCountMarks)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.useOnlyCountMarks = value;
|
||||
this.applySettingsUpdate();
|
||||
|
||||
setTimeout(() => {
|
||||
this.display();
|
||||
}, 200);
|
||||
})
|
||||
);
|
||||
|
||||
if (this.plugin.settings.useOnlyCountMarks) {
|
||||
new Setting(containerEl)
|
||||
.setName("Specific task markers to count")
|
||||
.setDesc(
|
||||
'Specify which task markers to count. Example: "x|X|>"'
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder(DEFAULT_SETTINGS.onlyCountTaskMarks)
|
||||
.setValue(this.plugin.settings.onlyCountTaskMarks)
|
||||
.onChange(async (value) => {
|
||||
if (value.length === 0) {
|
||||
this.plugin.settings.onlyCountTaskMarks =
|
||||
DEFAULT_SETTINGS.onlyCountTaskMarks;
|
||||
} else {
|
||||
this.plugin.settings.onlyCountTaskMarks = value;
|
||||
}
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Conditional Progress Bar Display")
|
||||
.setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hide progress bars based on conditions")
|
||||
.setDesc(
|
||||
"Toggle this to enable hiding progress bars based on tags, folders, or metadata."
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(
|
||||
this.plugin.settings.hideProgressBarBasedOnConditions
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hideProgressBarBasedOnConditions =
|
||||
value;
|
||||
this.applySettingsUpdate();
|
||||
|
||||
setTimeout(() => {
|
||||
this.display();
|
||||
}, 200);
|
||||
})
|
||||
);
|
||||
|
||||
if (this.plugin.settings.hideProgressBarBasedOnConditions) {
|
||||
new Setting(containerEl)
|
||||
.setName("Hide by tags")
|
||||
.setDesc(
|
||||
'Specify tags that will hide progress bars (comma-separated, without #). Example: "no-progress-bar,hide-progress"'
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder(DEFAULT_SETTINGS.hideProgressBarTags)
|
||||
.setValue(this.plugin.settings.hideProgressBarTags)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hideProgressBarTags = value;
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hide by folders")
|
||||
.setDesc(
|
||||
'Specify folder paths that will hide progress bars (comma-separated). Example: "Daily Notes,Projects/Hidden"'
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("folder1,folder2/subfolder")
|
||||
.setValue(this.plugin.settings.hideProgressBarFolders)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hideProgressBarFolders = value;
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Hide by metadata")
|
||||
.setDesc(
|
||||
'Specify frontmatter metadata that will hide progress bars. Example: "hide-progress-bar: true"'
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder(
|
||||
DEFAULT_SETTINGS.hideProgressBarMetadata
|
||||
)
|
||||
.setValue(this.plugin.settings.hideProgressBarMetadata)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hideProgressBarMetadata =
|
||||
value;
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.containerEl.createEl("h2", { text: "Say Thank You" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Donate")
|
||||
.setDesc(
|
||||
"If you like this plugin, consider donating to support continued development:"
|
||||
)
|
||||
.addButton((bt) => {
|
||||
bt.buttonEl.outerHTML = `<a href="https://www.buymeacoffee.com/boninall"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=boninall&button_colour=6495ED&font_colour=ffffff&font_family=Inter&outline_colour=000000&coffee_colour=FFDD00"></a>`;
|
||||
});
|
||||
}
|
||||
|
||||
showNumberToProgressbar() {
|
||||
new Setting(this.containerEl)
|
||||
.setName("Add number to the Progress Bar")
|
||||
.setDesc(
|
||||
"Toggle this to allow this plugin to add tasks number to progress bar."
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.addNumberToProgressBar)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.addNumberToProgressBar = value;
|
||||
this.applySettingsUpdate();
|
||||
|
||||
setTimeout(() => {
|
||||
this.display();
|
||||
}, 200);
|
||||
})
|
||||
);
|
||||
|
||||
if (this.plugin.settings.addNumberToProgressBar) {
|
||||
new Setting(this.containerEl)
|
||||
.setName("Show percentage")
|
||||
.setDesc(
|
||||
"Toggle this to allow this plugin to show percentage in the progress bar."
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.showPercentage)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.showPercentage = value;
|
||||
this.applySettingsUpdate();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
667
src/widget.ts
667
src/widget.ts
|
|
@ -8,20 +8,23 @@ import {
|
|||
} from "@codemirror/view";
|
||||
import { SearchCursor } from "@codemirror/search";
|
||||
import { App, editorInfoField, MarkdownView, TFile } from "obsidian";
|
||||
import { EditorState, Range } from "@codemirror/state";
|
||||
// @ts-ignore
|
||||
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 { shouldHideProgressBarInLivePriview } from "./utils";
|
||||
|
||||
interface tasks {
|
||||
interface Tasks {
|
||||
completed: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Type to represent a text range for safe access
|
||||
interface TextRange {
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
class TaskProgressBarWidget extends WidgetType {
|
||||
progressBarEl: HTMLSpanElement;
|
||||
|
|
@ -99,12 +102,19 @@ class TaskProgressBarWidget extends WidgetType {
|
|||
? `${Math.round((this.completed / this.total) * 10000) / 100}%`
|
||||
: `[${this.completed}/${this.total}]`;
|
||||
|
||||
this.numberEl = this.progressBarEl.createEl("div", {
|
||||
cls: "progress-status",
|
||||
text: text,
|
||||
});
|
||||
if (!this.numberEl) {
|
||||
this.numberEl = this.progressBarEl.createEl("div", {
|
||||
cls: "progress-status",
|
||||
text: text,
|
||||
});
|
||||
} else {
|
||||
this.numberEl.innerText = text;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.numberEl) {
|
||||
this.numberEl.innerText = `[${this.completed}/${this.total}]`;
|
||||
}
|
||||
this.numberEl.innerText = `[${this.completed}/${this.total}]`;
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
|
|
@ -151,20 +161,22 @@ class TaskProgressBarWidget extends WidgetType {
|
|||
}
|
||||
}
|
||||
|
||||
export function taskProgressBarExtension(app: App, plugin: TaskProgressBarPlugin) {
|
||||
export function taskProgressBarExtension(
|
||||
app: App,
|
||||
plugin: TaskProgressBarPlugin
|
||||
) {
|
||||
return ViewPlugin.fromClass(
|
||||
class {
|
||||
progressDecorations: DecorationSet = Decoration.none;
|
||||
|
||||
constructor(public view: EditorView) {
|
||||
|
||||
let {progress} = this.getDeco(view);
|
||||
let { progress } = this.getDeco(view);
|
||||
this.progressDecorations = progress;
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
let {progress} = this.getDeco(update.view);
|
||||
let { progress } = this.getDeco(update.view);
|
||||
this.progressDecorations = progress;
|
||||
}
|
||||
}
|
||||
|
|
@ -172,8 +184,7 @@ export function taskProgressBarExtension(app: App, plugin: TaskProgressBarPlugin
|
|||
getDeco(view: EditorView): {
|
||||
progress: DecorationSet;
|
||||
} {
|
||||
let {state} = view,
|
||||
// @ts-ignore
|
||||
let { state } = view,
|
||||
progressDecos: Range<Decoration>[] = [];
|
||||
|
||||
// Check if progress bars should be hidden based on settings
|
||||
|
|
@ -182,100 +193,231 @@ export function taskProgressBarExtension(app: App, plugin: TaskProgressBarPlugin
|
|||
progress: Decoration.none,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
for (let part of view.visibleRanges) {
|
||||
let taskBulletCursor: RegExpCursor | SearchCursor;
|
||||
let headingCursor: RegExpCursor | SearchCursor;
|
||||
try {
|
||||
taskBulletCursor = new RegExpCursor(state.doc, "^[\\t|\\s]*([-*+]|\\d+\\.)\\s\\[(.)\\]", {}, part.from, part.to);
|
||||
taskBulletCursor = new RegExpCursor(
|
||||
state.doc,
|
||||
"^[\\t|\\s]*([-*+]|\\d+\\.)\\s\\[(.)\\]",
|
||||
{},
|
||||
part.from,
|
||||
part.to
|
||||
);
|
||||
} catch (err) {
|
||||
console.debug(err);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process headings if enabled in settings
|
||||
if (plugin?.settings.addTaskProgressBarToHeading) {
|
||||
try {
|
||||
headingCursor = new RegExpCursor(state.doc, "^(#){1,6} ", {}, part.from, part.to);
|
||||
headingCursor = new RegExpCursor(
|
||||
state.doc,
|
||||
"^(#){1,6} ",
|
||||
{},
|
||||
part.from,
|
||||
part.to
|
||||
);
|
||||
} catch (err) {
|
||||
console.debug(err);
|
||||
continue;
|
||||
}
|
||||
// Showing task progress bar near heading items.
|
||||
while (!headingCursor.next().done) {
|
||||
let {from, to} = headingCursor.value;
|
||||
const headingLine = this.view.state.doc.lineAt(from);
|
||||
// @ts-ignore
|
||||
const range = this.calculateRangeForTransform(this.view.state, headingLine.from);
|
||||
|
||||
if (!range) continue;
|
||||
let tasksNum;
|
||||
// @ts-ignore
|
||||
if (this.view.state.doc.slice(range.from, range.to).text === undefined && this.view.state.doc.slice(range.from, range.to).children?.length > 0) {
|
||||
let allChildrenText: string[] = [];
|
||||
// @ts-ignore
|
||||
for (let i = 0; i < this.view.state.doc.slice(range.from, range.to).children?.length; i++) {
|
||||
// @ts-ignore
|
||||
allChildrenText = allChildrenText.concat(this.view.state.doc.slice(range.from, range.to).children[i].text);
|
||||
}
|
||||
tasksNum = this.calculateTasksNum(allChildrenText, false);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
tasksNum = this.calculateTasksNum(this.view.state.doc.slice(range.from, range.to).text, false);
|
||||
}
|
||||
if (tasksNum?.total === 0) continue;
|
||||
let startDeco = Decoration.widget({widget: new TaskProgressBarWidget(app, plugin, view, headingLine.to, headingLine.to, tasksNum.completed, tasksNum.total)});
|
||||
progressDecos.push(startDeco.range(headingLine.to, headingLine.to));
|
||||
}
|
||||
// Process headings
|
||||
this.processHeadings(
|
||||
headingCursor,
|
||||
progressDecos,
|
||||
view
|
||||
);
|
||||
}
|
||||
// Showing task progress bar near bullet items.
|
||||
while (!taskBulletCursor.next().done) {
|
||||
let {from} = taskBulletCursor.value;
|
||||
const linePos = view.state.doc.lineAt(from)?.from;
|
||||
|
||||
// Don't parse any tasks in code blocks or frontmatter
|
||||
// @ts-ignore
|
||||
let syntaxNode = syntaxTree(view.state).resolveInner(linePos + 1),
|
||||
// @ts-ignore
|
||||
nodeProps: string = syntaxNode.type.prop(tokenClassNodeProp),
|
||||
excludedSection = ["hmd-codeblock", "hmd-frontmatter"].find(token =>
|
||||
nodeProps?.split(" ").includes(token)
|
||||
);
|
||||
if (excludedSection) continue;
|
||||
const line = this.view.state.doc.lineAt(linePos);
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
if (!(/^[\s|\t]*([-*+]|\d+\.)\s\[(.)\]/.test(this.view.state.doc.slice(line.from, line.to).text))) return;
|
||||
// @ts-ignore
|
||||
const range = this.calculateRangeForTransform(this.view.state, line.to);
|
||||
if (!range) continue;
|
||||
let tasksNum;
|
||||
// @ts-ignore
|
||||
if ((this.view.state.doc.slice(range.from, range.to).text?.length === 1)) continue;
|
||||
// @ts-ignore
|
||||
if (this.view.state.doc.slice(range.from, range.to).text === undefined && this.view.state.doc.slice(range.from, range.to).children?.length !== undefined) {
|
||||
let allChildrenText: string[] = [];
|
||||
// @ts-ignore
|
||||
for (let i = 0; i < this.view.state.doc.slice(range.from, range.to).children?.length; i++) {
|
||||
// @ts-ignore
|
||||
allChildrenText = allChildrenText.concat(this.view.state.doc.slice(range.from, range.to).children[i].text);
|
||||
}
|
||||
tasksNum = this.calculateTasksNum(allChildrenText, true);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
tasksNum = this.calculateTasksNum(this.view.state.doc.slice(range.from, range.to).text, true);
|
||||
}
|
||||
if (tasksNum.total === 0) continue;
|
||||
let startDeco = Decoration.widget({widget: new TaskProgressBarWidget(app, plugin, view, line.to, line.to, tasksNum.completed, tasksNum.total)});
|
||||
progressDecos.push(startDeco.range(line.to, line.to));
|
||||
}
|
||||
// Process task bullets
|
||||
this.processBullets(taskBulletCursor, progressDecos, view);
|
||||
}
|
||||
|
||||
return {
|
||||
progress: Decoration.set(progressDecos.sort((a, b) => a.from - b.from)),
|
||||
progress: Decoration.set(
|
||||
progressDecos.sort((a, b) => a.from - b.from)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Process heading matches and add decorations
|
||||
*/
|
||||
private processHeadings(
|
||||
cursor: RegExpCursor | SearchCursor,
|
||||
decorations: Range<Decoration>[],
|
||||
view: EditorView
|
||||
) {
|
||||
while (!cursor.next().done) {
|
||||
let { from, to } = cursor.value;
|
||||
const headingLine = view.state.doc.lineAt(from);
|
||||
|
||||
public calculateRangeForTransform(state: EditorState, pos: number) {
|
||||
const range = this.calculateRangeForTransform(
|
||||
view.state,
|
||||
headingLine.from
|
||||
);
|
||||
|
||||
if (!range) continue;
|
||||
|
||||
const tasksNum = this.extractTasksFromRange(
|
||||
range,
|
||||
view.state,
|
||||
false
|
||||
);
|
||||
|
||||
if (tasksNum.total === 0) continue;
|
||||
|
||||
let startDeco = Decoration.widget({
|
||||
widget: new TaskProgressBarWidget(
|
||||
app,
|
||||
plugin,
|
||||
view,
|
||||
headingLine.to,
|
||||
headingLine.to,
|
||||
tasksNum.completed,
|
||||
tasksNum.total
|
||||
),
|
||||
});
|
||||
|
||||
decorations.push(
|
||||
startDeco.range(headingLine.to, headingLine.to)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process bullet matches and add decorations
|
||||
*/
|
||||
private processBullets(
|
||||
cursor: RegExpCursor | SearchCursor,
|
||||
decorations: Range<Decoration>[],
|
||||
view: EditorView
|
||||
) {
|
||||
while (!cursor.next().done) {
|
||||
let { from } = cursor.value;
|
||||
const linePos = view.state.doc.lineAt(from)?.from;
|
||||
|
||||
// Don't parse any tasks in code blocks or frontmatter
|
||||
const syntaxNode = syntaxTree(view.state).resolveInner(
|
||||
linePos + 1
|
||||
);
|
||||
const nodeProps = syntaxNode.type.prop(tokenClassNodeProp);
|
||||
const excludedSection = [
|
||||
"hmd-codeblock",
|
||||
"hmd-frontmatter",
|
||||
].find((token) => nodeProps?.split(" ").includes(token));
|
||||
|
||||
if (excludedSection) continue;
|
||||
|
||||
const line = view.state.doc.lineAt(linePos);
|
||||
|
||||
// Check if line is a task
|
||||
const lineText = this.getDocumentText(
|
||||
view.state.doc,
|
||||
line.from,
|
||||
line.to
|
||||
);
|
||||
if (
|
||||
!lineText ||
|
||||
!/^[\s|\t]*([-*+]|\d+\.)\s\[(.)\]/.test(lineText)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const range = this.calculateRangeForTransform(
|
||||
view.state,
|
||||
line.to
|
||||
);
|
||||
|
||||
if (!range) continue;
|
||||
|
||||
const rangeText = this.getDocumentText(
|
||||
view.state.doc,
|
||||
range.from,
|
||||
range.to
|
||||
);
|
||||
if (!rangeText || rangeText.length === 1) continue;
|
||||
|
||||
const tasksNum = this.extractTasksFromRange(
|
||||
range,
|
||||
view.state,
|
||||
true
|
||||
);
|
||||
|
||||
if (tasksNum.total === 0) continue;
|
||||
|
||||
let startDeco = Decoration.widget({
|
||||
widget: new TaskProgressBarWidget(
|
||||
app,
|
||||
plugin,
|
||||
view,
|
||||
line.to,
|
||||
line.to,
|
||||
tasksNum.completed,
|
||||
tasksNum.total
|
||||
),
|
||||
});
|
||||
|
||||
decorations.push(startDeco.range(line.to, line.to));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tasks count from a document range
|
||||
*/
|
||||
private extractTasksFromRange(
|
||||
range: TextRange,
|
||||
state: EditorState,
|
||||
isBullet: boolean
|
||||
): Tasks {
|
||||
const textArray = this.getDocumentTextArray(
|
||||
state.doc,
|
||||
range.from,
|
||||
range.to
|
||||
);
|
||||
return this.calculateTasksNum(textArray, isBullet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely extract text from a document range
|
||||
*/
|
||||
private getDocumentText(
|
||||
doc: Text,
|
||||
from: number,
|
||||
to: number
|
||||
): string | null {
|
||||
try {
|
||||
return doc.sliceString(from, to);
|
||||
} catch (e) {
|
||||
console.error("Error getting document text:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of text lines from a document range
|
||||
*/
|
||||
private getDocumentTextArray(
|
||||
doc: Text,
|
||||
from: number,
|
||||
to: number
|
||||
): string[] {
|
||||
const text = this.getDocumentText(doc, from, to);
|
||||
if (!text) return [];
|
||||
return text.split("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the foldable range for a position
|
||||
*/
|
||||
public calculateRangeForTransform(
|
||||
state: EditorState,
|
||||
pos: number
|
||||
): TextRange | null {
|
||||
const line = state.doc.lineAt(pos);
|
||||
const foldRange = foldable(state, line.from, line.to);
|
||||
|
||||
|
|
@ -283,67 +425,326 @@ export function taskProgressBarExtension(app: App, plugin: TaskProgressBarPlugin
|
|||
return null;
|
||||
}
|
||||
|
||||
return {from: line.from, to: foldRange.to};
|
||||
return { from: line.from, to: foldRange.to };
|
||||
}
|
||||
|
||||
public calculateTasksNum(textArray: string[], bullet: boolean): tasks {
|
||||
/**
|
||||
* Create regex for counting total tasks
|
||||
*/
|
||||
private createTotalTaskRegex(
|
||||
isHeading: boolean,
|
||||
level: number = 0,
|
||||
tabSize: number = 4
|
||||
): RegExp {
|
||||
// 获取排除的任务标记
|
||||
const excludePattern = plugin?.settings.excludeTaskMarks || "";
|
||||
|
||||
// 构建识别任务标记的正则表达式部分
|
||||
let markPattern = "\\[(.)\\]";
|
||||
|
||||
// 如果存在排除的标记,修改标记匹配模式
|
||||
if (excludePattern && excludePattern.length > 0) {
|
||||
// 构建一个不匹配被排除标记的模式
|
||||
const excludeChars = excludePattern
|
||||
.split("")
|
||||
.map((c) => "\\" + c)
|
||||
.join("");
|
||||
markPattern = `\\[([^${excludeChars}])\\]`;
|
||||
}
|
||||
|
||||
if (isHeading) {
|
||||
return new RegExp(`^([-*+]|\\d+\\.)\\s${markPattern}`);
|
||||
} else {
|
||||
// 如果是子级计数模式,使用更宽松的正则表达式匹配任何缩进级别的任务
|
||||
if (plugin?.settings.countSubLevel) {
|
||||
return new RegExp(
|
||||
`^[\\t|\\s]*?([-*+]|\\d+\\.)\\s${markPattern}`
|
||||
);
|
||||
} else {
|
||||
// 否则使用精确缩进级别匹配
|
||||
return new RegExp(
|
||||
`^[\\t|\\s]{${
|
||||
tabSize * (level + 1)
|
||||
}}([-*+]|\\d+\\.)\\s${markPattern}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create regex for matching completed tasks
|
||||
*/
|
||||
private createCompletedTaskRegex(
|
||||
plugin: TaskProgressBarPlugin,
|
||||
isHeading: boolean,
|
||||
level: number = 0,
|
||||
tabSize: number = 4
|
||||
): RegExp {
|
||||
// Extract settings
|
||||
const useOnlyCountMarks = plugin?.settings.useOnlyCountMarks;
|
||||
const onlyCountPattern =
|
||||
plugin?.settings.onlyCountTaskMarks || "x|X";
|
||||
const excludePattern = plugin?.settings.excludeTaskMarks || "";
|
||||
const alternativeMarks = plugin?.settings.alternativeMarks;
|
||||
|
||||
// Default patterns - 对子级计数的情况调整缩进匹配
|
||||
const basePattern = isHeading
|
||||
? "^([-*+]|\\d+\\.)\\s"
|
||||
: plugin?.settings.countSubLevel
|
||||
? "^[\\t|\\s]*?" // 如果是子级计数,使用非贪婪模式匹配任意缩进
|
||||
: "^[\\t|\\s]+";
|
||||
|
||||
const bulletPrefix = isHeading
|
||||
? ""
|
||||
: plugin?.settings.countSubLevel
|
||||
? "([-*+]|\\d+\\.)\\s" // 子级计数时简化前缀
|
||||
: level !== 0
|
||||
? `{${tabSize * (level + 1)}}([-*+]|\\d+\\.)\\s`
|
||||
: "([-*+]|\\d+\\.)\\s";
|
||||
|
||||
// 如果启用了仅统计特定标记功能
|
||||
if (useOnlyCountMarks) {
|
||||
return new RegExp(
|
||||
basePattern +
|
||||
bulletPrefix +
|
||||
"\\[(" +
|
||||
onlyCountPattern +
|
||||
")\\]"
|
||||
);
|
||||
}
|
||||
|
||||
// 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 - 如果没有启用特殊选项
|
||||
if (excludePattern) {
|
||||
return new RegExp(
|
||||
basePattern +
|
||||
bulletPrefix +
|
||||
"\\[[^ " +
|
||||
excludePattern +
|
||||
"]\\]"
|
||||
);
|
||||
} else {
|
||||
return new RegExp(
|
||||
basePattern + bulletPrefix + "\\[[^ ]\\]"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果启用了替代标记
|
||||
if (
|
||||
plugin?.settings.allowAlternateTaskStatus &&
|
||||
plugin?.settings.alternativeMarks
|
||||
) {
|
||||
const alternativeMarks = plugin?.settings.alternativeMarks
|
||||
.replace(/[()]/g, "")
|
||||
.split("|");
|
||||
const markMatch = text.match(/\[(.)]/);
|
||||
if (markMatch && markMatch[1]) {
|
||||
const mark = markMatch[1];
|
||||
return alternativeMarks.includes(mark);
|
||||
}
|
||||
}
|
||||
|
||||
// 标准检查 - 非空格的字符
|
||||
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 !== " ";
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a task marker should be excluded from counting
|
||||
*/
|
||||
private shouldExcludeTask(text: string): boolean {
|
||||
// 如果没有排除设置,返回false
|
||||
if (
|
||||
!plugin?.settings.excludeTaskMarks ||
|
||||
plugin.settings.excludeTaskMarks.length === 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查任务标记是否在排除列表中
|
||||
const taskMarkMatch = text.match(/\[(.)]/);
|
||||
if (taskMarkMatch && taskMarkMatch[1]) {
|
||||
const taskMark = taskMarkMatch[1];
|
||||
return plugin.settings.excludeTaskMarks.includes(taskMark);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tab size from vault configuration
|
||||
*/
|
||||
private getTabSize(): number {
|
||||
try {
|
||||
const vaultConfig = app.vault as any;
|
||||
const useTab =
|
||||
vaultConfig.getConfig?.("useTab") === undefined ||
|
||||
vaultConfig.getConfig?.("useTab") === true;
|
||||
return useTab
|
||||
? (vaultConfig.getConfig?.("tabSize") || 4) / 4
|
||||
: vaultConfig.getConfig?.("tabSize") || 4;
|
||||
} catch (e) {
|
||||
console.error("Error getting tab size:", e);
|
||||
return 4; // Default tab size
|
||||
}
|
||||
}
|
||||
|
||||
public calculateTasksNum(
|
||||
textArray: string[],
|
||||
bullet: boolean
|
||||
): Tasks {
|
||||
if (!textArray || textArray.length === 0) {
|
||||
return { completed: 0, total: 0 };
|
||||
}
|
||||
|
||||
let completed: number = 0;
|
||||
let total: number = 0;
|
||||
let level: number = 0;
|
||||
if (!textArray) return {completed: 0, total: 0};
|
||||
// @ts-ignore
|
||||
const useTab = (app.vault.getConfig("useTab") === undefined || app.vault.getConfig("useTab") === true);
|
||||
// @ts-ignore
|
||||
const tabSize = useTab ? app.vault.getConfig("tabSize") / 4 : app.vault.getConfig("tabSize");
|
||||
|
||||
let bulletCompleteRegex: RegExp = new RegExp(/^[\t|\s]+([-*+]|\d+\.)\s+\[[^ ]\]/);
|
||||
let bulletTotalRegex: RegExp = new RegExp(/^[\t|\s]+([-*+]|\d+\.)\s\[(.)\]/);
|
||||
let headingCompleteRegex: RegExp = new RegExp("([-*+]|\\d+\\.)\\s\\[[^ ]\\]");
|
||||
let headingTotalRegex: RegExp = new RegExp("([-*+]|\\d+\\.)\\s\\[(.)\\]");
|
||||
if (!plugin?.settings.countSubLevel && bullet) {
|
||||
// @ts-ignore
|
||||
level = textArray[0].match(/^[\s|\t]*/)[0].length / tabSize;
|
||||
// Total regex based on indent level
|
||||
bulletTotalRegex = new RegExp("^[\\t|\\s]{" + (tabSize * (level + 1)) + "}([-*+]|\\d+\\.)\\s\\[(.)\\]");
|
||||
bulletCompleteRegex = new RegExp("^[\\t|\\s]{" + (tabSize * (level + 1)) + "}([-*+]|\\d+\\.)\\s\\[[^ ]\\]");
|
||||
}
|
||||
if (plugin?.settings.countSubLevel && !bullet) {
|
||||
level = 0;
|
||||
headingTotalRegex = new RegExp("^([-*+]|\\d+\\.)\\s\\[(.)\\]");
|
||||
headingCompleteRegex = new RegExp("^([-*+]|\\d+\\.)\\s\\[[^ ]\\]");
|
||||
}
|
||||
if (plugin?.settings.alternativeMarks.length > 0 && plugin?.settings.allowAlternateTaskStatus) {
|
||||
const lengText = !plugin?.settings.countSubLevel && bullet ? `{${(tabSize * (level + 1))}}` : "";
|
||||
// Get tab size from vault config
|
||||
const tabSize = this.getTabSize();
|
||||
|
||||
bulletCompleteRegex = level !== 0 ? new RegExp("^[\\t|\\s]" + `${lengText}` + "([-*+]|\\d+\\.)\\s\\[" + plugin?.settings.alternativeMarks + "\\]") : (new RegExp("[\\t|\\s]" + `${lengText}` + "([-*+]|\\d+\\.)\\s\\[" + plugin?.settings.alternativeMarks + "\\]"));
|
||||
if (plugin?.settings.addTaskProgressBarToHeading) {
|
||||
headingCompleteRegex = level !== 0 ? new RegExp("^([-*+]|\\d+\\.)\\s+\\[" + plugin?.settings.alternativeMarks + "\\]") : new RegExp("([-*+]|\\d+\\.)\\s+\\[" + plugin?.settings.alternativeMarks + "\\]");
|
||||
// Determine indentation level for bullets
|
||||
if (!plugin?.settings.countSubLevel && bullet && textArray[0]) {
|
||||
const indentMatch = textArray[0].match(/^[\s|\t]*/);
|
||||
if (indentMatch) {
|
||||
level = indentMatch[0].length / tabSize;
|
||||
}
|
||||
}
|
||||
|
||||
// Create regexes based on settings and context
|
||||
const bulletTotalRegex = this.createTotalTaskRegex(
|
||||
false,
|
||||
level,
|
||||
tabSize
|
||||
);
|
||||
const bulletCompleteRegex = this.createCompletedTaskRegex(
|
||||
plugin,
|
||||
false,
|
||||
level,
|
||||
tabSize
|
||||
);
|
||||
const headingTotalRegex = this.createTotalTaskRegex(true);
|
||||
const headingCompleteRegex = this.createCompletedTaskRegex(
|
||||
plugin,
|
||||
true
|
||||
);
|
||||
|
||||
// Count tasks
|
||||
for (let i = 0; i < textArray.length; i++) {
|
||||
if (i === 0) {
|
||||
continue;
|
||||
}
|
||||
if (bullet) {
|
||||
if (textArray[i].match(bulletTotalRegex)) total++;
|
||||
if (textArray[i].match(bulletCompleteRegex)) completed++;
|
||||
continue;
|
||||
}
|
||||
if (plugin?.settings.addTaskProgressBarToHeading && !bullet) {
|
||||
if (textArray[i].match(headingTotalRegex)) total++;
|
||||
if (textArray[i].match(headingCompleteRegex)) completed++;
|
||||
}
|
||||
if (i === 0) continue; // Skip the first line
|
||||
|
||||
if (bullet) {
|
||||
// 确保每个任务只被计算一次
|
||||
const lineText = textArray[i].trim();
|
||||
// 首先检查是否匹配任务格式,然后检查是否是应该被排除的任务类型
|
||||
if (
|
||||
lineText &&
|
||||
lineText.match(bulletTotalRegex) &&
|
||||
!this.shouldExcludeTask(lineText)
|
||||
) {
|
||||
total++;
|
||||
// 使用新方法判断是否为已完成任务
|
||||
if (this.isCompletedTask(lineText)) {
|
||||
completed++;
|
||||
}
|
||||
}
|
||||
} else if (plugin?.settings.addTaskProgressBarToHeading) {
|
||||
const lineText = textArray[i].trim();
|
||||
// 同样使用shouldExcludeTask函数进行额外的验证
|
||||
if (
|
||||
lineText &&
|
||||
lineText.match(headingTotalRegex) &&
|
||||
!this.shouldExcludeTask(lineText)
|
||||
) {
|
||||
total++;
|
||||
// 使用新方法判断是否为已完成任务
|
||||
if (this.isCompletedTask(lineText)) {
|
||||
completed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {completed: completed, total: total};
|
||||
};
|
||||
|
||||
// 确保完成数不超过总数
|
||||
completed = Math.min(completed, total);
|
||||
|
||||
return { completed, total };
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: plugin => [
|
||||
// these are separated out so that we can set decoration priority
|
||||
// it's also much easier to sort the decorations when they're grouped
|
||||
EditorView.decorations.of(v => v.plugin(plugin)?.progressDecorations || Decoration.none),
|
||||
provide: (plugin) => [
|
||||
EditorView.decorations.of(
|
||||
(v) =>
|
||||
v.plugin(plugin)?.progressDecorations || Decoration.none
|
||||
),
|
||||
],
|
||||
}
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue