From 3b6e70f98d327e37b393d22edab9298e271aebd3 Mon Sep 17 00:00:00 2001 From: quorafind <951011105@qq.com> Date: Sun, 6 Apr 2025 17:28:18 +0800 Subject: [PATCH] feat: support new progressbar --- design/task-view.md | 0 src/common/setting-definition.ts | 19 +- src/editor-ext/progressBarWidget.ts | 370 ++++++++++++++++++++-------- src/setting.ts | 187 +++++++++++--- src/utils.ts | 5 + styles.css | 40 +++ 6 files changed, 486 insertions(+), 135 deletions(-) create mode 100644 design/task-view.md diff --git a/design/task-view.md b/design/task-view.md new file mode 100644 index 00000000..e69de29b diff --git a/src/common/setting-definition.ts b/src/common/setting-definition.ts index e2e31399..7728591d 100644 --- a/src/common/setting-definition.ts +++ b/src/common/setting-definition.ts @@ -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", diff --git a/src/editor-ext/progressBarWidget.ts b/src/editor-ext/progressBarWidget.ts index 623ff7f5..4620723b 100644 --- a/src/editor-ext/progressBarWidget.ts +++ b/src/editor-ext/progressBarWidget.ts @@ -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; } diff --git a/src/setting.ts b/src/setting.ts index eb162a68..bce1873c 100644 --- a/src/setting.ts +++ b/src/setting.ts @@ -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(); }) ); diff --git a/src/utils.ts b/src/utils.ts index 514198a7..5ae398c4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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; } diff --git a/styles.css b/styles.css index 4c960c2a..5f069b5a 100644 --- a/styles.css +++ b/styles.css @@ -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%; +}