mirror of
https://github.com/vannamhh/progress-tracker.git
synced 2026-07-22 05:43:38 +00:00
feat: update status and date finish
This commit is contained in:
parent
160a118ac4
commit
f8aa5c8de5
1 changed files with 274 additions and 74 deletions
348
main.ts
348
main.ts
|
|
@ -75,6 +75,9 @@ interface TaskProgressBarSettings {
|
|||
keyboardInputDelay: number; // Add keyboard input delay
|
||||
checkboxClickDelay: number; // Add checkbox click delay
|
||||
maxTabsHeight: string; // New setting for max-height of workspace tabs
|
||||
autoUpdateMetadata: boolean; // Add new setting to control metadata auto-update feature
|
||||
autoChangeStatus: boolean; // Control whether to change status
|
||||
autoUpdateFinishedDate: boolean; // Control whether to update finished date
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: TaskProgressBarSettings = {
|
||||
|
|
@ -93,7 +96,10 @@ const DEFAULT_SETTINGS: TaskProgressBarSettings = {
|
|||
editorChangeDelay: 500, // Default editor change delay (500ms)
|
||||
keyboardInputDelay: 100, // Default keyboard input delay (10ms)
|
||||
checkboxClickDelay: 200, // Default checkbox click delay (50ms)
|
||||
maxTabsHeight: "auto" // Default to "auto"
|
||||
maxTabsHeight: "auto", // Default to "auto"
|
||||
autoUpdateMetadata: true, // Default to enabled
|
||||
autoChangeStatus: true, // Default to enabled
|
||||
autoUpdateFinishedDate: true, // Default to enabled
|
||||
};
|
||||
|
||||
export default class TaskProgressBarPlugin extends Plugin {
|
||||
|
|
@ -107,7 +113,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
// Apply the max-height CSS style as soon as the plugin loads
|
||||
// Apply the max-height CSS style as soon as the plugin loads
|
||||
this.applyMaxTabsHeightStyle();
|
||||
|
||||
// Register view type for the sidebar
|
||||
|
|
@ -133,7 +139,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
if (file) {
|
||||
this.lastActiveFile = file;
|
||||
|
||||
// Always update when file changes to ensure accurate display
|
||||
// Always update when file changes to ensure accurate display
|
||||
setTimeout(async () => {
|
||||
await this.updateLastFileContent(file);
|
||||
if (this.sidebarView) {
|
||||
|
|
@ -280,6 +286,18 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
}
|
||||
}, 1500);
|
||||
}, 1000);
|
||||
|
||||
// Register commands
|
||||
this.addCommand({
|
||||
id: 'clear-completed-files-cache',
|
||||
name: 'Clear Completed Files Cache',
|
||||
callback: () => {
|
||||
if (this.sidebarView) {
|
||||
this.sidebarView.clearCompletedFilesCache();
|
||||
new Notice('Completed files cache cleared. Files can trigger completion notifications again.');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Kiểm tra Dataview API và thiết lập interval để kiểm tra lại nếu chưa có
|
||||
|
|
@ -391,6 +409,11 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
|
||||
// Detach the view when plugin unloads
|
||||
this.app.workspace.detachLeavesOfType("progress-tracker");
|
||||
|
||||
// Clear any in-memory data
|
||||
if (this.sidebarView) {
|
||||
this.sidebarView.clearCompletedFilesCache();
|
||||
}
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
|
|
@ -416,15 +439,17 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
applyMaxTabsHeightStyle() {
|
||||
try {
|
||||
// Remove any existing style element first
|
||||
const existingStyle = document.getElementById('progress-tracker-max-tabs-height');
|
||||
const existingStyle = document.getElementById(
|
||||
"progress-tracker-max-tabs-height"
|
||||
);
|
||||
if (existingStyle) {
|
||||
existingStyle.remove();
|
||||
}
|
||||
|
||||
// Create a new style element
|
||||
const style = document.createElement('style');
|
||||
style.id = 'progress-tracker-max-tabs-height';
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.id = "progress-tracker-max-tabs-height";
|
||||
|
||||
// Set the CSS rule with the user's preference
|
||||
style.textContent = `
|
||||
.workspace-tabs.mod-top.mod-top-right-space:not(.mod-top-left-space) {
|
||||
|
|
@ -432,13 +457,15 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
transition: max-height 0.3s ease;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
// Add the style to the document head
|
||||
document.head.appendChild(style);
|
||||
|
||||
|
||||
// Debug info
|
||||
if (this.settings.showDebugInfo) {
|
||||
console.log(`Applied max-tabs-height: ${this.settings.maxTabsHeight}`);
|
||||
console.log(
|
||||
`Applied max-tabs-height: ${this.settings.maxTabsHeight}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error applying max tabs height style:", error);
|
||||
|
|
@ -453,6 +480,7 @@ class TaskProgressBarView extends ItemView {
|
|||
lastUpdateTime: number = 0;
|
||||
lastFileUpdateMap: Map<string, number> = new Map(); // Track last update time per file
|
||||
initialLoadComplete: boolean = false; // Track if initial load is complete
|
||||
completedFilesMap: Map<string, boolean> = new Map(); // Track which files have been marked as completed
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: TaskProgressBarPlugin) {
|
||||
super(leaf);
|
||||
|
|
@ -537,9 +565,10 @@ class TaskProgressBarView extends ItemView {
|
|||
text: "No tasks found in this file",
|
||||
});
|
||||
} else {
|
||||
this.updateProgressBarContentWithString(
|
||||
content,
|
||||
// Create progress bar directly with content
|
||||
this.createProgressBarFromString(
|
||||
progressContainer,
|
||||
content,
|
||||
file
|
||||
);
|
||||
}
|
||||
|
|
@ -557,9 +586,10 @@ class TaskProgressBarView extends ItemView {
|
|||
console.log("No tasks found in file:", file.path);
|
||||
}
|
||||
} else {
|
||||
this.updateProgressBarContentWithString(
|
||||
fileContent,
|
||||
// Create progress bar directly with file content
|
||||
this.createProgressBarFromString(
|
||||
progressContainer,
|
||||
fileContent,
|
||||
file
|
||||
);
|
||||
}
|
||||
|
|
@ -568,7 +598,7 @@ class TaskProgressBarView extends ItemView {
|
|||
console.error("Error updating progress bar:", error);
|
||||
progressContainer.empty();
|
||||
progressContainer.createEl("p", {
|
||||
text: "Error updating progress bar",
|
||||
text: `Error updating progress bar: ${error.message}`,
|
||||
});
|
||||
} finally {
|
||||
// Xóa class sau khi cập nhật xong, chỉ khi animation được bật
|
||||
|
|
@ -585,43 +615,11 @@ class TaskProgressBarView extends ItemView {
|
|||
// Improved and more accurate task detection
|
||||
const standardTaskRegex = /- \[[x ]\]/i;
|
||||
const relaxedTaskRegex = /[-*] \[[x ]\]/i;
|
||||
|
||||
|
||||
// Return true if either regex matches
|
||||
return standardTaskRegex.test(content) || relaxedTaskRegex.test(content);
|
||||
}
|
||||
|
||||
// Phương thức mới để cập nhật với nội dung string trực tiếp
|
||||
async updateProgressBarContentWithString(
|
||||
content: string,
|
||||
progressContainer: HTMLElement,
|
||||
file: TFile
|
||||
) {
|
||||
// Always clear the container to prevent showing stale content
|
||||
progressContainer.empty();
|
||||
|
||||
// Get Dataview API
|
||||
const dvAPI = this.plugin.dvAPI;
|
||||
if (!dvAPI) {
|
||||
const dataviewWarning = progressContainer.createDiv({
|
||||
cls: "dataview-warning-compact",
|
||||
});
|
||||
dataviewWarning.createEl("span", {
|
||||
text: "Dataview not available",
|
||||
cls: "dataview-warning-text",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Tạo thanh tiến trình trực tiếp từ nội dung
|
||||
this.createProgressBarFromString(progressContainer, content, file);
|
||||
} catch (error) {
|
||||
console.error("Error updating progress bar:", error);
|
||||
progressContainer.empty();
|
||||
progressContainer.createEl("p", {
|
||||
text: "Error loading progress bar",
|
||||
});
|
||||
}
|
||||
return (
|
||||
standardTaskRegex.test(content) || relaxedTaskRegex.test(content)
|
||||
);
|
||||
}
|
||||
|
||||
// Phương thức tạo thanh tiến trình từ nội dung string
|
||||
|
|
@ -630,6 +628,9 @@ class TaskProgressBarView extends ItemView {
|
|||
content: string,
|
||||
file: TFile
|
||||
) {
|
||||
// Always clear the container first to ensure clean state
|
||||
container.empty();
|
||||
|
||||
try {
|
||||
// Log for debugging if enabled
|
||||
if (this.plugin.settings.showDebugInfo) {
|
||||
|
|
@ -637,17 +638,32 @@ class TaskProgressBarView extends ItemView {
|
|||
console.log(`Content length: ${content.length}`);
|
||||
}
|
||||
|
||||
// Get Dataview API - only check for warning display
|
||||
const dvAPI = this.plugin.dvAPI;
|
||||
if (!dvAPI) {
|
||||
const dataviewWarning = container.createDiv({
|
||||
cls: "dataview-warning-compact",
|
||||
});
|
||||
dataviewWarning.createEl("span", {
|
||||
text: "Dataview not available",
|
||||
cls: "dataview-warning-text",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Sử dụng regex chính xác hơn để đếm task
|
||||
const incompleteTasks = (content.match(/- \[ \]/g) || []).length;
|
||||
const completedTasks = (content.match(/- \[x\]/gi) || []).length;
|
||||
const completedTasks = (content.match(/- \[x\]/gi) || []).length;
|
||||
let totalTasks = incompleteTasks + completedTasks;
|
||||
|
||||
// Try with relaxed regex if needed
|
||||
let relaxedIncompleteTasks = 0;
|
||||
let relaxedCompletedTasks = 0;
|
||||
if (totalTasks === 0) {
|
||||
relaxedIncompleteTasks = (content.match(/[-*] \[ \]/g) || []).length;
|
||||
relaxedCompletedTasks = (content.match(/[-*] \[x\]/gi) || []).length;
|
||||
relaxedIncompleteTasks = (content.match(/[-*] \[ \]/g) || [])
|
||||
.length;
|
||||
relaxedCompletedTasks = (content.match(/[-*] \[x\]/gi) || [])
|
||||
.length;
|
||||
totalTasks = relaxedIncompleteTasks + relaxedCompletedTasks;
|
||||
}
|
||||
|
||||
|
|
@ -681,6 +697,20 @@ class TaskProgressBarView extends ItemView {
|
|||
|
||||
const percentage = Math.round((completedCount / totalTasks) * 100);
|
||||
|
||||
// Check if all tasks are completed (100%) and update metadata if enabled
|
||||
if (percentage === 100 && this.plugin.settings.autoUpdateMetadata) {
|
||||
// Only update metadata and show notification if this file hasn't been marked as completed yet
|
||||
if (!this.completedFilesMap.has(file.path)) {
|
||||
await this.updateFileMetadata(file, content);
|
||||
// Mark this file as completed to avoid repeated updates
|
||||
this.completedFilesMap.set(file.path, true);
|
||||
}
|
||||
} else if (percentage < 100) {
|
||||
// If percentage is less than 100%, remove from completed files map
|
||||
// This allows re-notification if the tasks are completed again after being incomplete
|
||||
this.completedFilesMap.delete(file.path);
|
||||
}
|
||||
|
||||
// Create the progress bar elements
|
||||
this.createProgressBarElements(
|
||||
container,
|
||||
|
|
@ -692,11 +722,117 @@ class TaskProgressBarView extends ItemView {
|
|||
console.error("Error creating progress bar from string:", error);
|
||||
container.empty();
|
||||
container.createEl("p", {
|
||||
text: "Error creating progress bar",
|
||||
text: `Error creating progress bar: ${error.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// New method to update file metadata when tasks are completed
|
||||
async updateFileMetadata(file: TFile, content: string) {
|
||||
try {
|
||||
// Check if file has YAML frontmatter
|
||||
const yamlRegex = /^---\s*\n([\s\S]*?)\n---/;
|
||||
const yamlMatch = content.match(yamlRegex);
|
||||
|
||||
if (!yamlMatch) {
|
||||
if (this.plugin.settings.showDebugInfo) {
|
||||
console.log(
|
||||
"No YAML frontmatter found in file:",
|
||||
file.path
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let yaml = yamlMatch[1];
|
||||
let needsUpdate = false;
|
||||
let updatedYaml = yaml;
|
||||
// Define today's date at the beginning of the function so it's available throughout
|
||||
const today = new Date().toISOString().split("T")[0]; // Format: YYYY-MM-DD
|
||||
|
||||
// Check if status is already "Completed" to avoid unnecessary updates
|
||||
const statusAlreadyCompleted = /status\s*:\s*Completed/i.test(yaml);
|
||||
|
||||
// Update status from "In Progress" to "Completed" if enabled and not already complete
|
||||
if (this.plugin.settings.autoChangeStatus && !statusAlreadyCompleted) {
|
||||
const statusRegex = /(status\s*:\s*)In Progress/i;
|
||||
if (statusRegex.test(yaml)) {
|
||||
updatedYaml = updatedYaml.replace(
|
||||
statusRegex,
|
||||
"$1Completed"
|
||||
);
|
||||
needsUpdate = true;
|
||||
|
||||
if (this.plugin.settings.showDebugInfo) {
|
||||
console.log(
|
||||
"Updating status to Completed in file:",
|
||||
file.path
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if finished date already exists and matches today's date
|
||||
const finishedDateRegex = /finished\s*:\s*(\d{4}-\d{2}-\d{2})/i;
|
||||
const finishedDateMatch = yaml.match(finishedDateRegex);
|
||||
const finishedDateAlreadySet = finishedDateMatch && finishedDateMatch[1] === today;
|
||||
|
||||
// Update finished date only if enabled and not already set to today's date
|
||||
if (this.plugin.settings.autoUpdateFinishedDate && !finishedDateAlreadySet) {
|
||||
const finishedRegex = /(finished\s*:)\s*([^\n]*)/i;
|
||||
|
||||
if (finishedRegex.test(yaml)) {
|
||||
// Replace existing finished date with proper spacing
|
||||
updatedYaml = updatedYaml.replace(
|
||||
finishedRegex,
|
||||
`$1 ${today}`
|
||||
);
|
||||
needsUpdate = true;
|
||||
} else {
|
||||
// Add finished date if it doesn't exist, ensuring proper spacing
|
||||
updatedYaml = updatedYaml.trim() + `\nfinished: ${today}`;
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
if (this.plugin.settings.showDebugInfo) {
|
||||
console.log(
|
||||
`Updating finished date to ${today} in file:`,
|
||||
file.path
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update file content if changes were made
|
||||
if (needsUpdate) {
|
||||
const updatedContent = content.replace(
|
||||
yamlRegex,
|
||||
`---\n${updatedYaml}\n---`
|
||||
);
|
||||
await this.plugin.app.vault.modify(file, updatedContent);
|
||||
|
||||
// Show more detailed notification about what was updated
|
||||
let updateMessage = `Updated metadata for ${file.basename}`;
|
||||
if (this.plugin.settings.autoChangeStatus && !statusAlreadyCompleted) {
|
||||
updateMessage += ": Status set to Completed";
|
||||
}
|
||||
if (this.plugin.settings.autoUpdateFinishedDate && !finishedDateAlreadySet) {
|
||||
if (this.plugin.settings.autoChangeStatus && !statusAlreadyCompleted) {
|
||||
updateMessage += " and";
|
||||
}
|
||||
updateMessage += ` finished date set to ${today}`;
|
||||
}
|
||||
new Notice(updateMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating file metadata:", error);
|
||||
if (this.plugin.settings.showDebugInfo) {
|
||||
new Notice(
|
||||
`Error updating metadata for ${file.basename}: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New helper method to create progress bar elements
|
||||
createProgressBarElements(
|
||||
container: HTMLElement,
|
||||
|
|
@ -809,6 +945,15 @@ class TaskProgressBarView extends ItemView {
|
|||
Applied color: ${progressElement.style.backgroundColor}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a method to clear the completed files map when needed
|
||||
// For example, when plugin is unloaded or when explicitly requested
|
||||
clearCompletedFilesCache() {
|
||||
this.completedFilesMap.clear();
|
||||
if (this.plugin.settings.showDebugInfo) {
|
||||
console.log("Cleared completed files cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TaskProgressBarSettingTab extends PluginSettingTab {
|
||||
|
|
@ -1265,21 +1410,23 @@ class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
// Fix the Max Tabs Height setting to allow proper input
|
||||
new Setting(containerEl)
|
||||
.setName("Max Tabs Height")
|
||||
.setDesc("Maximum height for workspace tabs (e.g., 110px, 200px, auto)")
|
||||
.setDesc(
|
||||
"Maximum height for workspace tabs (e.g., 110px, 200px, auto)"
|
||||
)
|
||||
.addText((text) => {
|
||||
// Set initial value
|
||||
text.setValue(this.plugin.settings.maxTabsHeight);
|
||||
|
||||
|
||||
// Apply improved validation only when the field loses focus
|
||||
// This allows typing intermediary values that may not be valid yet
|
||||
text.inputEl.addEventListener("blur", async () => {
|
||||
const value = text.inputEl.value;
|
||||
// Validate only when user is done editing
|
||||
const isValid =
|
||||
value === "auto" ||
|
||||
value === "none" ||
|
||||
const isValid =
|
||||
value === "auto" ||
|
||||
value === "none" ||
|
||||
/^\d+(\.\d+)?(px|em|rem|vh|%)$/.test(value);
|
||||
|
||||
|
||||
if (isValid) {
|
||||
// Only update if changed to avoid unnecessary saves
|
||||
if (this.plugin.settings.maxTabsHeight !== value) {
|
||||
|
|
@ -1288,12 +1435,14 @@ class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
new Notice(`Max tabs height updated to ${value}`);
|
||||
}
|
||||
} else {
|
||||
new Notice("Please enter 'auto', 'none' or a valid CSS length value (e.g., 110px)");
|
||||
new Notice(
|
||||
"Please enter 'auto', 'none' or a valid CSS length value (e.g., 110px)"
|
||||
);
|
||||
// Reset to previous valid value
|
||||
text.setValue(this.plugin.settings.maxTabsHeight);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Add Enter key handling
|
||||
text.inputEl.addEventListener("keydown", async (event) => {
|
||||
if (event.key === "Enter") {
|
||||
|
|
@ -1301,22 +1450,73 @@ class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
text.inputEl.blur(); // Trigger the blur event to validate
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Improve UX
|
||||
text.inputEl.style.width = "120px";
|
||||
text.inputEl.placeholder = "auto";
|
||||
|
||||
|
||||
return text;
|
||||
})
|
||||
.addExtraButton(button => button
|
||||
.setIcon("reset")
|
||||
.setTooltip("Reset to default (auto)")
|
||||
.onClick(async () => {
|
||||
this.plugin.settings.maxTabsHeight = "auto";
|
||||
await this.plugin.saveSettings();
|
||||
this.display(); // Refresh the settings panel
|
||||
new Notice("Max tabs height reset to 'auto'");
|
||||
})
|
||||
.addExtraButton((button) =>
|
||||
button
|
||||
.setIcon("reset")
|
||||
.setTooltip("Reset to default (auto)")
|
||||
.onClick(async () => {
|
||||
this.plugin.settings.maxTabsHeight = "auto";
|
||||
await this.plugin.saveSettings();
|
||||
this.display(); // Refresh the settings panel
|
||||
new Notice("Max tabs height reset to 'auto'");
|
||||
})
|
||||
);
|
||||
|
||||
// Add Metadata Auto-Update Settings
|
||||
containerEl.createEl("h3", { text: "Metadata Auto-Update" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Auto-update metadata")
|
||||
.setDesc(
|
||||
"Automatically update metadata when all tasks are completed"
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.autoUpdateMetadata)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoUpdateMetadata = value;
|
||||
await this.plugin.saveSettings();
|
||||
// Refresh the display to show/hide related settings
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
|
||||
// Only show these settings if auto-update is enabled
|
||||
if (this.plugin.settings.autoUpdateMetadata) {
|
||||
new Setting(containerEl)
|
||||
.setName("Change status")
|
||||
.setDesc(
|
||||
"Change 'status: In Progress' to 'status: Completed' when tasks reach 100%"
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.autoChangeStatus)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoChangeStatus = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Update finished date")
|
||||
.setDesc(
|
||||
"Set 'finished: ' to today's date when tasks reach 100%"
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.autoUpdateFinishedDate)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoUpdateFinishedDate = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue