feat: support new progressbar

This commit is contained in:
quorafind 2025-04-06 17:28:18 +08:00
parent d5af8b94ff
commit 3b6e70f98d
6 changed files with 486 additions and 135 deletions

0
design/task-view.md Normal file
View file

View file

@ -3,13 +3,24 @@ import { TaskFilterOptions } from "../editor-ext/filterTasks";
import { t } from "../translations/helper";
export interface TaskProgressBarSettings {
showProgressBar: boolean;
progressBarDisplayMode: "graphical" | "text" | "both" | "none";
addTaskProgressBarToHeading: boolean;
addProgressBarToNonTaskBullet: boolean;
enableHeadingProgressBar: boolean;
addNumberToProgressBar: boolean;
showPercentage: boolean;
// Progress text display options
displayMode?:
| "percentage"
| "bracketPercentage"
| "fraction"
| "bracketFraction"
| "detailed"
| "custom"
| "range-based";
customFormat?: string;
progressRanges: Array<{
min: number;
max: number;
@ -110,7 +121,7 @@ export interface TaskProgressBarSettings {
}
export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
showProgressBar: false,
progressBarDisplayMode: "both",
addTaskProgressBarToHeading: false,
addProgressBarToNonTaskBullet: false,
enableHeadingProgressBar: false,
@ -125,6 +136,10 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
hideProgressBarFolders: "",
hideProgressBarMetadata: "hide-progress-bar",
// Progress text display options
displayMode: "bracketFraction",
customFormat: "[{{COMPLETED}}/{{TOTAL}}]",
// Default task statuses
taskStatuses: {
completed: "x|X",

View file

@ -34,6 +34,212 @@ export interface HTMLElementWithView extends HTMLElement {
view: EditorView;
}
export interface ProgressData {
completed: number;
total: number;
inProgress?: number;
abandoned?: number;
notStarted?: number;
planned?: number;
}
/**
* Format the progress text according to settings and data
* Supports various display modes including fraction, percentage, and custom formats
*/
export function formatProgressText(
data: ProgressData,
plugin: TaskProgressBarPlugin
): string {
if (!data.total) return "";
// Calculate percentages
const completedPercentage =
Math.round((data.completed / data.total) * 10000) / 100;
const inProgressPercentage = data.inProgress
? Math.round((data.inProgress / data.total) * 10000) / 100
: 0;
const abandonedPercentage = data.abandoned
? Math.round((data.abandoned / data.total) * 10000) / 100
: 0;
const plannedPercentage = data.planned
? Math.round((data.planned / data.total) * 10000) / 100
: 0;
// Create a full data object with percentages for expression evaluation
const fullData = {
...data,
percentages: {
completed: completedPercentage,
inProgress: inProgressPercentage,
abandoned: abandonedPercentage,
planned: plannedPercentage,
notStarted: data.notStarted
? Math.round((data.notStarted / data.total) * 10000) / 100
: 0,
},
};
// Get status symbols
const completedSymbol = "✓";
const inProgressSymbol = "⟳";
const abandonedSymbol = "✗";
const plannedSymbol = "?";
// Get display mode from settings, with fallbacks for backwards compatibility
const displayMode =
plugin?.settings.displayMode ||
(plugin?.settings.progressBarDisplayMode === "text" ||
plugin?.settings.progressBarDisplayMode === "both"
? plugin?.settings.showPercentage
? "percentage"
: "bracketFraction"
: "bracketFraction");
// Process text with template formatting
let resultText = "";
// Handle different display modes
switch (displayMode) {
case "percentage":
// Simple percentage (e.g., "75%")
resultText = `${completedPercentage}%`;
break;
case "bracketPercentage":
// Percentage with brackets (e.g., "[75%]")
resultText = `[${completedPercentage}%]`;
break;
case "fraction":
// Simple fraction (e.g., "3/4")
resultText = `${data.completed}/${data.total}`;
break;
case "bracketFraction":
// Fraction with brackets (e.g., "[3/4]")
resultText = `[${data.completed}/${data.total}]`;
break;
case "detailed":
// Detailed format showing all task statuses
resultText = `[${data.completed}${completedSymbol} ${
data.inProgress || 0
}${inProgressSymbol} ${data.abandoned || 0}${abandonedSymbol} ${
data.planned || 0
}${plannedSymbol} / ${data.total}]`;
break;
case "custom":
// Handle custom format if available in settings
if (plugin?.settings.customFormat) {
resultText = plugin.settings.customFormat
.replace(/{{COMPLETED}}/g, data.completed.toString())
.replace(/{{TOTAL}}/g, data.total.toString())
.replace(
/{{IN_PROGRESS}}/g,
(data.inProgress || 0).toString()
)
.replace(/{{ABANDONED}}/g, (data.abandoned || 0).toString())
.replace(/{{PLANNED}}/g, (data.planned || 0).toString())
.replace(
/{{NOT_STARTED}}/g,
(data.notStarted || 0).toString()
)
.replace(/{{PERCENT}}/g, completedPercentage.toString())
.replace(/{{PROGRESS}}/g, completedPercentage.toString())
.replace(
/{{PERCENT_IN_PROGRESS}}/g,
inProgressPercentage.toString()
)
.replace(
/{{PERCENT_ABANDONED}}/g,
abandonedPercentage.toString()
)
.replace(
/{{PERCENT_PLANNED}}/g,
plannedPercentage.toString()
)
.replace(/{{COMPLETED_SYMBOL}}/g, completedSymbol)
.replace(/{{IN_PROGRESS_SYMBOL}}/g, inProgressSymbol)
.replace(/{{ABANDONED_SYMBOL}}/g, abandonedSymbol)
.replace(/{{PLANNED_SYMBOL}}/g, plannedSymbol);
} else {
resultText = `[${data.completed}/${data.total}]`;
}
break;
case "range-based":
// Check if custom progress ranges are enabled
if (plugin?.settings.customizeProgressRanges) {
// Find a matching range for the current percentage
const matchingRange = plugin.settings.progressRanges.find(
(range) =>
completedPercentage >= range.min &&
completedPercentage <= range.max
);
// If a matching range is found, use its custom text
if (matchingRange) {
resultText = matchingRange.text.replace(
"{{PROGRESS}}",
completedPercentage.toString()
);
} else {
resultText = `${completedPercentage}%`;
}
} else {
resultText = `${completedPercentage}%`;
}
break;
default:
// Legacy behavior for compatibility
if (
plugin?.settings.progressBarDisplayMode === "text" ||
plugin?.settings.progressBarDisplayMode === "both"
) {
// If using text mode, check if percentage is preferred
if (plugin?.settings.showPercentage) {
resultText = `${completedPercentage}%`;
} else {
// Show detailed counts if we have in-progress, abandoned, or planned tasks
if (
(data.inProgress && data.inProgress > 0) ||
(data.abandoned && data.abandoned > 0) ||
(data.planned && data.planned > 0)
) {
resultText = `[${data.completed}${
data.inProgress || 0
} ${data.abandoned || 0} ${data.planned || 0}? / ${
data.total
}]`;
} else {
// Simple fraction format with brackets
resultText = `[${data.completed}/${data.total}]`;
}
}
} else {
// Default to bracket fraction if no specific text mode is set
resultText = `[${data.completed}/${data.total}]`;
}
}
// Process JavaScript expressions enclosed in ${= }
resultText = resultText.replace(/\${=(.+?)}/g, (match, expr) => {
try {
// Create a safe function to evaluate the expression with the data context
const evalFunc = new Function("data", `return ${expr}`);
return evalFunc(fullData);
} catch (error) {
console.error("Error evaluating expression:", expr, error);
return match; // Return the original match on error
}
});
return resultText;
}
class TaskProgressBarWidget extends WidgetType {
progressBarEl: HTMLSpanElement;
progressBackGroundEl: HTMLDivElement;
@ -161,46 +367,17 @@ class TaskProgressBarWidget extends WidgetType {
changeNumber() {
if (this.plugin?.settings.addNumberToProgressBar) {
let text;
if (this.plugin?.settings.showPercentage) {
// Calculate percentage of completed tasks
const percentage =
Math.round((this.completed / this.total) * 10000) / 100;
// Check if custom progress ranges are enabled
if (this.plugin?.settings.customizeProgressRanges) {
// Find a matching range for the current percentage
const matchingRange =
this.plugin.settings.progressRanges.find(
(range) =>
percentage >= range.min &&
percentage <= range.max
);
// If a matching range is found, use its custom text
if (matchingRange) {
text = matchingRange.text.replace(
"{{PROGRESS}}",
percentage.toString()
);
} else {
text = `${percentage}%`;
}
} else {
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}]`;
}
}
let text = formatProgressText(
{
completed: this.completed,
total: this.total,
inProgress: this.inProgress,
abandoned: this.abandoned,
notStarted: this.notStarted,
planned: this.planned,
},
this.plugin
);
if (!this.numberEl) {
this.numberEl = this.progressBarEl.createEl("div", {
@ -259,84 +436,69 @@ class TaskProgressBarWidget extends WidgetType {
}
}
);
this.progressBackGroundEl = this.progressBarEl.createEl("div", {
cls: "progress-bar-inline-background",
});
this.progressBackGroundEl.toggleClass(
"hidden",
!this.plugin?.settings.showProgressBar
);
// Check if graphical progress bar should be shown
const showGraphicalBar =
this.plugin?.settings.progressBarDisplayMode === "graphical" ||
this.plugin?.settings.progressBarDisplayMode === "both";
// 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 (showGraphicalBar) {
this.progressBackGroundEl = this.progressBarEl.createEl("div", {
cls: "progress-bar-inline-background",
});
}
if (this.abandoned > 0) {
this.abandonedEl = this.progressBackGroundEl.createEl("div", {
cls: "progress-bar-inline progress-abandoned",
// Create elements for each status type
this.progressEl = this.progressBackGroundEl.createEl("div", {
cls: "progress-bar-inline progress-completed",
});
}
if (this.planned > 0) {
this.plannedEl = this.progressBackGroundEl.createEl("div", {
cls: "progress-bar-inline progress-planned",
});
}
if (this.plugin?.settings.addNumberToProgressBar && this.total) {
let text;
if (this.plugin?.settings.showPercentage) {
const percentage =
Math.round((this.completed / this.total) * 10000) / 100;
// Check if custom progress ranges are enabled
if (this.plugin?.settings.customizeProgressRanges) {
// Find a matching range for the current percentage
const matchingRange =
this.plugin.settings.progressRanges.find(
(range) =>
percentage >= range.min &&
percentage <= range.max
);
// If a matching range is found, use its custom text
if (matchingRange) {
text = matchingRange.text.replace(
"{{PROGRESS}}",
percentage.toString()
);
} else {
text = `${percentage}%`;
}
} else {
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}]`;
}
// 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",
});
}
this.changePercentage();
}
// Check if text progress should be shown (either as the only option or together with graphic bar)
const showText =
this.plugin?.settings.progressBarDisplayMode === "text" ||
this.plugin?.settings.progressBarDisplayMode === "both" ||
this.plugin?.settings.addNumberToProgressBar;
if (showText && this.total) {
const text = formatProgressText(
{
completed: this.completed,
total: this.total,
inProgress: this.inProgress,
abandoned: this.abandoned,
notStarted: this.notStarted,
planned: this.planned,
},
this.plugin
);
this.numberEl = this.progressBarEl.createEl("div", {
cls: "progress-status",
text: text,
});
}
this.changePercentage();
return this.progressBarEl;
}

View file

@ -236,18 +236,24 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
.setHeading();
new Setting(containerEl)
.setName(t("Show progress bar"))
.setDesc(t("Toggle this to show the progress bar."))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.showProgressBar)
.onChange(async (value) => {
this.plugin.settings.showProgressBar = value;
.setName(t("Progress display mode"))
.setDesc(t("Choose how to display task progress"))
.addDropdown((dropdown) =>
dropdown
.addOption("none", t("No progress indicators"))
.addOption("graphical", t("Graphical progress bar"))
.addOption("text", t("Text progress indicator"))
.addOption("both", t("Both graphical and text"))
.setValue(this.plugin.settings.progressBarDisplayMode)
.onChange(async (value: any) => {
this.plugin.settings.progressBarDisplayMode = value;
this.applySettingsUpdate();
this.display();
})
);
if (this.plugin.settings.showProgressBar) {
// Only show these options if some form of progress bar is enabled
if (this.plugin.settings.progressBarDisplayMode !== "none") {
new Setting(containerEl)
.setName(t("Support hover to show progress info"))
.setDesc(
@ -326,7 +332,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
.setName(t("Count sub children of current Task"))
.setDesc(
t(
"Toggle this to allow this plugin to count sub tasks when generating progress bar ."
"Toggle this to allow this plugin to count sub tasks when generating progress bar."
)
)
.addToggle((toggle) =>
@ -338,7 +344,13 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
})
);
this.displayNumberToProgressbar(containerEl);
// Only show the number settings for modes that include text display
if (
this.plugin.settings.progressBarDisplayMode === "text" ||
this.plugin.settings.progressBarDisplayMode === "both"
) {
this.displayNumberToProgressbar(containerEl);
}
new Setting(containerEl)
.setName(t("Hide progress bars"))
@ -434,30 +446,142 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
}
}
// Renamed from showNumberToProgressbar() to match the pattern
private displayNumberToProgressbar(containerEl: HTMLElement): void {
// Add setting for display mode
new Setting(containerEl)
.setName(t("Add number to the Progress Bar"))
.setDesc(
t(
"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;
.setName(t("Progress format"))
.setDesc(t("Choose how to display the task progress"))
.addDropdown((dropdown) => {
dropdown
.addOption("percentage", t("Percentage (75%)"))
.addOption(
"bracketPercentage",
t("Bracketed percentage ([75%])")
)
.addOption("fraction", t("Fraction (3/4)"))
.addOption(
"bracketFraction",
t("Bracketed fraction ([3/4])")
)
.addOption("detailed", t("Detailed ([3✓ 1⟳ 0✗ 1? / 5])"))
.addOption("custom", t("Custom format"))
.addOption("range-based", t("Range-based text"))
.setValue(
this.plugin.settings.displayMode || "bracketFraction"
)
.onChange(async (value: any) => {
this.plugin.settings.displayMode = value;
this.applySettingsUpdate();
})
);
this.display();
});
});
if (this.plugin.settings.addNumberToProgressBar) {
// Show custom format setting only when custom format is selected
if (this.plugin.settings.displayMode === "custom") {
new Setting(containerEl)
.setName(t("Custom format"))
.setDesc(
t(
"Use placeholders like {{COMPLETED}}, {{TOTAL}}, {{PERCENT}}, etc."
)
)
.addTextArea((text) => {
text.inputEl.toggleClass("custom-format-textarea", true);
text.setPlaceholder("[{{COMPLETED}}/{{TOTAL}}]")
.setValue(
this.plugin.settings.customFormat ||
"[{{COMPLETED}}/{{TOTAL}}]"
)
.onChange(async (value) => {
this.plugin.settings.customFormat = value;
this.applySettingsUpdate();
});
});
// Add help text for available placeholders
new Setting(containerEl)
.setName(t("Available placeholders"))
.setDesc(
t(
"Available placeholders: {{COMPLETED}}, {{TOTAL}}, {{IN_PROGRESS}}, {{ABANDONED}}, {{PLANNED}}, {{NOT_STARTED}}, {{PERCENT}}, {{COMPLETED_SYMBOL}}, {{IN_PROGRESS_SYMBOL}}, {{ABANDONED_SYMBOL}}, {{PLANNED_SYMBOL}}"
)
);
// Show examples of advanced formats using expressions
containerEl.createEl("div", {
cls: "setting-item-name",
text: t("Expression examples:"),
});
const exampleContainer = containerEl.createEl("div", {
cls: "expression-examples",
});
const examples = [
{
name: t("Text Progress Bar"),
code: '[${="=".repeat(Math.floor(data.percentages.completed/10)) + " ".repeat(10-Math.floor(data.percentages.completed/10))}] {{PERCENT}}%',
},
{
name: t("Emoji Progress Bar"),
code: '${="⬛".repeat(Math.floor(data.percentages.completed/10)) + "⬜".repeat(10-Math.floor(data.percentages.completed/10))} {{PERCENT}}%',
},
{
name: t("Color-coded Status"),
code: "{{COMPLETED}}/{{TOTAL}} ${=data.percentages.completed < 30 ? '🔴' : data.percentages.completed < 70 ? '🟠' : '🟢'}",
},
{
name: t("Status with Icons"),
code: "[{{COMPLETED_SYMBOL}}:{{COMPLETED}} {{IN_PROGRESS_SYMBOL}}:{{IN_PROGRESS}} {{PLANNED_SYMBOL}}:{{PLANNED}} / {{TOTAL}}]",
},
];
examples.forEach((example) => {
const exampleItem = exampleContainer.createEl("div", {
cls: "expression-example-item",
});
exampleItem.createEl("div", {
cls: "expression-example-name",
text: example.name,
});
const codeEl = exampleItem.createEl("code", {
cls: "expression-example-code",
text: example.code,
});
const useButton = exampleItem.createEl("button", {
cls: "expression-example-use",
text: t("Use"),
});
useButton.addEventListener("click", () => {
// 修复正确设置自定义格式并立即调用applySettingsUpdate
this.plugin.settings.customFormat = example.code;
this.applySettingsUpdate();
// 更新展示的输入框内容
const inputs = containerEl.querySelectorAll("input");
for (const input of Array.from(inputs)) {
if (input.placeholder === "[{{COMPLETED}}/{{TOTAL}}]") {
input.value = example.code;
break;
}
}
});
});
}
// Only show legacy percentage toggle for range-based or when displayMode is not set
else if (
this.plugin.settings.displayMode === "range-based" ||
!this.plugin.settings.displayMode
) {
new Setting(containerEl)
.setName(t("Show percentage"))
.setDesc(
t(
"Toggle this to allow this plugin to show percentage in the progress bar."
"Toggle this to show percentage instead of completed/total count."
)
)
.addToggle((toggle) =>
@ -469,12 +593,16 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
})
);
if (this.plugin.settings.showPercentage) {
// If percentage display and range-based mode is selected
if (
this.plugin.settings.showPercentage &&
this.plugin.settings.displayMode === "range-based"
) {
new Setting(containerEl)
.setName(t("Customize progress text"))
.setName(t("Customize progress ranges"))
.setDesc(
t(
"Toggle this to customize text representation for different progress percentage ranges."
"Toggle this to customize the text for different progress ranges."
)
)
.addToggle((toggle) =>
@ -486,6 +614,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
this.plugin.settings.customizeProgressRanges =
value;
this.applySettingsUpdate();
this.display();
})
);

View file

@ -82,6 +82,11 @@ export function shouldHideProgressBarInLivePriview(
plugin: TaskProgressBarPlugin,
view: EditorView
): boolean {
// If progress display mode is set to "none", hide progress bars
if (plugin.settings.progressBarDisplayMode === "none") {
return true;
}
if (!plugin.settings.hideProgressBarBasedOnConditions) {
return false;
}

View file

@ -1678,3 +1678,43 @@ settings:
.task-genius-settings-header {
display: none;
}
/* Expression examples in settings */
.expression-examples {
margin-top: 8px;
border-radius: 5px;
}
.expression-example-item {
margin-bottom: 12px;
padding: 8px;
border-radius: 4px;
/* background-color: var(--background-primary); */
display: flex;
flex-direction: column;
gap: 6px;
border: 1px solid var(--background-modifier-border);
}
.expression-example-name {
font-weight: bold;
}
.expression-example-code {
padding: 4px 8px;
background-color: var(--background-secondary);
border-radius: 4px;
font-family: var(--font-monospace);
font-size: 0.9em;
overflow-wrap: break-word;
}
.expression-example-use {
align-self: flex-end;
margin-top: 4px;
}
.custom-format-textarea {
height: 200px;
width: 100%;
}