mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat(timer): add timer statistics panel and Working On view
- Add TimerStatisticsPanel component for displaying active and completed timers - Implement completed timer history with configurable max records - Add timer controls (start/pause/resume/stop) to task list and tree views - Integrate timer auto-start option in quick capture settings - Add Working On navigation item for viewing active timers - Refactor timer manager with unified storage abstraction - Add i18n translations for timer features (en/zh-cn) - Add timer statistics CSS styles
This commit is contained in:
parent
14ca05a9da
commit
9be6bf7580
21 changed files with 3125 additions and 610 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit 3cb570633ae01ab569b26a956639fc167da398ba
|
||||
Subproject commit 3107a754e650b64cb5dd68101ebbad99ac422f27
|
||||
|
|
@ -428,6 +428,8 @@ export interface QuickCaptureSettings {
|
|||
templateFile: string; // Template file path
|
||||
writeContentTagsToFrontmatter?: boolean; // When true, write #tags from content into frontmatter.tags (merged, deduped)
|
||||
};
|
||||
// Timer integration settings
|
||||
autoStartTimer?: boolean; // Whether to auto-start timer when creating task (default false)
|
||||
}
|
||||
|
||||
/** Define the structure for task gutter settings */
|
||||
|
|
@ -1192,6 +1194,8 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
|
|||
templateFile: "",
|
||||
writeContentTagsToFrontmatter: false,
|
||||
},
|
||||
// Timer integration
|
||||
autoStartTimer: false,
|
||||
},
|
||||
|
||||
// Workflow Defaults
|
||||
|
|
@ -1370,6 +1374,16 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
|
|||
filterRules: {},
|
||||
filterBlanks: false,
|
||||
},
|
||||
{
|
||||
id: "working-on",
|
||||
name: t("Working On"),
|
||||
icon: "timer",
|
||||
type: "default",
|
||||
visible: true,
|
||||
hideCompletedAndAbandonedTasks: true,
|
||||
filterRules: {},
|
||||
filterBlanks: false,
|
||||
},
|
||||
{
|
||||
id: "forecast",
|
||||
name: t("Forecast"),
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
getNextStatusPrimary,
|
||||
getAllStatusMarks,
|
||||
} from "@/utils/status-cycle-resolver";
|
||||
import { TaskTimerManager } from "@/managers/timer-manager";
|
||||
|
||||
/**
|
||||
* FluentActionHandlers - Handles all user actions and task operations
|
||||
|
|
@ -56,7 +57,7 @@ export class FluentActionHandlers extends Component {
|
|||
private app: App,
|
||||
private plugin: TaskProgressBarPlugin,
|
||||
private getWorkspaceId: () => string,
|
||||
private useSideLeaves: () => boolean,
|
||||
private useSideLeaves: () => boolean
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
|
@ -173,7 +174,7 @@ export class FluentActionHandlers extends Component {
|
|||
async handleTaskUpdate(
|
||||
originalTask: Task,
|
||||
updatedTask: Task,
|
||||
successMessage?: string,
|
||||
successMessage?: string
|
||||
): Promise<void> {
|
||||
if (!this.plugin.writeAPI) {
|
||||
console.error("WriteAPI not available");
|
||||
|
|
@ -183,7 +184,7 @@ export class FluentActionHandlers extends Component {
|
|||
try {
|
||||
const updates = this.extractChangedFields(
|
||||
originalTask,
|
||||
updatedTask,
|
||||
updatedTask
|
||||
);
|
||||
const writeResult = await this.plugin.writeAPI.updateTask({
|
||||
taskId: originalTask.id,
|
||||
|
|
@ -211,13 +212,13 @@ export class FluentActionHandlers extends Component {
|
|||
*/
|
||||
async handleKanbanTaskStatusUpdate(
|
||||
task: Task,
|
||||
newStatusMark: string,
|
||||
newStatusMark: string
|
||||
): Promise<void> {
|
||||
console.log(
|
||||
`[FluentActionHandlers] Processing kanban status update for task ${task.id}`,
|
||||
`[FluentActionHandlers] Processing kanban status update for task ${task.id}`
|
||||
);
|
||||
console.log(
|
||||
`[FluentActionHandlers] Status change: ${task.status} -> ${newStatusMark}`,
|
||||
`[FluentActionHandlers] Status change: ${task.status} -> ${newStatusMark}`
|
||||
);
|
||||
|
||||
const isCompleted = this.isCompletedMark(newStatusMark);
|
||||
|
|
@ -225,7 +226,7 @@ export class FluentActionHandlers extends Component {
|
|||
|
||||
if (task.status !== newStatusMark || task.completed !== isCompleted) {
|
||||
console.log(
|
||||
"[FluentActionHandlers] Status change detected, calling handleTaskUpdate...",
|
||||
"[FluentActionHandlers] Status change detected, calling handleTaskUpdate..."
|
||||
);
|
||||
await this.handleTaskUpdate(task, {
|
||||
...task,
|
||||
|
|
@ -239,7 +240,7 @@ export class FluentActionHandlers extends Component {
|
|||
console.log("[FluentActionHandlers] handleTaskUpdate completed");
|
||||
} else {
|
||||
console.log(
|
||||
"[FluentActionHandlers] No status change needed, skipping update",
|
||||
"[FluentActionHandlers] No status change needed, skipping update"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -272,7 +273,7 @@ export class FluentActionHandlers extends Component {
|
|||
const currentMark = task.status || " ";
|
||||
const applicableCycles = findApplicableCycles(
|
||||
currentMark,
|
||||
this.plugin.settings.statusCycles,
|
||||
this.plugin.settings.statusCycles
|
||||
);
|
||||
|
||||
if (applicableCycles.length > 0) {
|
||||
|
|
@ -285,7 +286,7 @@ export class FluentActionHandlers extends Component {
|
|||
for (const cycle of applicableCycles) {
|
||||
const nextStatusResult = getNextStatusPrimary(
|
||||
currentMark,
|
||||
[cycle],
|
||||
[cycle]
|
||||
);
|
||||
if (nextStatusResult) {
|
||||
submenu.addItem((subItem) => {
|
||||
|
|
@ -300,9 +301,9 @@ export class FluentActionHandlers extends Component {
|
|||
createTaskCheckbox(
|
||||
nextStatusResult.mark,
|
||||
task,
|
||||
el,
|
||||
el
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
subItem.titleEl.createEl("span", {
|
||||
cls: "status-option",
|
||||
|
|
@ -310,7 +311,7 @@ export class FluentActionHandlers extends Component {
|
|||
});
|
||||
subItem.onClick(async () => {
|
||||
const willComplete = this.isCompletedMark(
|
||||
nextStatusResult.mark,
|
||||
nextStatusResult.mark
|
||||
);
|
||||
const updatedTask = {
|
||||
...task,
|
||||
|
|
@ -331,7 +332,7 @@ export class FluentActionHandlers extends Component {
|
|||
|
||||
await this.handleKanbanTaskStatusUpdate(
|
||||
updatedTask,
|
||||
nextStatusResult.mark,
|
||||
nextStatusResult.mark
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -347,7 +348,7 @@ export class FluentActionHandlers extends Component {
|
|||
|
||||
// Show all available statuses
|
||||
const allStatusNames = getAllStatusNames(
|
||||
this.plugin.settings.statusCycles,
|
||||
this.plugin.settings.statusCycles
|
||||
);
|
||||
for (const statusName of Array.from(allStatusNames)) {
|
||||
// Find the mark for this status
|
||||
|
|
@ -367,7 +368,7 @@ export class FluentActionHandlers extends Component {
|
|||
},
|
||||
(el) => {
|
||||
createTaskCheckbox(mark, task, el);
|
||||
},
|
||||
}
|
||||
);
|
||||
subItem.titleEl.createEl("span", {
|
||||
cls: "status-option",
|
||||
|
|
@ -389,7 +390,7 @@ export class FluentActionHandlers extends Component {
|
|||
|
||||
await this.handleKanbanTaskStatusUpdate(
|
||||
updatedTask,
|
||||
mark,
|
||||
mark
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -408,7 +409,7 @@ export class FluentActionHandlers extends Component {
|
|||
},
|
||||
(el) => {
|
||||
createTaskCheckbox(mark, task, el);
|
||||
},
|
||||
}
|
||||
);
|
||||
subItem.titleEl.createEl("span", {
|
||||
cls: "status-option",
|
||||
|
|
@ -438,6 +439,9 @@ export class FluentActionHandlers extends Component {
|
|||
this.addPriorityMenuItems(menu, task);
|
||||
this.addDateMenuItems(menu, task);
|
||||
|
||||
// Timer menu items
|
||||
this.addTimerMenuItems(menu, task);
|
||||
|
||||
menu.addSeparator();
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(t("Set Project"));
|
||||
|
|
@ -562,7 +566,7 @@ export class FluentActionHandlers extends Component {
|
|||
private addPostponeDateMenu(
|
||||
menu: Menu,
|
||||
task: Task,
|
||||
dateType: TaskDateType,
|
||||
dateType: TaskDateType
|
||||
): void {
|
||||
menu.addItem((item) => {
|
||||
const subMenu = item
|
||||
|
|
@ -600,7 +604,7 @@ export class FluentActionHandlers extends Component {
|
|||
private async postponeDate(
|
||||
task: Task,
|
||||
dateType: TaskDateType,
|
||||
offsetDays: number,
|
||||
offsetDays: number
|
||||
): Promise<void> {
|
||||
try {
|
||||
const currentTimestamp = task.metadata?.[dateType];
|
||||
|
|
@ -614,7 +618,7 @@ export class FluentActionHandlers extends Component {
|
|||
task,
|
||||
dateType,
|
||||
newTimestamp,
|
||||
offsetDays,
|
||||
offsetDays
|
||||
);
|
||||
|
||||
const updatedTask: Task = {
|
||||
|
|
@ -624,23 +628,172 @@ export class FluentActionHandlers extends Component {
|
|||
|
||||
const action = offsetDays > 0 ? "postponed" : "advanced";
|
||||
const message = `${this.getDateLabel(
|
||||
dateType,
|
||||
dateType
|
||||
)} ${action} by ${Math.abs(offsetDays)} day(s)`;
|
||||
|
||||
await this.handleTaskUpdate(task, updatedTask, t(message));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[FluentActionHandlers] Failed to postpone date:",
|
||||
error,
|
||||
error
|
||||
);
|
||||
new Notice(t("Failed to postpone date"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add timer-related menu items
|
||||
*/
|
||||
private addTimerMenuItems(menu: Menu, task: Task): void {
|
||||
// Only show timer options if timer feature is enabled
|
||||
if (!this.plugin.settings.taskTimer?.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
const timerManager = new TaskTimerManager(
|
||||
this.plugin.settings.taskTimer
|
||||
);
|
||||
|
||||
// Check if task has an existing block ID and active timer
|
||||
const blockId = task.metadata?.id;
|
||||
const existingTimer = blockId
|
||||
? timerManager.getTimerByFileAndBlock(task.filePath, blockId)
|
||||
: null;
|
||||
|
||||
if (existingTimer && existingTimer.status === "running") {
|
||||
// Timer is running - show pause and stop options
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(t("Pause Timer"));
|
||||
item.setIcon("pause");
|
||||
item.onClick(() => {
|
||||
const taskId = `taskTimer_${task.filePath}#${blockId}`;
|
||||
timerManager.pauseTimer(taskId);
|
||||
new Notice(t("Timer paused"));
|
||||
});
|
||||
});
|
||||
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(t("Stop Timer"));
|
||||
item.setIcon("square");
|
||||
item.onClick(async () => {
|
||||
const taskId = `taskTimer_${task.filePath}#${blockId}`;
|
||||
const duration = timerManager.completeTimer(taskId);
|
||||
new Notice(t("Timer stopped") + `: ${duration}`);
|
||||
});
|
||||
});
|
||||
} else if (existingTimer && existingTimer.status === "paused") {
|
||||
// Timer is paused - show resume and stop options
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(t("Resume Timer"));
|
||||
item.setIcon("play");
|
||||
item.onClick(() => {
|
||||
const taskId = `taskTimer_${task.filePath}#${blockId}`;
|
||||
timerManager.resumeTimer(taskId);
|
||||
new Notice(t("Timer resumed"));
|
||||
});
|
||||
});
|
||||
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(t("Stop Timer"));
|
||||
item.setIcon("square");
|
||||
item.onClick(async () => {
|
||||
const taskId = `taskTimer_${task.filePath}#${blockId}`;
|
||||
const duration = timerManager.completeTimer(taskId);
|
||||
new Notice(t("Timer stopped") + `: ${duration}`);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// No active timer - show start option
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(t("Start Timer"));
|
||||
item.setIcon("timer");
|
||||
item.onClick(async () => {
|
||||
await this.startTimerForTask(task);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a timer for a task
|
||||
* If the task doesn't have a block ID, generate one and update the task
|
||||
*/
|
||||
private async startTimerForTask(task: Task): Promise<void> {
|
||||
try {
|
||||
const timerManager = new TaskTimerManager(
|
||||
this.plugin.settings.taskTimer
|
||||
);
|
||||
|
||||
let blockId = task.metadata?.id;
|
||||
|
||||
// If task doesn't have a block ID, generate one and save it
|
||||
if (!blockId) {
|
||||
blockId = timerManager.generateBlockId();
|
||||
|
||||
// Update task with the new block ID
|
||||
const updatedTask: Task = {
|
||||
...task,
|
||||
metadata: {
|
||||
...task.metadata,
|
||||
id: blockId,
|
||||
},
|
||||
};
|
||||
|
||||
// Save the block ID to the task
|
||||
await this.handleTaskUpdate(
|
||||
task,
|
||||
updatedTask,
|
||||
t("Block ID added")
|
||||
);
|
||||
|
||||
// Update local reference
|
||||
task = updatedTask;
|
||||
}
|
||||
|
||||
// Change task status to In Progress if not already
|
||||
const inProgressMarks = (
|
||||
this.plugin.settings.taskStatuses.inProgress || ">|/"
|
||||
).split("|");
|
||||
const completedMarks = (
|
||||
this.plugin.settings.taskStatuses.completed || "x|X"
|
||||
).split("|");
|
||||
|
||||
// Only change status if not completed and not already in progress
|
||||
if (
|
||||
!task.completed &&
|
||||
!inProgressMarks.includes(task.status || " ")
|
||||
) {
|
||||
const inProgressMark = inProgressMarks[0] || "/";
|
||||
const statusUpdatedTask: Task = {
|
||||
...task,
|
||||
status: inProgressMark,
|
||||
};
|
||||
|
||||
await this.handleTaskUpdate(
|
||||
task,
|
||||
statusUpdatedTask,
|
||||
t("Task status updated")
|
||||
);
|
||||
}
|
||||
|
||||
// Start the timer
|
||||
timerManager.startTimer(task.filePath, blockId);
|
||||
new Notice(t("Timer started"));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[FluentActionHandlers] Failed to start timer:",
|
||||
error
|
||||
);
|
||||
new Notice(t("Failed to start timer"));
|
||||
}
|
||||
}
|
||||
|
||||
private openDatePicker(
|
||||
task: Task,
|
||||
dateType: TaskDateType,
|
||||
isPostpone: boolean = false,
|
||||
isPostpone: boolean = false
|
||||
): void {
|
||||
const currentDate = task.metadata?.[dateType];
|
||||
const initialDate =
|
||||
|
|
@ -652,7 +805,7 @@ export class FluentActionHandlers extends Component {
|
|||
this.app,
|
||||
this.plugin,
|
||||
initialDate,
|
||||
this.getDateMark(dateType),
|
||||
this.getDateMark(dateType)
|
||||
);
|
||||
|
||||
modal.onDateSelected = async (dateStr: string | null) => {
|
||||
|
|
@ -666,14 +819,14 @@ export class FluentActionHandlers extends Component {
|
|||
|
||||
if (isPostpone && typeof currentDate === "number") {
|
||||
const offsetDays = Math.round(
|
||||
(newTimestamp - currentDate) / (24 * 60 * 60 * 1000),
|
||||
(newTimestamp - currentDate) / (24 * 60 * 60 * 1000)
|
||||
);
|
||||
|
||||
const updatedMetadata = smartPostponeRelatedDates(
|
||||
task,
|
||||
dateType,
|
||||
newTimestamp,
|
||||
offsetDays,
|
||||
offsetDays
|
||||
);
|
||||
|
||||
const updatedTask: Task = {
|
||||
|
|
@ -684,7 +837,7 @@ export class FluentActionHandlers extends Component {
|
|||
await this.handleTaskUpdate(
|
||||
task,
|
||||
updatedTask,
|
||||
t(`${this.getDateLabel(dateType)} updated`),
|
||||
t(`${this.getDateLabel(dateType)} updated`)
|
||||
);
|
||||
} else {
|
||||
const updatedTask: Task = {
|
||||
|
|
@ -698,7 +851,7 @@ export class FluentActionHandlers extends Component {
|
|||
await this.handleTaskUpdate(
|
||||
task,
|
||||
updatedTask,
|
||||
t(`${this.getDateLabel(dateType)} updated`),
|
||||
t(`${this.getDateLabel(dateType)} updated`)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -809,7 +962,7 @@ export class FluentActionHandlers extends Component {
|
|||
} catch (error) {
|
||||
console.error(
|
||||
"[FluentActionHandlers] Failed to duplicate task:",
|
||||
error,
|
||||
error
|
||||
);
|
||||
new Notice(t("Failed to duplicate task"));
|
||||
}
|
||||
|
|
@ -905,7 +1058,7 @@ export class FluentActionHandlers extends Component {
|
|||
*/
|
||||
private async deleteTask(
|
||||
task: Task,
|
||||
deleteChildren: boolean,
|
||||
deleteChildren: boolean
|
||||
): Promise<void> {
|
||||
if (!this.plugin.writeAPI) {
|
||||
console.error("WriteAPI not available for deleteTask");
|
||||
|
|
@ -933,13 +1086,13 @@ export class FluentActionHandlers extends Component {
|
|||
new Notice(
|
||||
t("Failed to delete task") +
|
||||
": " +
|
||||
(result.error || "Unknown error"),
|
||||
(result.error || "Unknown error")
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting task:", error);
|
||||
new Notice(
|
||||
t("Failed to delete task") + ": " + (error as any).message,
|
||||
t("Failed to delete task") + ": " + (error as any).message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -995,7 +1148,7 @@ export class FluentActionHandlers extends Component {
|
|||
try {
|
||||
const lower = mark.toLowerCase();
|
||||
const completedCfg = String(
|
||||
this.plugin.settings.taskStatuses?.completed || "x",
|
||||
this.plugin.settings.taskStatuses?.completed || "x"
|
||||
);
|
||||
const completedSet = completedCfg
|
||||
.split("|")
|
||||
|
|
@ -1012,7 +1165,7 @@ export class FluentActionHandlers extends Component {
|
|||
*/
|
||||
private extractChangedFields(
|
||||
originalTask: Task,
|
||||
updatedTask: Task,
|
||||
updatedTask: Task
|
||||
): Partial<Task> {
|
||||
const changes: Partial<Task> = {};
|
||||
|
||||
|
|
@ -1041,6 +1194,7 @@ export class FluentActionHandlers extends Component {
|
|||
"scheduledDate",
|
||||
"completedDate",
|
||||
"recurrence",
|
||||
"id", // Block ID for timer tracking
|
||||
];
|
||||
|
||||
for (const field of metadataFields) {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ const VIEW_MODE_CONFIG: Record<string, ViewMode[]> = {
|
|||
today: ["list", "tree", "kanban", "calendar"],
|
||||
upcoming: ["list", "tree", "kanban", "calendar"],
|
||||
flagged: ["list", "tree", "kanban", "calendar"],
|
||||
"working-on": ["list", "tree", "kanban", "calendar"],
|
||||
// Projects: empty by default (overview mode), but FluentTaskView enables modes when a project is selected
|
||||
projects: [],
|
||||
|
||||
|
|
@ -109,7 +110,7 @@ export class FluentComponentManager extends Component {
|
|||
onTaskContextMenu: (event: MouseEvent, task: Task) => void;
|
||||
onKanbanTaskStatusUpdate: (
|
||||
taskId: string,
|
||||
newStatusMark: string,
|
||||
newStatusMark: string
|
||||
) => void;
|
||||
};
|
||||
|
||||
|
|
@ -123,15 +124,15 @@ export class FluentComponentManager extends Component {
|
|||
onTaskCompleted: (task: Task) => void;
|
||||
onTaskUpdate: (
|
||||
originalTask: Task,
|
||||
updatedTask: Task,
|
||||
updatedTask: Task
|
||||
) => Promise<void>;
|
||||
onTaskContextMenu: (event: MouseEvent, task: Task) => void;
|
||||
onKanbanTaskStatusUpdate: (
|
||||
taskId: string,
|
||||
newStatusMark: string,
|
||||
newStatusMark: string
|
||||
) => void;
|
||||
},
|
||||
private selectionManager?: TaskSelectionManager,
|
||||
private selectionManager?: TaskSelectionManager
|
||||
) {
|
||||
super();
|
||||
this.viewHandlers = viewHandlers;
|
||||
|
|
@ -169,7 +170,7 @@ export class FluentComponentManager extends Component {
|
|||
this.app,
|
||||
this.plugin,
|
||||
this.contentArea,
|
||||
viewHandlers,
|
||||
viewHandlers
|
||||
);
|
||||
this.parentView.addChild(this.viewComponentManager);
|
||||
|
||||
|
|
@ -190,7 +191,7 @@ export class FluentComponentManager extends Component {
|
|||
if (originalTask && updatedTask) {
|
||||
await this.viewHandlers.onTaskUpdate(
|
||||
originalTask,
|
||||
updatedTask,
|
||||
updatedTask
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
@ -198,7 +199,7 @@ export class FluentComponentManager extends Component {
|
|||
if (task) this.viewHandlers.onTaskContextMenu(event, task);
|
||||
},
|
||||
selectionManager: this.selectionManager,
|
||||
},
|
||||
}
|
||||
);
|
||||
this.parentView.addChild(this.contentComponent);
|
||||
this.contentComponent.load();
|
||||
|
|
@ -219,14 +220,14 @@ export class FluentComponentManager extends Component {
|
|||
if (originalTask && updatedTask) {
|
||||
await this.viewHandlers.onTaskUpdate(
|
||||
originalTask,
|
||||
updatedTask,
|
||||
updatedTask
|
||||
);
|
||||
}
|
||||
},
|
||||
onTaskContextMenu: (event, task) => {
|
||||
if (task) this.viewHandlers.onTaskContextMenu(event, task);
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
this.parentView.addChild(this.forecastComponent);
|
||||
this.forecastComponent.load();
|
||||
|
|
@ -247,14 +248,14 @@ export class FluentComponentManager extends Component {
|
|||
if (originalTask && updatedTask) {
|
||||
await this.viewHandlers.onTaskUpdate(
|
||||
originalTask,
|
||||
updatedTask,
|
||||
updatedTask
|
||||
);
|
||||
}
|
||||
},
|
||||
onTaskContextMenu: (event, task) => {
|
||||
if (task) this.viewHandlers.onTaskContextMenu(event, task);
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
this.parentView.addChild(this.tagsComponent);
|
||||
this.tagsComponent.load();
|
||||
|
|
@ -275,14 +276,14 @@ export class FluentComponentManager extends Component {
|
|||
if (originalTask && updatedTask) {
|
||||
await this.viewHandlers.onTaskUpdate(
|
||||
originalTask,
|
||||
updatedTask,
|
||||
updatedTask
|
||||
);
|
||||
}
|
||||
},
|
||||
onTaskContextMenu: (event, task) => {
|
||||
if (task) this.viewHandlers.onTaskContextMenu(event, task);
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
this.parentView.addChild(this.projectsComponent);
|
||||
this.projectsComponent.load();
|
||||
|
|
@ -303,14 +304,14 @@ export class FluentComponentManager extends Component {
|
|||
if (originalTask && updatedTask) {
|
||||
await this.viewHandlers.onTaskUpdate(
|
||||
originalTask,
|
||||
updatedTask,
|
||||
updatedTask
|
||||
);
|
||||
}
|
||||
},
|
||||
onTaskContextMenu: (event, task) => {
|
||||
if (task) this.viewHandlers.onTaskContextMenu(event, task);
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
this.parentView.addChild(this.reviewComponent);
|
||||
this.reviewComponent.load();
|
||||
|
|
@ -336,7 +337,7 @@ export class FluentComponentManager extends Component {
|
|||
onEventContextMenu: (ev: MouseEvent, event: Task) => {
|
||||
if (event) this.viewHandlers.onTaskContextMenu(ev, event);
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
this.parentView.addChild(this.calendarComponent);
|
||||
|
||||
|
|
@ -350,7 +351,7 @@ export class FluentComponentManager extends Component {
|
|||
onTaskStatusUpdate: async (taskId, newStatusMark) => {
|
||||
this.viewHandlers.onKanbanTaskStatusUpdate(
|
||||
taskId,
|
||||
newStatusMark,
|
||||
newStatusMark
|
||||
);
|
||||
},
|
||||
onTaskSelected: (task) => {
|
||||
|
|
@ -362,7 +363,7 @@ export class FluentComponentManager extends Component {
|
|||
onTaskContextMenu: (event, task) => {
|
||||
if (task) this.viewHandlers.onTaskContextMenu(event, task);
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
this.parentView.addChild(this.kanbanComponent);
|
||||
|
||||
|
|
@ -377,7 +378,7 @@ export class FluentComponentManager extends Component {
|
|||
this.viewHandlers.onTaskCompleted(task),
|
||||
onTaskContextMenu: (event: MouseEvent, task: Task) =>
|
||||
this.viewHandlers.onTaskContextMenu(event, task),
|
||||
},
|
||||
}
|
||||
);
|
||||
this.parentView.addChild(this.ganttComponent);
|
||||
this.ganttComponent.load();
|
||||
|
|
@ -408,7 +409,7 @@ export class FluentComponentManager extends Component {
|
|||
// Smart hiding - only hide currently visible component (unless initial hide)
|
||||
if (!isInitialHide && this.currentVisibleComponent) {
|
||||
console.log(
|
||||
"[FluentComponent] Smart hide - only hiding current visible component",
|
||||
"[FluentComponent] Smart hide - only hiding current visible component"
|
||||
);
|
||||
this.currentVisibleComponent.containerEl?.hide();
|
||||
this.currentVisibleComponent = null;
|
||||
|
|
@ -416,7 +417,7 @@ export class FluentComponentManager extends Component {
|
|||
// Hide all components
|
||||
console.log(
|
||||
"[FluentComponent] Hiding all components",
|
||||
isInitialHide ? "(initial hide)" : "",
|
||||
isInitialHide ? "(initial hide)" : ""
|
||||
);
|
||||
this.contentComponent?.containerEl.hide();
|
||||
this.forecastComponent?.containerEl.hide();
|
||||
|
|
@ -454,20 +455,20 @@ export class FluentComponentManager extends Component {
|
|||
filteredTasks: Task[],
|
||||
currentFilterState: RootFilterState | null,
|
||||
viewMode: ViewMode,
|
||||
project?: string | null,
|
||||
project?: string | null
|
||||
): void {
|
||||
console.log(
|
||||
"[FluentComponent] switchView called with:",
|
||||
viewId,
|
||||
"viewMode:",
|
||||
viewMode,
|
||||
viewMode
|
||||
);
|
||||
|
||||
// Remove transient overlays (loading/error/empty) before showing components
|
||||
if (this.contentArea) {
|
||||
this.contentArea
|
||||
.querySelectorAll(
|
||||
".tg-fluent-loading, .tg-fluent-error-state, .tg-fluent-empty-state",
|
||||
".tg-fluent-loading, .tg-fluent-error-state, .tg-fluent-empty-state"
|
||||
)
|
||||
.forEach((el) => el.remove());
|
||||
}
|
||||
|
|
@ -500,7 +501,7 @@ export class FluentComponentManager extends Component {
|
|||
viewId,
|
||||
tasks,
|
||||
filteredTasks,
|
||||
viewMode,
|
||||
viewMode
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -521,7 +522,7 @@ export class FluentComponentManager extends Component {
|
|||
this.app,
|
||||
this.plugin,
|
||||
twoColumnConfig,
|
||||
viewId,
|
||||
viewId
|
||||
);
|
||||
this.parentView.addChild(twoColumnComponent);
|
||||
|
||||
|
|
@ -539,7 +540,7 @@ export class FluentComponentManager extends Component {
|
|||
const twoColumnComponent = this.twoColumnViewComponents.get(viewId);
|
||||
if (!twoColumnComponent) {
|
||||
console.warn(
|
||||
`[FluentComponent] Missing two column component for view ${viewId}`,
|
||||
`[FluentComponent] Missing two column component for view ${viewId}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -551,7 +552,7 @@ export class FluentComponentManager extends Component {
|
|||
// Check if it's a special view managed by ViewComponentManager
|
||||
if (this.viewComponentManager.isSpecialView(viewId)) {
|
||||
targetComponent = this.viewComponentManager.showComponent(
|
||||
viewId,
|
||||
viewId
|
||||
) as ManagedViewComponent | null;
|
||||
} else if (
|
||||
specificViewType === "forecast" ||
|
||||
|
|
@ -573,13 +574,13 @@ export class FluentComponentManager extends Component {
|
|||
// 2. Without project selection (from view navigation) → use ProjectsComponent for overview
|
||||
if (project) {
|
||||
console.log(
|
||||
"[FluentComponent] Projects view with selected project - using ContentComponent",
|
||||
"[FluentComponent] Projects view with selected project - using ContentComponent"
|
||||
);
|
||||
targetComponent = this.contentComponent;
|
||||
modeForComponent = viewId;
|
||||
} else {
|
||||
console.log(
|
||||
"[FluentComponent] Projects view without selection - using ProjectsComponent",
|
||||
"[FluentComponent] Projects view without selection - using ProjectsComponent"
|
||||
);
|
||||
targetComponent = this.projectsComponent;
|
||||
modeForComponent = viewId;
|
||||
|
|
@ -612,13 +613,13 @@ export class FluentComponentManager extends Component {
|
|||
|
||||
console.log(
|
||||
"[FluentComponent] Target component determined:",
|
||||
targetComponent?.constructor?.name,
|
||||
targetComponent?.constructor?.name
|
||||
);
|
||||
|
||||
if (targetComponent) {
|
||||
console.log(
|
||||
`[FluentComponent] Activating component for view ${viewId}:`,
|
||||
targetComponent.constructor.name,
|
||||
targetComponent.constructor.name
|
||||
);
|
||||
targetComponent.containerEl.show();
|
||||
this.currentVisibleComponent = targetComponent;
|
||||
|
|
@ -629,7 +630,7 @@ export class FluentComponentManager extends Component {
|
|||
this.topNavigation
|
||||
) {
|
||||
console.log(
|
||||
"[FluentComponent] Setting TopNavigation reference for ContentComponent",
|
||||
"[FluentComponent] Setting TopNavigation reference for ContentComponent"
|
||||
);
|
||||
this.contentComponent.setTopNavigation(this.topNavigation);
|
||||
}
|
||||
|
|
@ -637,7 +638,7 @@ export class FluentComponentManager extends Component {
|
|||
// Set view mode first for ContentComponent
|
||||
if (typeof targetComponent.setViewMode === "function") {
|
||||
console.log(
|
||||
`[FluentComponent] Setting view mode for ${viewId} to ${modeForComponent}`,
|
||||
`[FluentComponent] Setting view mode for ${viewId} to ${modeForComponent}`
|
||||
);
|
||||
targetComponent.setViewMode(modeForComponent as any, project);
|
||||
}
|
||||
|
|
@ -648,14 +649,14 @@ export class FluentComponentManager extends Component {
|
|||
// Tags view: uses filtered tasks for tag index (left sidebar), all tasks for tree view lookup
|
||||
if (viewId === "tags") {
|
||||
console.log(
|
||||
`[FluentComponent] Calling setTasks for ${viewId} with FILTERED tasks (${filteredTasks.length}) and ALL tasks (${tasks.length})`,
|
||||
`[FluentComponent] Calling setTasks for ${viewId} with FILTERED tasks (${filteredTasks.length}) and ALL tasks (${tasks.length})`
|
||||
);
|
||||
targetComponent.setTasks(filteredTasks, tasks);
|
||||
} else if (viewId === "review") {
|
||||
// Review view still needs all tasks
|
||||
console.log(
|
||||
`[FluentComponent] Calling setTasks for ${viewId} with ALL tasks:`,
|
||||
tasks.length,
|
||||
tasks.length
|
||||
);
|
||||
targetComponent.setTasks(tasks);
|
||||
} else if (viewId === "projects" && !project) {
|
||||
|
|
@ -663,7 +664,7 @@ export class FluentComponentManager extends Component {
|
|||
// First param: filtered tasks (for building sidebar project index)
|
||||
// Second param: all tasks (for tree view parent-child lookup)
|
||||
console.log(
|
||||
`[FluentComponent] Calling setTasks for projects with FILTERED tasks (${filteredTasks.length}) and ALL tasks (${tasks.length})`,
|
||||
`[FluentComponent] Calling setTasks for projects with FILTERED tasks (${filteredTasks.length}) and ALL tasks (${tasks.length})`
|
||||
);
|
||||
targetComponent.setTasks(filteredTasks, tasks);
|
||||
} else {
|
||||
|
|
@ -672,14 +673,14 @@ export class FluentComponentManager extends Component {
|
|||
// Forecast view: remove badge-only items
|
||||
if (viewId === "forecast") {
|
||||
filteredTasksLocal = filteredTasksLocal.filter(
|
||||
(task) => !(task as any).badge,
|
||||
(task) => !(task as any).badge
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
"[FluentComponent] Calling setTasks with filtered:",
|
||||
filteredTasksLocal.length,
|
||||
"all:",
|
||||
tasks.length,
|
||||
tasks.length
|
||||
);
|
||||
targetComponent.setTasks(filteredTasksLocal, tasks);
|
||||
}
|
||||
|
|
@ -701,8 +702,8 @@ export class FluentComponentManager extends Component {
|
|||
tasks,
|
||||
viewId as any,
|
||||
this.plugin,
|
||||
filterOptions,
|
||||
),
|
||||
filterOptions
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -715,7 +716,7 @@ export class FluentComponentManager extends Component {
|
|||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`[FluentComponent] No target component found for viewId: ${viewId}`,
|
||||
`[FluentComponent] No target component found for viewId: ${viewId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -727,11 +728,11 @@ export class FluentComponentManager extends Component {
|
|||
viewId: string,
|
||||
tasks: Task[],
|
||||
filteredTasks: Task[],
|
||||
viewMode: ViewMode,
|
||||
viewMode: ViewMode
|
||||
): void {
|
||||
console.log(
|
||||
"[FluentComponent] renderContentWithViewMode called, viewMode:",
|
||||
viewMode,
|
||||
viewMode
|
||||
);
|
||||
|
||||
// Hide current component
|
||||
|
|
@ -763,7 +764,7 @@ export class FluentComponentManager extends Component {
|
|||
|
||||
console.log(
|
||||
"[FluentComponent] Setting tasks to ContentComponent, filtered:",
|
||||
filteredTasks.length,
|
||||
filteredTasks.length
|
||||
);
|
||||
this.contentComponent.setTasks(filteredTasks, tasks);
|
||||
this.currentVisibleComponent = this.contentComponent;
|
||||
|
|
@ -786,14 +787,14 @@ export class FluentComponentManager extends Component {
|
|||
(cycleId: string | null) => {
|
||||
console.log(
|
||||
"[FluentComponent] Cycle changed to:",
|
||||
cycleId,
|
||||
cycleId
|
||||
);
|
||||
// Update kanban component's selected cycle
|
||||
this.kanbanComponent["selectedCycleId"] = cycleId;
|
||||
this.kanbanComponent["saveCycleSelection"]();
|
||||
// Re-render columns with new cycle
|
||||
this.kanbanComponent["renderColumns"]();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Show cycle selector with current selection
|
||||
|
|
@ -803,7 +804,7 @@ export class FluentComponentManager extends Component {
|
|||
console.log(
|
||||
"[FluentComponent] Setting",
|
||||
filteredTasks.length,
|
||||
"tasks to kanban",
|
||||
"tasks to kanban"
|
||||
);
|
||||
this.kanbanComponent.setTasks(filteredTasks);
|
||||
this.currentVisibleComponent = this.kanbanComponent;
|
||||
|
|
@ -812,11 +813,11 @@ export class FluentComponentManager extends Component {
|
|||
case "calendar":
|
||||
// Use CalendarComponent
|
||||
console.log(
|
||||
"[FluentComponent] Calendar mode in renderContentWithViewMode",
|
||||
"[FluentComponent] Calendar mode in renderContentWithViewMode"
|
||||
);
|
||||
if (!this.calendarComponent) {
|
||||
console.log(
|
||||
"[FluentComponent] No calendar component available!",
|
||||
"[FluentComponent] No calendar component available!"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -827,7 +828,7 @@ export class FluentComponentManager extends Component {
|
|||
console.log(
|
||||
"[FluentComponent] Setting",
|
||||
filteredTasks.length,
|
||||
"tasks to calendar",
|
||||
"tasks to calendar"
|
||||
);
|
||||
this.calendarComponent.setTasks(filteredTasks);
|
||||
this.currentVisibleComponent = this.calendarComponent;
|
||||
|
|
@ -843,7 +844,7 @@ export class FluentComponentManager extends Component {
|
|||
viewId: string,
|
||||
tasks: Task[],
|
||||
filteredTasks: Task[],
|
||||
viewMode: ViewMode,
|
||||
viewMode: ViewMode
|
||||
): void {
|
||||
// Content-based views (list/tree/kanban/calendar)
|
||||
if (this.isContentBasedView(viewId)) {
|
||||
|
|
@ -860,7 +861,7 @@ export class FluentComponentManager extends Component {
|
|||
this.contentComponent?.setTasks?.(
|
||||
filteredTasks,
|
||||
tasks,
|
||||
true,
|
||||
true
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
|
@ -936,7 +937,7 @@ export class FluentComponentManager extends Component {
|
|||
*/
|
||||
renderErrorState(
|
||||
context: ErrorContext | string,
|
||||
onRetry: () => void,
|
||||
onRetry: () => void
|
||||
): void {
|
||||
if (!this.contentArea) return;
|
||||
|
||||
|
|
@ -1001,7 +1002,7 @@ export class FluentComponentManager extends Component {
|
|||
*/
|
||||
private createErrorContext(
|
||||
errorEl: HTMLElement,
|
||||
errorContext: ErrorContext,
|
||||
errorContext: ErrorContext
|
||||
): void {
|
||||
if (
|
||||
!errorContext.viewId &&
|
||||
|
|
@ -1043,7 +1044,7 @@ export class FluentComponentManager extends Component {
|
|||
*/
|
||||
private createErrorMessage(
|
||||
errorEl: HTMLElement,
|
||||
errorContext: ErrorContext,
|
||||
errorContext: ErrorContext
|
||||
): void {
|
||||
const userMessage =
|
||||
errorContext.userMessage ||
|
||||
|
|
@ -1061,7 +1062,7 @@ export class FluentComponentManager extends Component {
|
|||
*/
|
||||
private createTechnicalDetails(
|
||||
errorEl: HTMLElement,
|
||||
errorContext: ErrorContext,
|
||||
errorContext: ErrorContext
|
||||
): void {
|
||||
if (!errorContext.originalError) return;
|
||||
|
||||
|
|
@ -1159,7 +1160,7 @@ export class FluentComponentManager extends Component {
|
|||
emptyEl.createDiv({
|
||||
cls: "tg-fluent-empty-description",
|
||||
text: t(
|
||||
"Create your first task to get started with Task Genius",
|
||||
"Create your first task to get started with Task Genius"
|
||||
),
|
||||
});
|
||||
|
||||
|
|
@ -1193,12 +1194,12 @@ export class FluentComponentManager extends Component {
|
|||
*/
|
||||
getAvailableModesForView(
|
||||
viewId: string,
|
||||
selectedProject?: string | null,
|
||||
selectedProject?: string | null
|
||||
): ViewMode[] {
|
||||
// Check for special two-column views
|
||||
const viewConfig = getViewSettingOrDefault(
|
||||
this.plugin,
|
||||
viewId as ViewMode,
|
||||
viewId as ViewMode
|
||||
);
|
||||
if (viewConfig?.specificConfig?.viewType === "twocolumn") {
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { RootFilterState } from "@/components/features/task/filter/ViewTaskFilte
|
|||
import { isDataflowEnabled } from "@/dataflow/createDataflow";
|
||||
import { Events, on } from "@/dataflow/events/Events";
|
||||
import { sortTasks } from "@/commands/sortTaskCommands";
|
||||
import { TaskTimerManager } from "@/managers/timer-manager";
|
||||
|
||||
/**
|
||||
* FluentDataManager - Stateless data loading, filtering, and sorting executor
|
||||
|
|
@ -121,12 +122,20 @@ export class FluentDataManager extends Component {
|
|||
const viewId = this.getCurrentViewId();
|
||||
const filterState = this.getCurrentFilterState();
|
||||
|
||||
console.log(`[FluentData] applyFilters called for viewId: ${viewId}, total tasks: ${tasks.length}`);
|
||||
|
||||
// Build filter options
|
||||
const filterOptions: any = {
|
||||
textQuery:
|
||||
filterState.filterInputValue || filterState.searchQuery || "",
|
||||
};
|
||||
|
||||
// Always enable v2Filters for Working-on so the special filter runs
|
||||
if (viewId === "working-on") {
|
||||
filterOptions.v2Filters = filterOptions.v2Filters || {};
|
||||
console.log("[FluentData] Working-on view detected, will apply special filter");
|
||||
}
|
||||
|
||||
// Apply advanced filters from the filter popover/modal
|
||||
if (
|
||||
filterState.currentFilterState &&
|
||||
|
|
@ -161,8 +170,15 @@ export class FluentDataManager extends Component {
|
|||
filterOptions,
|
||||
);
|
||||
|
||||
// Apply additional fluent-specific filters if needed
|
||||
if (filterOptions.v2Filters) {
|
||||
// Always apply working-on filter when in that view, regardless of v2Filters
|
||||
if (viewId === "working-on") {
|
||||
console.log(`[FluentData] Before applyWorkingOnFilter: ${filteredTasks.length} tasks`);
|
||||
filteredTasks = this.applyWorkingOnFilter(filteredTasks);
|
||||
console.log(`[FluentData] After applyWorkingOnFilter: ${filteredTasks.length} tasks`);
|
||||
}
|
||||
|
||||
// Apply additional fluent-specific filters if needed (but skip working-on duplicate filter)
|
||||
if (filterOptions.v2Filters && viewId !== "working-on") {
|
||||
filteredTasks = this.applyV2Filters(
|
||||
filteredTasks,
|
||||
filterOptions.v2Filters,
|
||||
|
|
@ -170,7 +186,7 @@ export class FluentDataManager extends Component {
|
|||
}
|
||||
|
||||
console.log(
|
||||
`[FluentData] Filtered ${filteredTasks.length} tasks from ${tasks.length} total`,
|
||||
`[FluentData] Final filtered result: ${filteredTasks.length} tasks from ${tasks.length} total for viewId: ${viewId}`,
|
||||
);
|
||||
|
||||
// Apply sorting (global default first, then view-specific)
|
||||
|
|
@ -192,6 +208,12 @@ export class FluentDataManager extends Component {
|
|||
const normalizeProjectId = (value?: string | null): string =>
|
||||
(value ?? "").trim().toLowerCase();
|
||||
|
||||
// Working-on View Special Filter
|
||||
// Shows tasks that are: 1) In Progress status OR 2) Have an active timer
|
||||
if (viewId === "working-on") {
|
||||
result = this.applyWorkingOnFilter(result);
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if (filters.status && filters.status !== "all") {
|
||||
switch (filters.status) {
|
||||
|
|
@ -282,6 +304,64 @@ export class FluentDataManager extends Component {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply Working-on view filter
|
||||
* Shows tasks that are: 1) In Progress status OR 2) Have an active timer (running or paused)
|
||||
* @param tasks - Tasks to filter
|
||||
* @returns Filtered tasks for Working-on view
|
||||
*/
|
||||
private applyWorkingOnFilter(tasks: Task[]): Task[] {
|
||||
console.log(`[FluentData] applyWorkingOnFilter called with ${tasks.length} tasks`);
|
||||
|
||||
// Get in-progress status marks from settings
|
||||
const inProgressMarks = (
|
||||
this.plugin.settings.taskStatuses.inProgress || ">|/"
|
||||
)
|
||||
.split("|")
|
||||
.map((m) => m.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
console.log(`[FluentData] In-progress marks: ${inProgressMarks.join(", ")}`);
|
||||
|
||||
// Get timer manager instance if timer feature is enabled
|
||||
let timerManager: TaskTimerManager | null = null;
|
||||
let activeTimerBlockIds: Set<string> = new Set();
|
||||
|
||||
if (this.plugin.settings.taskTimer?.enabled) {
|
||||
timerManager = new TaskTimerManager(this.plugin.settings.taskTimer);
|
||||
|
||||
// Get all active timers and build a set of block IDs
|
||||
const activeTimers = timerManager.getAllActiveTimers();
|
||||
console.log(`[FluentData] Active timers count: ${activeTimers.length}`);
|
||||
for (const timer of activeTimers) {
|
||||
if (timer.status === "running" || timer.status === "paused") {
|
||||
activeTimerBlockIds.add(timer.blockId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = tasks.filter((task) => {
|
||||
// Skip completed tasks
|
||||
if (task.completed) return false;
|
||||
|
||||
// Condition 1: Task has In Progress status
|
||||
const taskStatus = task.status || " ";
|
||||
const isInProgress = inProgressMarks.includes(taskStatus);
|
||||
if (isInProgress) return true;
|
||||
|
||||
// Condition 2: Task has an active timer
|
||||
const blockId = task.metadata?.id;
|
||||
if (blockId && activeTimerBlockIds.has(blockId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
console.log(`[FluentData] applyWorkingOnFilter result: ${result.length} tasks after filter`);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply sorting to tasks (pure function - returns sorted tasks)
|
||||
* Applies global default sorting first, then view-specific sorting
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { SuggestManager } from "@/components/ui/suggest";
|
|||
import { EmbeddableMarkdownEditor } from "@/editor-extensions/core/markdown-editor";
|
||||
import { extractMetadataFromFilter } from "../../task/filter/filter-metadata-extractor";
|
||||
import type { RootFilterState } from "../../task/filter/ViewTaskFilter";
|
||||
import { TaskTimerManager } from "@/managers/timer-manager";
|
||||
|
||||
/**
|
||||
* Quick capture save strategy types
|
||||
|
|
@ -553,6 +554,25 @@ export abstract class BaseQuickCaptureModal extends Modal {
|
|||
|
||||
let targetFile = this.tempTargetFilePath;
|
||||
|
||||
// Timer auto-start: prepare block ID if enabled (checkbox mode only)
|
||||
let timerBlockId: string | undefined;
|
||||
const shouldAutoStartTimer =
|
||||
this.currentMode === "checkbox" &&
|
||||
this.plugin.settings.taskTimer?.enabled &&
|
||||
this.plugin.settings.quickCapture.autoStartTimer;
|
||||
|
||||
if (shouldAutoStartTimer) {
|
||||
const timerManager = new TaskTimerManager(
|
||||
this.plugin.settings.taskTimer,
|
||||
);
|
||||
timerBlockId = timerManager.generateBlockId();
|
||||
// Append block ID to the content
|
||||
processedContent = this.appendBlockIdToContent(
|
||||
processedContent,
|
||||
timerBlockId,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle file mode
|
||||
if (this.currentMode === "file" && this.taskMetadata.customFileName) {
|
||||
targetFile = processDateTemplates(this.taskMetadata.customFileName);
|
||||
|
|
@ -606,6 +626,64 @@ export abstract class BaseQuickCaptureModal extends Modal {
|
|||
};
|
||||
|
||||
await saveCapture(this.app, processedContent, captureOptions);
|
||||
|
||||
// Start timer after successful save (checkbox mode only)
|
||||
if (shouldAutoStartTimer && timerBlockId) {
|
||||
await this.startTimerForNewTask(targetFile, timerBlockId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append block ID to content for timer tracking
|
||||
*/
|
||||
private appendBlockIdToContent(content: string, blockId: string): string {
|
||||
// Check if content already has a block ID
|
||||
if (/\s\^[\w-]+\s*$/.test(content)) {
|
||||
return content;
|
||||
}
|
||||
return `${content} ^${blockId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start timer for a newly created task
|
||||
*/
|
||||
private async startTimerForNewTask(
|
||||
targetFile: string,
|
||||
blockId: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const timerManager = new TaskTimerManager(
|
||||
this.plugin.settings.taskTimer,
|
||||
);
|
||||
|
||||
// Resolve the actual file path
|
||||
let resolvedPath = targetFile;
|
||||
if (this.plugin.settings.quickCapture.targetType === "daily-note") {
|
||||
const dateStr = moment().format(
|
||||
this.plugin.settings.quickCapture.dailyNoteSettings.format,
|
||||
);
|
||||
const folder =
|
||||
this.plugin.settings.quickCapture.dailyNoteSettings.folder;
|
||||
resolvedPath = folder
|
||||
? `${folder}/${dateStr}.md`
|
||||
: `${dateStr}.md`;
|
||||
}
|
||||
|
||||
// Ensure .md extension
|
||||
if (!resolvedPath.endsWith(".md")) {
|
||||
resolvedPath += ".md";
|
||||
}
|
||||
|
||||
// Start the timer
|
||||
timerManager.startTimer(resolvedPath, blockId);
|
||||
|
||||
new Notice(t("Timer started for new task"));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[QuickCapture] Failed to start timer for new task:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -304,6 +304,31 @@ export function renderQuickCaptureSettingsTab(
|
|||
}),
|
||||
);
|
||||
|
||||
// Timer integration section (only show if task timer is enabled)
|
||||
if (settingTab.plugin.settings.taskTimer?.enabled) {
|
||||
new Setting(containerEl).setName(t("Timer Integration")).setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Auto-start timer"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Automatically start the timer when creating a new task via quick capture (checkbox mode only)",
|
||||
),
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(
|
||||
settingTab.plugin.settings.quickCapture
|
||||
.autoStartTimer ?? false,
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
settingTab.plugin.settings.quickCapture.autoStartTimer =
|
||||
value;
|
||||
settingTab.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// File creation mode settings
|
||||
new Setting(containerEl).setName(t("File Creation Mode")).setHeading();
|
||||
|
||||
|
|
|
|||
538
src/components/features/task/view/TimerStatisticsPanel.ts
Normal file
538
src/components/features/task/view/TimerStatisticsPanel.ts
Normal file
|
|
@ -0,0 +1,538 @@
|
|||
import { Component, setIcon } from "obsidian";
|
||||
import TaskProgressBarPlugin from "@/index";
|
||||
import { Task } from "@/types/task";
|
||||
import {
|
||||
CompletedTimerRecord,
|
||||
TaskTimerManager,
|
||||
TimerState,
|
||||
} from "@/managers/timer-manager";
|
||||
import { t } from "@/translations/helper";
|
||||
import "@/styles/timer-statistics.css";
|
||||
|
||||
interface TimerStatisticsOptions {
|
||||
onTaskClick?: (task: Task | null) => void;
|
||||
onTaskContextMenu?: (event: MouseEvent, task: Task | null) => void;
|
||||
}
|
||||
|
||||
export class TimerStatisticsPanel extends Component {
|
||||
public containerEl: HTMLElement;
|
||||
private plugin: TaskProgressBarPlugin;
|
||||
private tasks: Task[];
|
||||
private timerManager: TaskTimerManager;
|
||||
private hasActiveTimerInterval: boolean = false;
|
||||
private activeTimerSignature = "";
|
||||
private lastCleanup = 0;
|
||||
private onTaskClick?: (task: Task | null) => void;
|
||||
private onTaskContextMenu?: (event: MouseEvent, task: Task | null) => void;
|
||||
|
||||
constructor(
|
||||
private parentEl: HTMLElement,
|
||||
plugin: TaskProgressBarPlugin,
|
||||
tasks: Task[] = [],
|
||||
options: TimerStatisticsOptions = {}
|
||||
) {
|
||||
super();
|
||||
this.plugin = plugin;
|
||||
this.tasks = tasks;
|
||||
this.onTaskClick = options.onTaskClick;
|
||||
this.onTaskContextMenu = options.onTaskContextMenu;
|
||||
// Reuse the plugin's shared timer manager to keep settings and state consistent
|
||||
if (!this.plugin.taskTimerManager) {
|
||||
this.plugin.taskTimerManager = new TaskTimerManager(
|
||||
this.plugin.settings.taskTimer
|
||||
);
|
||||
} else {
|
||||
this.plugin.taskTimerManager.updateSettings(
|
||||
this.plugin.settings.taskTimer
|
||||
);
|
||||
}
|
||||
this.timerManager = this.plugin.taskTimerManager;
|
||||
}
|
||||
|
||||
onload() {
|
||||
this.containerEl = this.parentEl.createDiv({
|
||||
cls: "timer-statistics-panel",
|
||||
});
|
||||
|
||||
// Clean up old/orphaned timers before initial render
|
||||
this.timerManager.cleanup();
|
||||
this.lastCleanup = Date.now();
|
||||
|
||||
this.render();
|
||||
this.startUpdateInterval();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the tasks list
|
||||
*/
|
||||
public setTasks(tasks: Task[]) {
|
||||
this.tasks = tasks;
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic updates using registerInterval
|
||||
*/
|
||||
private startUpdateInterval() {
|
||||
if (this.hasActiveTimerInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hasActiveTimerInterval = true;
|
||||
|
||||
// Update every second - registerInterval auto-cleans on component unload
|
||||
this.registerInterval(
|
||||
window.setInterval(() => {
|
||||
this.updateTimerDisplays();
|
||||
}, 1000)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update timer displays without full re-render
|
||||
*/
|
||||
private updateTimerDisplays() {
|
||||
if (!this.containerEl) return;
|
||||
|
||||
// Update summary values
|
||||
const allTimers = this.timerManager.getAllActiveTimers();
|
||||
const signature = this.computeTimerSignature(allTimers);
|
||||
|
||||
// If timer list or statuses changed, re-render to add/remove items
|
||||
if (signature !== this.activeTimerSignature) {
|
||||
this.activeTimerSignature = signature;
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
let totalDuration = 0;
|
||||
let runningCount = 0;
|
||||
let pausedCount = 0;
|
||||
|
||||
allTimers.forEach((timer) => {
|
||||
totalDuration += this.timerManager.getCurrentDuration(timer.taskId);
|
||||
if (timer.status === "running") {
|
||||
runningCount++;
|
||||
} else if (timer.status === "paused") {
|
||||
pausedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
// Update summary card values
|
||||
const totalTimeEl = this.containerEl.querySelector(
|
||||
".timer-stats-card-total .timer-stats-card-value"
|
||||
);
|
||||
if (totalTimeEl) {
|
||||
totalTimeEl.textContent =
|
||||
this.timerManager.formatDuration(totalDuration);
|
||||
}
|
||||
|
||||
const runningCountEl = this.containerEl.querySelector(
|
||||
".timer-stats-card-running .timer-stats-card-value"
|
||||
);
|
||||
if (runningCountEl) {
|
||||
runningCountEl.textContent = String(runningCount);
|
||||
}
|
||||
|
||||
const pausedCountEl = this.containerEl.querySelector(
|
||||
".timer-stats-card-paused .timer-stats-card-value"
|
||||
);
|
||||
if (pausedCountEl) {
|
||||
pausedCountEl.textContent = String(pausedCount);
|
||||
}
|
||||
|
||||
const totalTimersEl = this.containerEl.querySelector(
|
||||
".timer-stats-card-count .timer-stats-card-value"
|
||||
);
|
||||
if (totalTimersEl) {
|
||||
totalTimersEl.textContent = String(allTimers.length);
|
||||
}
|
||||
|
||||
// Update individual timer durations
|
||||
this.containerEl
|
||||
.querySelectorAll(".timer-item[data-task-id]")
|
||||
.forEach((el) => {
|
||||
const taskId = el.getAttribute("data-task-id");
|
||||
if (taskId) {
|
||||
const duration =
|
||||
this.timerManager.getCurrentDuration(taskId);
|
||||
const durationEl = el.querySelector(".timer-item-duration");
|
||||
if (durationEl) {
|
||||
durationEl.textContent =
|
||||
this.timerManager.formatDuration(duration);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Periodic cleanup while panel is open
|
||||
const now = Date.now();
|
||||
if (now - this.lastCleanup > 15 * 60 * 1000) {
|
||||
this.timerManager.cleanup();
|
||||
this.lastCleanup = now;
|
||||
}
|
||||
}
|
||||
|
||||
private render() {
|
||||
this.containerEl.empty();
|
||||
|
||||
// Get all active timers
|
||||
const allTimers = this.timerManager.getAllActiveTimers();
|
||||
this.activeTimerSignature = this.computeTimerSignature(allTimers);
|
||||
const completedTimers = this.timerManager.getRecentCompletedTimers(50);
|
||||
|
||||
// Header
|
||||
const headerEl = this.containerEl.createDiv({
|
||||
cls: "timer-stats-header",
|
||||
});
|
||||
headerEl.createEl("h2", {
|
||||
cls: "timer-stats-title",
|
||||
text: t("Timer Statistics"),
|
||||
});
|
||||
|
||||
// Summary section
|
||||
const summaryEl = this.containerEl.createDiv({
|
||||
cls: "timer-stats-summary",
|
||||
});
|
||||
this.renderSummary(summaryEl, allTimers);
|
||||
|
||||
// Active timers section
|
||||
const activeSection = this.containerEl.createDiv({
|
||||
cls: "timer-stats-section",
|
||||
});
|
||||
|
||||
activeSection.createEl("h3", { text: t("Active Timers") });
|
||||
|
||||
if (allTimers.length === 0) {
|
||||
activeSection.createDiv({
|
||||
cls: "timer-stats-empty",
|
||||
text: t("No active timers"),
|
||||
});
|
||||
} else {
|
||||
const timerListEl = activeSection.createDiv({
|
||||
cls: "timer-stats-list",
|
||||
});
|
||||
|
||||
// Group timers by status
|
||||
const runningTimers = allTimers.filter(
|
||||
(timer) => timer.status === "running"
|
||||
);
|
||||
const pausedTimers = allTimers.filter(
|
||||
(timer) => timer.status === "paused"
|
||||
);
|
||||
|
||||
if (runningTimers.length > 0) {
|
||||
this.renderTimerGroup(
|
||||
timerListEl,
|
||||
t("Running"),
|
||||
runningTimers,
|
||||
"running"
|
||||
);
|
||||
}
|
||||
|
||||
if (pausedTimers.length > 0) {
|
||||
this.renderTimerGroup(
|
||||
timerListEl,
|
||||
t("Paused"),
|
||||
pausedTimers,
|
||||
"paused"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Completed timers section
|
||||
const completedSection = this.containerEl.createDiv({
|
||||
cls: "timer-stats-section",
|
||||
});
|
||||
completedSection.createEl("h3", { text: t("Completed Timers") });
|
||||
|
||||
if (completedTimers.length === 0) {
|
||||
completedSection.createDiv({
|
||||
cls: "timer-stats-empty",
|
||||
text: t("No completed timers"),
|
||||
});
|
||||
} else {
|
||||
this.renderCompletedTimers(completedSection, completedTimers);
|
||||
}
|
||||
}
|
||||
|
||||
private renderSummary(containerEl: HTMLElement, timers: TimerState[]) {
|
||||
// Calculate totals
|
||||
let totalDuration = 0;
|
||||
let runningCount = 0;
|
||||
let pausedCount = 0;
|
||||
|
||||
timers.forEach((timer) => {
|
||||
totalDuration += this.timerManager.getCurrentDuration(timer.taskId);
|
||||
if (timer.status === "running") {
|
||||
runningCount++;
|
||||
} else if (timer.status === "paused") {
|
||||
pausedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
// Summary cards
|
||||
const cardsEl = containerEl.createDiv({ cls: "timer-stats-cards" });
|
||||
|
||||
// Total time card
|
||||
const totalCard = cardsEl.createDiv({
|
||||
cls: "timer-stats-card timer-stats-card-total",
|
||||
});
|
||||
const totalIcon = totalCard.createDiv({ cls: "timer-stats-card-icon" });
|
||||
setIcon(totalIcon, "clock");
|
||||
totalCard.createDiv({
|
||||
cls: "timer-stats-card-value",
|
||||
text: this.timerManager.formatDuration(totalDuration),
|
||||
});
|
||||
totalCard.createDiv({
|
||||
cls: "timer-stats-card-label",
|
||||
text: t("Total Time"),
|
||||
});
|
||||
|
||||
// Running count card
|
||||
const runningCard = cardsEl.createDiv({
|
||||
cls: "timer-stats-card running timer-stats-card-running",
|
||||
});
|
||||
const runningIcon = runningCard.createDiv({
|
||||
cls: "timer-stats-card-icon",
|
||||
});
|
||||
setIcon(runningIcon, "play");
|
||||
runningCard.createDiv({
|
||||
cls: "timer-stats-card-value",
|
||||
text: String(runningCount),
|
||||
});
|
||||
runningCard.createDiv({
|
||||
cls: "timer-stats-card-label",
|
||||
text: t("Running"),
|
||||
});
|
||||
|
||||
// Paused count card
|
||||
const pausedCard = cardsEl.createDiv({
|
||||
cls: "timer-stats-card paused timer-stats-card-paused",
|
||||
});
|
||||
const pausedIcon = pausedCard.createDiv({
|
||||
cls: "timer-stats-card-icon",
|
||||
});
|
||||
setIcon(pausedIcon, "pause");
|
||||
pausedCard.createDiv({
|
||||
cls: "timer-stats-card-value",
|
||||
text: String(pausedCount),
|
||||
});
|
||||
pausedCard.createDiv({
|
||||
cls: "timer-stats-card-label",
|
||||
text: t("Paused"),
|
||||
});
|
||||
|
||||
// Total timers card
|
||||
const totalTimersCard = cardsEl.createDiv({
|
||||
cls: "timer-stats-card timer-stats-card-count",
|
||||
});
|
||||
const totalTimersIcon = totalTimersCard.createDiv({
|
||||
cls: "timer-stats-card-icon",
|
||||
});
|
||||
setIcon(totalTimersIcon, "list");
|
||||
totalTimersCard.createDiv({
|
||||
cls: "timer-stats-card-value",
|
||||
text: String(timers.length),
|
||||
});
|
||||
totalTimersCard.createDiv({
|
||||
cls: "timer-stats-card-label",
|
||||
text: t("Total Timers"),
|
||||
});
|
||||
}
|
||||
|
||||
private renderTimerGroup(
|
||||
containerEl: HTMLElement,
|
||||
title: string,
|
||||
timers: TimerState[],
|
||||
status: string
|
||||
) {
|
||||
const groupEl = containerEl.createDiv({
|
||||
cls: `timer-group timer-group-${status}`,
|
||||
});
|
||||
|
||||
groupEl.createDiv({ cls: "timer-group-title", text: title });
|
||||
|
||||
const listEl = groupEl.createDiv({ cls: "timer-group-list" });
|
||||
|
||||
timers.forEach((timer) => {
|
||||
const timerEl = listEl.createDiv({
|
||||
cls: "timer-item",
|
||||
attr: { "data-task-id": timer.taskId },
|
||||
});
|
||||
|
||||
// Find associated task
|
||||
const task = this.tasks.find((t) => {
|
||||
const blockId = t.metadata?.id;
|
||||
return (
|
||||
blockId &&
|
||||
t.filePath === timer.filePath &&
|
||||
blockId === timer.blockId
|
||||
);
|
||||
});
|
||||
|
||||
// Click and context menu handlers to open details or menu
|
||||
if (this.onTaskClick) {
|
||||
this.registerDomEvent(timerEl, "click", () => {
|
||||
this.onTaskClick?.(task ?? null);
|
||||
});
|
||||
}
|
||||
if (this.onTaskContextMenu) {
|
||||
this.registerDomEvent(timerEl, "contextmenu", (event) => {
|
||||
console.log("contextmenu", event);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.onTaskContextMenu?.(event, task ?? null);
|
||||
});
|
||||
}
|
||||
|
||||
// Timer info
|
||||
const infoEl = timerEl.createDiv({ cls: "timer-item-info" });
|
||||
|
||||
// Task name or file path
|
||||
const nameEl = infoEl.createDiv({ cls: "timer-item-name" });
|
||||
if (task) {
|
||||
nameEl.setText(
|
||||
task.content || task.originalMarkdown || t("Untitled")
|
||||
);
|
||||
} else {
|
||||
// Show file path if task not found
|
||||
const fileName =
|
||||
timer.filePath.split("/").pop() || timer.filePath;
|
||||
nameEl.setText(fileName);
|
||||
}
|
||||
|
||||
// File path
|
||||
infoEl.createDiv({
|
||||
cls: "timer-item-path",
|
||||
text: timer.filePath,
|
||||
});
|
||||
|
||||
// Duration
|
||||
const duration = this.timerManager.getCurrentDuration(timer.taskId);
|
||||
timerEl.createDiv({
|
||||
cls: "timer-item-duration",
|
||||
text: this.timerManager.formatDuration(duration),
|
||||
});
|
||||
|
||||
// Status indicator
|
||||
const statusEl = timerEl.createDiv({
|
||||
cls: `timer-item-status timer-status-${timer.status}`,
|
||||
});
|
||||
setIcon(
|
||||
statusEl,
|
||||
timer.status === "running" ? "play-circle" : "pause-circle"
|
||||
);
|
||||
|
||||
// Actions
|
||||
const actionsEl = timerEl.createDiv({ cls: "timer-item-actions" });
|
||||
|
||||
if (timer.status === "running") {
|
||||
// Pause button
|
||||
const pauseBtn = actionsEl.createEl("button", {
|
||||
cls: "timer-action-btn",
|
||||
attr: { "aria-label": t("Pause") },
|
||||
});
|
||||
setIcon(pauseBtn, "pause");
|
||||
this.registerDomEvent(pauseBtn, "click", (evt) => {
|
||||
evt.stopPropagation();
|
||||
this.timerManager.pauseTimer(timer.taskId);
|
||||
this.render();
|
||||
});
|
||||
} else {
|
||||
// Resume button
|
||||
const resumeBtn = actionsEl.createEl("button", {
|
||||
cls: "timer-action-btn",
|
||||
attr: { "aria-label": t("Resume") },
|
||||
});
|
||||
setIcon(resumeBtn, "play");
|
||||
this.registerDomEvent(resumeBtn, "click", (evt) => {
|
||||
evt.stopPropagation();
|
||||
this.timerManager.resumeTimer(timer.taskId);
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
// Stop button
|
||||
const stopBtn = actionsEl.createEl("button", {
|
||||
cls: "timer-action-btn timer-action-stop",
|
||||
attr: { "aria-label": t("Stop") },
|
||||
});
|
||||
setIcon(stopBtn, "square");
|
||||
this.registerDomEvent(stopBtn, "click", (evt) => {
|
||||
evt.stopPropagation();
|
||||
this.timerManager.completeTimer(timer.taskId);
|
||||
this.render();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private renderCompletedTimers(
|
||||
containerEl: HTMLElement,
|
||||
completedTimers: CompletedTimerRecord[]
|
||||
) {
|
||||
const listEl = containerEl.createDiv({
|
||||
cls: "timer-completed-list",
|
||||
});
|
||||
|
||||
completedTimers.forEach((record) => {
|
||||
const itemEl = listEl.createDiv({ cls: "timer-completed-item" });
|
||||
|
||||
// Try to resolve the task to show nicer title
|
||||
const task = this.tasks.find((t) => {
|
||||
const blockId = t.metadata?.id;
|
||||
return (
|
||||
blockId &&
|
||||
t.filePath === record.filePath &&
|
||||
blockId === record.blockId
|
||||
);
|
||||
});
|
||||
|
||||
const infoEl = itemEl.createDiv({ cls: "timer-completed-info" });
|
||||
const nameEl = infoEl.createDiv({ cls: "timer-completed-name" });
|
||||
|
||||
if (task) {
|
||||
nameEl.setText(
|
||||
task.content || task.originalMarkdown || t("Untitled")
|
||||
);
|
||||
} else {
|
||||
const fileName =
|
||||
record.filePath.split("/").pop() || record.filePath;
|
||||
nameEl.setText(fileName);
|
||||
}
|
||||
|
||||
infoEl.createDiv({
|
||||
cls: "timer-completed-path",
|
||||
text: record.filePath,
|
||||
});
|
||||
|
||||
const metaEl = itemEl.createDiv({ cls: "timer-completed-meta" });
|
||||
|
||||
metaEl.createDiv({
|
||||
cls: "timer-completed-duration",
|
||||
text: this.timerManager.formatDuration(record.duration),
|
||||
});
|
||||
|
||||
metaEl.createDiv({
|
||||
cls: "timer-completed-time",
|
||||
text: `${t("Completed at")}: ${new Date(
|
||||
record.completedAt
|
||||
).toLocaleString()}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private computeTimerSignature(timers: TimerState[]): string {
|
||||
return timers
|
||||
.map(
|
||||
(timer) =>
|
||||
`${timer.taskId}:${timer.status}:${timer.segments.length}`
|
||||
)
|
||||
.sort()
|
||||
.join("|");
|
||||
}
|
||||
|
||||
onunload() {
|
||||
// Interval is auto-cleaned by registerInterval
|
||||
this.containerEl?.remove();
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,8 @@ import { t } from "@/translations/helper";
|
|||
import TaskProgressBarPlugin from "@/index";
|
||||
import { getInitialViewMode, saveViewMode } from "@/utils/ui/view-mode-utils";
|
||||
import { TopNavigation } from "@/components/features/fluent/components/FluentTopNavigation";
|
||||
import { TaskTimerManager } from "@/managers/timer-manager";
|
||||
import { TimerStatisticsPanel } from "./TimerStatisticsPanel";
|
||||
|
||||
import "@/styles/task-list.css";
|
||||
import "@/styles/tree-view.css";
|
||||
|
|
@ -77,6 +79,11 @@ export class ContentComponent extends Component {
|
|||
|
||||
// Top Navigation reference (for registering custom buttons)
|
||||
private topNavigation: TopNavigation | null = null;
|
||||
private workingOnSummaryEl: HTMLElement | null = null;
|
||||
|
||||
// Timer Statistics Panel
|
||||
private timerStatisticsPanel: TimerStatisticsPanel | null = null;
|
||||
private isShowingTimerStats: boolean = false;
|
||||
|
||||
// State
|
||||
private currentViewId: ViewMode = "inbox"; // Renamed from currentViewMode
|
||||
|
|
@ -93,7 +100,7 @@ export class ContentComponent extends Component {
|
|||
private parentEl: HTMLElement,
|
||||
private app: App,
|
||||
private plugin: TaskProgressBarPlugin,
|
||||
private params: ContentComponentParams = {},
|
||||
private params: ContentComponentParams = {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
|
@ -187,7 +194,7 @@ export class ContentComponent extends Component {
|
|||
{
|
||||
root: this.taskListEl, // Observe within the task list container
|
||||
threshold: 0.1, // Trigger when 10% of the marker is visible
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -198,11 +205,11 @@ export class ContentComponent extends Component {
|
|||
this.isTreeView = getInitialViewMode(
|
||||
this.app,
|
||||
this.plugin,
|
||||
this.currentViewId,
|
||||
this.currentViewId
|
||||
);
|
||||
// Update the toggle button icon to match the initial state
|
||||
const viewToggleBtn = this.headerEl?.querySelector(
|
||||
".view-toggle-btn",
|
||||
".view-toggle-btn"
|
||||
) as HTMLElement;
|
||||
if (viewToggleBtn) {
|
||||
setIcon(viewToggleBtn, this.isTreeView ? "git-branch" : "list");
|
||||
|
|
@ -212,7 +219,7 @@ export class ContentComponent extends Component {
|
|||
private toggleViewMode() {
|
||||
this.isTreeView = !this.isTreeView;
|
||||
const viewToggleBtn = this.headerEl.querySelector(
|
||||
".view-toggle-btn",
|
||||
".view-toggle-btn"
|
||||
) as HTMLElement;
|
||||
if (viewToggleBtn) {
|
||||
setIcon(viewToggleBtn, this.isTreeView ? "git-branch" : "list");
|
||||
|
|
@ -228,7 +235,7 @@ export class ContentComponent extends Component {
|
|||
if (this.isTreeView !== isTree) {
|
||||
this.isTreeView = isTree;
|
||||
const viewToggleBtn = this.headerEl?.querySelector(
|
||||
".view-toggle-btn",
|
||||
".view-toggle-btn"
|
||||
) as HTMLElement;
|
||||
if (viewToggleBtn) {
|
||||
setIcon(viewToggleBtn, this.isTreeView ? "git-branch" : "list");
|
||||
|
|
@ -284,7 +291,7 @@ export class ContentComponent extends Component {
|
|||
|
||||
// Show menu at button position
|
||||
const buttonEl = document.querySelector(
|
||||
'.fluent-nav-custom-buttons [aria-label="' + t("Group By") + '"]',
|
||||
'.fluent-nav-custom-buttons [aria-label="' + t("Group By") + '"]'
|
||||
) as HTMLElement;
|
||||
|
||||
if (buttonEl) {
|
||||
|
|
@ -292,7 +299,7 @@ export class ContentComponent extends Component {
|
|||
new MouseEvent("click", {
|
||||
clientX: buttonEl.getBoundingClientRect().left,
|
||||
clientY: buttonEl.getBoundingClientRect().bottom,
|
||||
}),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// Fallback: show at current mouse position
|
||||
|
|
@ -316,7 +323,7 @@ export class ContentComponent extends Component {
|
|||
this.taskGroups = groupTasksBy(
|
||||
this.filteredTasks,
|
||||
this.groupByDimension,
|
||||
this.plugin.settings,
|
||||
this.plugin.settings
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -350,7 +357,7 @@ export class ContentComponent extends Component {
|
|||
group: TaskGroup,
|
||||
containerEl: HTMLElement,
|
||||
taskMap: Map<string, Task>,
|
||||
level: number,
|
||||
level: number
|
||||
): void {
|
||||
// Create section element with level-specific class
|
||||
const sectionEl = containerEl.createDiv({
|
||||
|
|
@ -405,13 +412,13 @@ export class ContentComponent extends Component {
|
|||
setGroupExpandedState(
|
||||
this.currentViewId,
|
||||
group.key,
|
||||
group.isExpanded,
|
||||
group.isExpanded
|
||||
);
|
||||
|
||||
// Update icon
|
||||
setIcon(
|
||||
toggleEl,
|
||||
group.isExpanded ? "chevron-down" : "chevron-right",
|
||||
group.isExpanded ? "chevron-down" : "chevron-right"
|
||||
);
|
||||
|
||||
// Show/hide and render/destroy content
|
||||
|
|
@ -441,7 +448,7 @@ export class ContentComponent extends Component {
|
|||
group: TaskGroup,
|
||||
containerEl: HTMLElement,
|
||||
taskMap: Map<string, Task>,
|
||||
level: number,
|
||||
level: number
|
||||
): void {
|
||||
// Clear existing content
|
||||
containerEl.empty();
|
||||
|
|
@ -453,7 +460,7 @@ export class ContentComponent extends Component {
|
|||
childGroup,
|
||||
containerEl,
|
||||
taskMap,
|
||||
level + 1,
|
||||
level + 1
|
||||
);
|
||||
});
|
||||
} else {
|
||||
|
|
@ -463,7 +470,7 @@ export class ContentComponent extends Component {
|
|||
containerEl,
|
||||
this.plugin,
|
||||
this.app,
|
||||
this.currentViewId,
|
||||
this.currentViewId
|
||||
);
|
||||
|
||||
// Set up callbacks
|
||||
|
|
@ -481,7 +488,7 @@ export class ContentComponent extends Component {
|
|||
// Set up task update callback
|
||||
group.renderer.onTaskUpdate = async (
|
||||
originalTask: Task,
|
||||
updatedTask: Task,
|
||||
updatedTask: Task
|
||||
) => {
|
||||
if (this.params.onTaskUpdate) {
|
||||
await this.params.onTaskUpdate(originalTask, updatedTask);
|
||||
|
|
@ -493,7 +500,7 @@ export class ContentComponent extends Component {
|
|||
group.tasks,
|
||||
this.isTreeView,
|
||||
taskMap,
|
||||
t("No tasks in this group."),
|
||||
t("No tasks in this group.")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -506,7 +513,7 @@ export class ContentComponent extends Component {
|
|||
*/
|
||||
private destroyGroupContent(
|
||||
group: TaskGroup,
|
||||
containerEl: HTMLElement,
|
||||
containerEl: HTMLElement
|
||||
): void {
|
||||
// Recursively destroy child groups (depth-first)
|
||||
if (group.children && group.children.length > 0) {
|
||||
|
|
@ -541,7 +548,7 @@ export class ContentComponent extends Component {
|
|||
public setTasks(
|
||||
tasks: Task[],
|
||||
notFilteredTasks: Task[],
|
||||
forceRefresh: boolean = false,
|
||||
forceRefresh: boolean = false
|
||||
) {
|
||||
const updateSignatures = () => {
|
||||
this.lastAllTasksSignature = this.computeTaskSignature(tasks);
|
||||
|
|
@ -559,6 +566,10 @@ export class ContentComponent extends Component {
|
|||
this.notFilteredTasks = notFilteredTasks;
|
||||
updateSignatures();
|
||||
this.applyFilters();
|
||||
// Update statistics panel if showing
|
||||
if (this.timerStatisticsPanel) {
|
||||
this.timerStatisticsPanel.setTasks(this.filteredTasks);
|
||||
}
|
||||
this.debounceRefreshTaskList();
|
||||
return;
|
||||
}
|
||||
|
|
@ -566,7 +577,7 @@ export class ContentComponent extends Component {
|
|||
// If a force refresh is pending, skip non-forced updates
|
||||
if (this.pendingForceRefresh) {
|
||||
console.log(
|
||||
"ContentComponent: Skipping non-forced update, force refresh is pending",
|
||||
"ContentComponent: Skipping non-forced update, force refresh is pending"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -579,7 +590,7 @@ export class ContentComponent extends Component {
|
|||
nextNotFilteredSignature === this.lastNotFilteredTasksSignature
|
||||
) {
|
||||
console.log(
|
||||
"ContentComponent: Task signatures unchanged, skipping refresh",
|
||||
"ContentComponent: Task signatures unchanged, skipping refresh"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -589,11 +600,16 @@ export class ContentComponent extends Component {
|
|||
this.lastAllTasksSignature = nextAllSignature;
|
||||
this.lastNotFilteredTasksSignature = nextNotFilteredSignature;
|
||||
this.applyFilters();
|
||||
// Update statistics panel if showing
|
||||
if (this.timerStatisticsPanel) {
|
||||
this.timerStatisticsPanel.setTasks(this.filteredTasks);
|
||||
}
|
||||
this.debounceRefreshTaskList();
|
||||
}
|
||||
|
||||
// Updated method signature
|
||||
public setViewMode(viewId: ViewMode, project?: string | null) {
|
||||
const previousViewId = this.currentViewId;
|
||||
this.currentViewId = viewId;
|
||||
this.selectedProjectForView = project === undefined ? null : project;
|
||||
|
||||
|
|
@ -609,24 +625,36 @@ export class ContentComponent extends Component {
|
|||
// Re-initialize Group By for the new view
|
||||
this.initializeGroupBy();
|
||||
|
||||
// Handle working-on specific buttons
|
||||
if (previousViewId === "working-on" && viewId !== "working-on") {
|
||||
// Switching away from working-on
|
||||
this.unregisterWorkingOnButtons();
|
||||
} else if (previousViewId !== "working-on" && viewId === "working-on") {
|
||||
// Switching to working-on
|
||||
this.registerWorkingOnButtons();
|
||||
}
|
||||
|
||||
this.applyFilters();
|
||||
this.debounceRefreshTaskList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the TopNavigation reference and register Group By button
|
||||
* Set the TopNavigation reference and register buttons
|
||||
*/
|
||||
public setTopNavigation(nav: TopNavigation | null): void {
|
||||
// Unregister old button if exists
|
||||
// Unregister old buttons if exists
|
||||
if (this.topNavigation) {
|
||||
this.unregisterGroupByButton();
|
||||
this.unregisterWorkingOnButtons();
|
||||
}
|
||||
|
||||
this.topNavigation = nav;
|
||||
|
||||
// Register new button if nav is provided
|
||||
// Register buttons if nav is provided
|
||||
if (nav) {
|
||||
this.registerGroupByButton();
|
||||
// Register working-on buttons if applicable
|
||||
this.registerWorkingOnButtons();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -648,7 +676,7 @@ export class ContentComponent extends Component {
|
|||
});
|
||||
|
||||
console.log(
|
||||
"[ContentComponent] Registered Group By button in TopNavigation",
|
||||
"[ContentComponent] Registered Group By button in TopNavigation"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -662,27 +690,149 @@ export class ContentComponent extends Component {
|
|||
|
||||
this.topNavigation.unregisterCustomButton("content-group-by");
|
||||
console.log(
|
||||
"[ContentComponent] Unregistered Group By button from TopNavigation",
|
||||
"[ContentComponent] Unregistered Group By button from TopNavigation"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Working-On specific buttons (Timer Statistics)
|
||||
*/
|
||||
private registerWorkingOnButtons(): void {
|
||||
if (!this.topNavigation) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only register if timer is enabled and we're in working-on view
|
||||
if (
|
||||
this.currentViewId !== "working-on" ||
|
||||
!this.plugin.settings.taskTimer?.enabled
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.topNavigation.registerCustomButton({
|
||||
id: "working-on-timer-stats",
|
||||
icon: "bar-chart-2",
|
||||
tooltip: t("Timer Statistics"),
|
||||
onClick: () => {
|
||||
this.toggleTimerStatisticsPanel();
|
||||
},
|
||||
});
|
||||
|
||||
console.log(
|
||||
"[ContentComponent] Registered Timer Statistics button for working-on view"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister Working-On specific buttons
|
||||
*/
|
||||
private unregisterWorkingOnButtons(): void {
|
||||
// Hide the statistics panel if showing
|
||||
this.hideTimerStatisticsPanel();
|
||||
|
||||
if (!this.topNavigation) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.topNavigation.unregisterCustomButton("working-on-timer-stats");
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle Timer Statistics Panel
|
||||
*/
|
||||
private toggleTimerStatisticsPanel(): void {
|
||||
if (this.isShowingTimerStats) {
|
||||
this.hideTimerStatisticsPanel();
|
||||
} else {
|
||||
this.showTimerStatisticsPanel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Timer Statistics Panel
|
||||
*/
|
||||
private showTimerStatisticsPanel(): void {
|
||||
if (this.timerStatisticsPanel) {
|
||||
return; // Already showing
|
||||
}
|
||||
|
||||
this.isShowingTimerStats = true;
|
||||
|
||||
// Hide the task list
|
||||
this.taskListEl.hide();
|
||||
|
||||
// Create and show the statistics panel
|
||||
this.timerStatisticsPanel = new TimerStatisticsPanel(
|
||||
this.containerEl,
|
||||
this.plugin,
|
||||
this.filteredTasks,
|
||||
{
|
||||
onTaskClick: (task) => {
|
||||
if (task) {
|
||||
this.selectTask(task);
|
||||
}
|
||||
},
|
||||
onTaskContextMenu: (event, task) => {
|
||||
if (task && this.params.onTaskContextMenu) {
|
||||
this.params.onTaskContextMenu(event, task);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
this.addChild(this.timerStatisticsPanel);
|
||||
this.timerStatisticsPanel.load();
|
||||
|
||||
// Update button state
|
||||
this.updateTimerStatsButtonState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide Timer Statistics Panel
|
||||
*/
|
||||
private hideTimerStatisticsPanel(): void {
|
||||
if (!this.timerStatisticsPanel) {
|
||||
return; // Not showing
|
||||
}
|
||||
|
||||
this.isShowingTimerStats = false;
|
||||
|
||||
// Remove the statistics panel
|
||||
this.removeChild(this.timerStatisticsPanel);
|
||||
this.timerStatisticsPanel = null;
|
||||
|
||||
// Show the task list
|
||||
this.taskListEl.show();
|
||||
|
||||
// Update button state
|
||||
this.updateTimerStatsButtonState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the timer stats button visual state
|
||||
*/
|
||||
private updateTimerStatsButtonState(): void {
|
||||
// The button state can be indicated by adding/removing a class
|
||||
// This is optional - could be used to highlight the button when panel is open
|
||||
}
|
||||
|
||||
private applyFilters() {
|
||||
// Call the centralized filter utility
|
||||
this.filteredTasks = filterTasks(
|
||||
this.allTasks,
|
||||
this.currentViewId,
|
||||
this.plugin,
|
||||
{ textQuery: this.filterInput?.value }, // Pass text query from input
|
||||
{ textQuery: this.filterInput?.value } // Pass text query from input
|
||||
);
|
||||
|
||||
const sortCriteria = this.plugin.settings.viewConfiguration.find(
|
||||
(view) => view.id === this.currentViewId,
|
||||
(view) => view.id === this.currentViewId
|
||||
)?.sortCriteria;
|
||||
if (sortCriteria && sortCriteria.length > 0) {
|
||||
this.filteredTasks = sortTasks(
|
||||
this.filteredTasks,
|
||||
sortCriteria,
|
||||
this.plugin.settings,
|
||||
this.plugin.settings
|
||||
);
|
||||
} else {
|
||||
// Default sorting: completed tasks last, then by priority, due date, content,
|
||||
|
|
@ -710,7 +860,7 @@ export class ContentComponent extends Component {
|
|||
});
|
||||
const contentCmp = collator.compare(
|
||||
a.content ?? "",
|
||||
b.content ?? "",
|
||||
b.content ?? ""
|
||||
);
|
||||
if (contentCmp !== 0) return contentCmp;
|
||||
// Lowest-priority tie-breakers to ensure stability across files
|
||||
|
|
@ -725,6 +875,11 @@ export class ContentComponent extends Component {
|
|||
|
||||
// Update the task count display
|
||||
this.countEl.setText(`${this.filteredTasks.length} ${t("tasks")}`);
|
||||
|
||||
// Keep timer statistics panel in sync with current filters
|
||||
if (this.timerStatisticsPanel) {
|
||||
this.timerStatisticsPanel.setTasks(this.filteredTasks);
|
||||
}
|
||||
}
|
||||
|
||||
private filterTasks(query: string) {
|
||||
|
|
@ -742,7 +897,7 @@ export class ContentComponent extends Component {
|
|||
task.originalMarkdown ?? "",
|
||||
task.content ?? "",
|
||||
task.metadata ? JSON.stringify(task.metadata) : "",
|
||||
].join("|"),
|
||||
].join("|")
|
||||
)
|
||||
.join(";");
|
||||
}
|
||||
|
|
@ -808,7 +963,7 @@ export class ContentComponent extends Component {
|
|||
// If a render is already in progress, queue a refresh instead of skipping
|
||||
if (this.isRendering) {
|
||||
console.log(
|
||||
"ContentComponent: Already rendering, queueing a refresh",
|
||||
"ContentComponent: Already rendering, queueing a refresh"
|
||||
);
|
||||
this.pendingForceRefresh = true;
|
||||
return;
|
||||
|
|
@ -833,6 +988,9 @@ export class ContentComponent extends Component {
|
|||
this.nextRootTaskIndex = 0;
|
||||
this.rootTasks = [];
|
||||
|
||||
// Update working-on summary (if applicable)
|
||||
this.renderWorkingOnSummary();
|
||||
|
||||
if (this.filteredTasks.length === 0) {
|
||||
this.taskListEl.replaceChildren();
|
||||
this.disposeComponentList(previousTaskComponents);
|
||||
|
|
@ -861,19 +1019,19 @@ export class ContentComponent extends Component {
|
|||
const taskMap = new Map<string, Task>();
|
||||
// Add all non-filtered tasks to the taskMap
|
||||
this.notFilteredTasks.forEach((task) =>
|
||||
taskMap.set(task.id, task),
|
||||
taskMap.set(task.id, task)
|
||||
);
|
||||
this.rootTasks = tasksToTree(this.filteredTasks); // Calculate root tasks
|
||||
// Sort roots according to view's sort criteria (fallback to sensible defaults)
|
||||
const viewSortCriteria =
|
||||
this.plugin.settings.viewConfiguration.find(
|
||||
(view) => view.id === this.currentViewId,
|
||||
(view) => view.id === this.currentViewId
|
||||
)?.sortCriteria;
|
||||
if (viewSortCriteria && viewSortCriteria.length > 0) {
|
||||
this.rootTasks = sortTasks(
|
||||
this.rootTasks,
|
||||
viewSortCriteria,
|
||||
this.plugin.settings,
|
||||
this.plugin.settings
|
||||
);
|
||||
} else {
|
||||
// Default sorting: completed tasks last, then by priority, due date, content,
|
||||
|
|
@ -902,12 +1060,12 @@ export class ContentComponent extends Component {
|
|||
});
|
||||
const contentCmp = collator.compare(
|
||||
a.content ?? "",
|
||||
b.content ?? "",
|
||||
b.content ?? ""
|
||||
);
|
||||
if (contentCmp !== 0) return contentCmp;
|
||||
// Lowest-priority tie-breakers to ensure stability across files
|
||||
const fp = (a.filePath || "").localeCompare(
|
||||
b.filePath || "",
|
||||
b.filePath || ""
|
||||
);
|
||||
if (fp !== 0) return fp;
|
||||
return (a.line ?? 0) - (b.line ?? 0);
|
||||
|
|
@ -954,7 +1112,7 @@ export class ContentComponent extends Component {
|
|||
const containerRect = container.getBoundingClientRect();
|
||||
// Find first visible task item
|
||||
const items = Array.from(
|
||||
container.querySelectorAll<HTMLElement>(".task-item"),
|
||||
container.querySelectorAll<HTMLElement>(".task-item")
|
||||
);
|
||||
for (const el of items) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
|
@ -979,7 +1137,7 @@ export class ContentComponent extends Component {
|
|||
// Try anchor-based restoration first
|
||||
if (state.anchorId) {
|
||||
const anchorEl = container.querySelector<HTMLElement>(
|
||||
`[data-task-id="${state.anchorId}"]`,
|
||||
`[data-task-id="${state.anchorId}"]`
|
||||
);
|
||||
if (anchorEl) {
|
||||
const desiredOffset = state.anchorOffset;
|
||||
|
|
@ -996,7 +1154,7 @@ export class ContentComponent extends Component {
|
|||
}
|
||||
|
||||
private loadTaskBatch(
|
||||
target: DocumentFragment | HTMLElement = this.taskListEl,
|
||||
target: DocumentFragment | HTMLElement = this.taskListEl
|
||||
): number {
|
||||
const fragment = document.createDocumentFragment();
|
||||
const countToLoad = this.taskPageSize;
|
||||
|
|
@ -1012,7 +1170,7 @@ export class ContentComponent extends Component {
|
|||
this.currentViewId, // Pass currentViewId
|
||||
this.app,
|
||||
this.plugin,
|
||||
this.params.selectionManager, // Pass selection manager
|
||||
this.params.selectionManager // Pass selection manager
|
||||
);
|
||||
|
||||
// Attach event handlers
|
||||
|
|
@ -1043,7 +1201,7 @@ export class ContentComponent extends Component {
|
|||
|
||||
private loadRootTaskBatch(
|
||||
taskMap: Map<string, Task>,
|
||||
target: DocumentFragment | HTMLElement = this.taskListEl,
|
||||
target: DocumentFragment | HTMLElement = this.taskListEl
|
||||
): number {
|
||||
const fragment = document.createDocumentFragment();
|
||||
const countToLoad = this.taskPageSize;
|
||||
|
|
@ -1060,7 +1218,7 @@ export class ContentComponent extends Component {
|
|||
for (let i = start; i < end; i++) {
|
||||
const rootTask = this.rootTasks[i];
|
||||
const childTasks = this.notFilteredTasks.filter(
|
||||
(task) => task.metadata.parent === rootTask.id,
|
||||
(task) => task.metadata.parent === rootTask.id
|
||||
);
|
||||
|
||||
const treeComponent = new TaskTreeItemComponent(
|
||||
|
|
@ -1071,7 +1229,7 @@ export class ContentComponent extends Component {
|
|||
childTasks,
|
||||
taskMap,
|
||||
this.plugin,
|
||||
this.params.selectionManager, // Pass selection manager
|
||||
this.params.selectionManager // Pass selection manager
|
||||
);
|
||||
|
||||
// Attach event handlers
|
||||
|
|
@ -1134,6 +1292,73 @@ export class ContentComponent extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
private renderWorkingOnSummary(): void {
|
||||
// Only render for working-on view when timer is enabled
|
||||
if (this.currentViewId !== "working-on") {
|
||||
if (this.workingOnSummaryEl) {
|
||||
this.workingOnSummaryEl.remove();
|
||||
this.workingOnSummaryEl = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.plugin.settings.taskTimer?.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.workingOnSummaryEl) {
|
||||
this.workingOnSummaryEl = this.taskListEl.createDiv({
|
||||
cls: "working-on-summary",
|
||||
});
|
||||
}
|
||||
|
||||
const timerManager = new TaskTimerManager(
|
||||
this.plugin.settings.taskTimer
|
||||
);
|
||||
|
||||
let activeDuration = 0;
|
||||
let completedDuration = 0;
|
||||
let activeCount = 0;
|
||||
|
||||
const accumulate = (task: Task, includeInActive: boolean) => {
|
||||
const blockId = task.metadata?.id;
|
||||
if (!blockId) return;
|
||||
const timer = timerManager.getTimerByFileAndBlock(
|
||||
task.filePath,
|
||||
blockId
|
||||
);
|
||||
if (!timer) return;
|
||||
const duration = timerManager.getCurrentDuration(timer.taskId);
|
||||
if (includeInActive) {
|
||||
activeDuration += duration;
|
||||
activeCount++;
|
||||
} else {
|
||||
completedDuration += duration;
|
||||
}
|
||||
};
|
||||
|
||||
// Active timers from current filtered working-on tasks
|
||||
this.filteredTasks.forEach((task) => accumulate(task, true));
|
||||
// Completed timers (if any) from all tasks
|
||||
this.notFilteredTasks
|
||||
.filter((t) => t.completed)
|
||||
.forEach((task) => accumulate(task, false));
|
||||
|
||||
const activeText = timerManager.formatDuration(activeDuration);
|
||||
const completedText = timerManager.formatDuration(completedDuration);
|
||||
|
||||
this.workingOnSummaryEl.setText(
|
||||
`⏱ ${t("Active timers")}: ${activeCount} · ${t(
|
||||
"Total time"
|
||||
)}: ${activeText} · ${t("Completed time")}: ${completedText}`
|
||||
);
|
||||
|
||||
// Ensure the summary stays at the top
|
||||
if (this.workingOnSummaryEl.parentElement !== this.taskListEl) {
|
||||
this.taskListEl.prepend(this.workingOnSummaryEl);
|
||||
}
|
||||
}
|
||||
|
||||
private loadMoreTasks() {
|
||||
// console.log("Load more tasks triggered...");
|
||||
this.removeLoadMarker(); // Remove the current marker
|
||||
|
|
@ -1145,7 +1370,7 @@ export class ContentComponent extends Component {
|
|||
// );
|
||||
const taskMap = new Map<string, Task>();
|
||||
this.filteredTasks.forEach((task) =>
|
||||
taskMap.set(task.id, task),
|
||||
taskMap.set(task.id, task)
|
||||
);
|
||||
this.loadRootTaskBatch(taskMap);
|
||||
} else {
|
||||
|
|
@ -1206,12 +1431,12 @@ export class ContentComponent extends Component {
|
|||
public updateTask(updatedTask: Task) {
|
||||
// 1) Update sources
|
||||
const taskIndexAll = this.allTasks.findIndex(
|
||||
(t) => t.id === updatedTask.id,
|
||||
(t) => t.id === updatedTask.id
|
||||
);
|
||||
if (taskIndexAll !== -1)
|
||||
this.allTasks[taskIndexAll] = { ...updatedTask };
|
||||
const taskIndexNotFiltered = this.notFilteredTasks.findIndex(
|
||||
(t) => t.id === updatedTask.id,
|
||||
(t) => t.id === updatedTask.id
|
||||
);
|
||||
if (taskIndexNotFiltered !== -1)
|
||||
this.notFilteredTasks[taskIndexNotFiltered] = { ...updatedTask };
|
||||
|
|
@ -1222,17 +1447,17 @@ export class ContentComponent extends Component {
|
|||
const prevLen = this.filteredTasks.length;
|
||||
this.applyFilters();
|
||||
const taskFromFiltered = this.filteredTasks.find(
|
||||
(t) => t.id === updatedTask.id,
|
||||
(t) => t.id === updatedTask.id
|
||||
);
|
||||
const taskStillVisible = !!taskFromFiltered;
|
||||
|
||||
// Helper: insert list item at correct position (list view)
|
||||
const insertListItem = (taskToInsert: Task) => {
|
||||
const compIds = new Set(
|
||||
this.taskComponents.map((c) => c.getTask().id),
|
||||
this.taskComponents.map((c) => c.getTask().id)
|
||||
);
|
||||
const sortedIndex = this.filteredTasks.findIndex(
|
||||
(t) => t.id === taskToInsert.id,
|
||||
(t) => t.id === taskToInsert.id
|
||||
);
|
||||
// Find the next rendered neighbor after sortedIndex
|
||||
let nextComp: any = null;
|
||||
|
|
@ -1240,7 +1465,7 @@ export class ContentComponent extends Component {
|
|||
const id = this.filteredTasks[i].id;
|
||||
if (compIds.has(id)) {
|
||||
nextComp = this.taskComponents.find(
|
||||
(c) => c.getTask().id === id,
|
||||
(c) => c.getTask().id === id
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
|
@ -1251,7 +1476,7 @@ export class ContentComponent extends Component {
|
|||
this.currentViewId,
|
||||
this.app,
|
||||
this.plugin,
|
||||
this.params.selectionManager, // Pass selection manager
|
||||
this.params.selectionManager // Pass selection manager
|
||||
);
|
||||
// Attach events
|
||||
taskComponent.onTaskSelected = this.selectTask.bind(this);
|
||||
|
|
@ -1272,7 +1497,7 @@ export class ContentComponent extends Component {
|
|||
if (nextComp) {
|
||||
this.taskListEl.insertBefore(
|
||||
taskComponent.element,
|
||||
nextComp.element,
|
||||
nextComp.element
|
||||
);
|
||||
const idx = this.taskComponents.indexOf(nextComp);
|
||||
this.taskComponents.splice(idx, 0, taskComponent);
|
||||
|
|
@ -1285,7 +1510,7 @@ export class ContentComponent extends Component {
|
|||
// Helper: remove list item
|
||||
const removeListItem = (taskId: string) => {
|
||||
const idx = this.taskComponents.findIndex(
|
||||
(c) => c.getTask().id === taskId,
|
||||
(c) => c.getTask().id === taskId
|
||||
);
|
||||
if (idx >= 0) {
|
||||
const comp = this.taskComponents[idx];
|
||||
|
|
@ -1306,7 +1531,7 @@ export class ContentComponent extends Component {
|
|||
if (!this.isTreeView) {
|
||||
// List view: update in place or insert if new to view
|
||||
const comp = this.taskComponents.find(
|
||||
(c) => c.getTask().id === updatedTask.id,
|
||||
(c) => c.getTask().id === updatedTask.id
|
||||
);
|
||||
if (comp) {
|
||||
comp.updateTask(taskFromFiltered!);
|
||||
|
|
@ -1316,7 +1541,7 @@ export class ContentComponent extends Component {
|
|||
} else {
|
||||
// Tree view: update existing subtree or insert to parent/root
|
||||
const comp = this.treeComponents.find(
|
||||
(c) => c.getTask().id === updatedTask.id,
|
||||
(c) => c.getTask().id === updatedTask.id
|
||||
);
|
||||
if (comp) {
|
||||
comp.updateTask(taskFromFiltered!);
|
||||
|
|
@ -1341,7 +1566,7 @@ export class ContentComponent extends Component {
|
|||
const newChildren =
|
||||
this.notFilteredTasks.filter(
|
||||
(t) =>
|
||||
t.metadata.parent === parentId,
|
||||
t.metadata.parent === parentId
|
||||
);
|
||||
parentComp.updateChildTasks(newChildren);
|
||||
updated = true;
|
||||
|
|
@ -1352,11 +1577,11 @@ export class ContentComponent extends Component {
|
|||
// Root insertion
|
||||
const taskMap = new Map<string, Task>();
|
||||
this.notFilteredTasks.forEach((t) =>
|
||||
taskMap.set(t.id, t),
|
||||
taskMap.set(t.id, t)
|
||||
);
|
||||
const childTasks = this.notFilteredTasks.filter(
|
||||
(t) =>
|
||||
t.metadata.parent === taskFromFiltered!.id,
|
||||
t.metadata.parent === taskFromFiltered!.id
|
||||
);
|
||||
const newRoot = new TaskTreeItemComponent(
|
||||
taskFromFiltered!,
|
||||
|
|
@ -1366,7 +1591,7 @@ export class ContentComponent extends Component {
|
|||
childTasks,
|
||||
taskMap,
|
||||
this.plugin,
|
||||
this.params.selectionManager, // Pass selection manager
|
||||
this.params.selectionManager // Pass selection manager
|
||||
);
|
||||
newRoot.onTaskSelected = this.selectTask.bind(this);
|
||||
newRoot.onTaskCompleted = (t) => {
|
||||
|
|
@ -1391,7 +1616,7 @@ export class ContentComponent extends Component {
|
|||
if (
|
||||
rootComparator(
|
||||
taskFromFiltered!,
|
||||
this.treeComponents[i].getTask(),
|
||||
this.treeComponents[i].getTask()
|
||||
) < 0
|
||||
) {
|
||||
insertAt = i;
|
||||
|
|
@ -1401,12 +1626,12 @@ export class ContentComponent extends Component {
|
|||
if (insertAt < this.treeComponents.length) {
|
||||
this.taskListEl.insertBefore(
|
||||
newRoot.element,
|
||||
this.treeComponents[insertAt].element,
|
||||
this.treeComponents[insertAt].element
|
||||
);
|
||||
this.treeComponents.splice(
|
||||
insertAt,
|
||||
0,
|
||||
newRoot,
|
||||
newRoot
|
||||
);
|
||||
} else {
|
||||
this.taskListEl.appendChild(newRoot.element);
|
||||
|
|
@ -1428,7 +1653,7 @@ export class ContentComponent extends Component {
|
|||
// Tree view removal
|
||||
// If root component exists, remove it
|
||||
const idx = this.treeComponents.findIndex(
|
||||
(c) => c.getTask().id === updatedTask.id,
|
||||
(c) => c.getTask().id === updatedTask.id
|
||||
);
|
||||
if (idx >= 0) {
|
||||
const comp = this.treeComponents[idx];
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { MarkdownRendererComponent } from "@/components/ui/renderers/MarkdownRen
|
|||
|
||||
import "@/styles/task-status-indicator.css";
|
||||
import { createTaskCheckbox } from "./details";
|
||||
import { TaskTimerManager } from "@/managers/timer-manager";
|
||||
import { getRelativeTimeString } from "@/utils/date/date-formatter";
|
||||
import { t } from "@/translations/helper";
|
||||
import TaskProgressBarPlugin from "@/index";
|
||||
|
|
@ -45,6 +46,10 @@ export class TaskListItemComponent extends Component {
|
|||
|
||||
private settings: TaskProgressBarSettings;
|
||||
|
||||
// Timer update interval (managed by registerInterval, auto-cleaned on unload)
|
||||
private timerEl: HTMLElement | null = null;
|
||||
private hasActiveTimerInterval: boolean = false;
|
||||
|
||||
// Use shared editor manager instead of individual editors
|
||||
private static editorManager: InlineEditorManager | null = null;
|
||||
|
||||
|
|
@ -65,7 +70,7 @@ export class TaskListItemComponent extends Component {
|
|||
private viewMode: string,
|
||||
private app: App,
|
||||
private plugin: TaskProgressBarPlugin,
|
||||
selectionManager?: TaskSelectionManager,
|
||||
selectionManager?: TaskSelectionManager
|
||||
) {
|
||||
super();
|
||||
|
||||
|
|
@ -83,7 +88,7 @@ export class TaskListItemComponent extends Component {
|
|||
if (!TaskListItemComponent.editorManager) {
|
||||
TaskListItemComponent.editorManager = new InlineEditorManager(
|
||||
this.app,
|
||||
this.plugin,
|
||||
this.plugin
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -99,7 +104,7 @@ export class TaskListItemComponent extends Component {
|
|||
try {
|
||||
await this.onTaskUpdate(originalTask, updatedTask);
|
||||
console.log(
|
||||
"listItem onTaskUpdate completed successfully",
|
||||
"listItem onTaskUpdate completed successfully"
|
||||
);
|
||||
// Don't update task reference here - let onContentEditFinished handle it
|
||||
} catch (error) {
|
||||
|
|
@ -112,7 +117,7 @@ export class TaskListItemComponent extends Component {
|
|||
},
|
||||
onContentEditFinished: (
|
||||
targetEl: HTMLElement,
|
||||
updatedTask: Task,
|
||||
updatedTask: Task
|
||||
) => {
|
||||
// Update the task reference with the saved task
|
||||
this.task = updatedTask;
|
||||
|
|
@ -125,13 +130,13 @@ export class TaskListItemComponent extends Component {
|
|||
|
||||
// Release the editor from the manager
|
||||
TaskListItemComponent.editorManager?.releaseEditor(
|
||||
this.task.id,
|
||||
this.task.id
|
||||
);
|
||||
},
|
||||
onMetadataEditFinished: (
|
||||
targetEl: HTMLElement,
|
||||
updatedTask: Task,
|
||||
fieldType: string,
|
||||
fieldType: string
|
||||
) => {
|
||||
// Update the task reference with the saved task
|
||||
this.task = updatedTask;
|
||||
|
|
@ -141,7 +146,7 @@ export class TaskListItemComponent extends Component {
|
|||
|
||||
// Release the editor from the manager
|
||||
TaskListItemComponent.editorManager?.releaseEditor(
|
||||
this.task.id,
|
||||
this.task.id
|
||||
);
|
||||
},
|
||||
useEmbeddedEditor: true, // Enable Obsidian's embedded editor
|
||||
|
|
@ -149,7 +154,7 @@ export class TaskListItemComponent extends Component {
|
|||
|
||||
return TaskListItemComponent.editorManager!.getEditor(
|
||||
this.task,
|
||||
editorOptions,
|
||||
editorOptions
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -159,7 +164,7 @@ export class TaskListItemComponent extends Component {
|
|||
private isCurrentlyEditing(): boolean {
|
||||
return (
|
||||
TaskListItemComponent.editorManager?.hasActiveEditor(
|
||||
this.task.id,
|
||||
this.task.id
|
||||
) || false
|
||||
);
|
||||
}
|
||||
|
|
@ -172,8 +177,8 @@ export class TaskListItemComponent extends Component {
|
|||
"task-genius:selection-changed",
|
||||
() => {
|
||||
this.updateSelectionVisualState();
|
||||
},
|
||||
),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Setup long press detection for mobile
|
||||
|
|
@ -185,7 +190,7 @@ export class TaskListItemComponent extends Component {
|
|||
this.selectionManager?.enterSelectionMode();
|
||||
this.handleMultiSelect();
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -210,11 +215,11 @@ export class TaskListItemComponent extends Component {
|
|||
() => {
|
||||
// Refresh view after operation
|
||||
// The parent view should handle this via task updates
|
||||
},
|
||||
}
|
||||
).catch((error) => {
|
||||
console.error(
|
||||
"Failed to show bulk operations menu:",
|
||||
error,
|
||||
error
|
||||
);
|
||||
});
|
||||
return;
|
||||
|
|
@ -253,7 +258,7 @@ export class TaskListItemComponent extends Component {
|
|||
const checkbox = createTaskCheckbox(
|
||||
this.task.status,
|
||||
this.task,
|
||||
el,
|
||||
el
|
||||
);
|
||||
this.checkboxInput = checkbox;
|
||||
|
||||
|
|
@ -279,7 +284,7 @@ export class TaskListItemComponent extends Component {
|
|||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.element.appendChild(checkboxEl);
|
||||
|
|
@ -371,7 +376,7 @@ export class TaskListItemComponent extends Component {
|
|||
}
|
||||
|
||||
const priorityConfig = TaskListItemComponent.PRIORITY_CONFIG.find(
|
||||
(config) => config.value === numericPriority,
|
||||
(config) => config.value === numericPriority
|
||||
);
|
||||
const classes = ["task-priority"];
|
||||
if (priorityConfig) {
|
||||
|
|
@ -445,13 +450,24 @@ export class TaskListItemComponent extends Component {
|
|||
}
|
||||
|
||||
private renderMetadata() {
|
||||
// Clear timer element reference before re-rendering
|
||||
this.timerEl = null;
|
||||
|
||||
this.metadataEl.empty();
|
||||
|
||||
// Working-on timer display: show elapsed time if timer exists
|
||||
if (
|
||||
this.viewMode === "working-on" &&
|
||||
this.plugin.settings.taskTimer?.enabled
|
||||
) {
|
||||
this.renderTimerMetadata();
|
||||
}
|
||||
|
||||
// For cancelled tasks, show cancelled date (independent of completion status)
|
||||
if (this.task.metadata.cancelledDate) {
|
||||
this.renderDateMetadata(
|
||||
"cancelled",
|
||||
this.task.metadata.cancelledDate,
|
||||
this.task.metadata.cancelledDate
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -468,7 +484,7 @@ export class TaskListItemComponent extends Component {
|
|||
if (this.task.metadata.scheduledDate) {
|
||||
this.renderDateMetadata(
|
||||
"scheduled",
|
||||
this.task.metadata.scheduledDate,
|
||||
this.task.metadata.scheduledDate
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -486,7 +502,7 @@ export class TaskListItemComponent extends Component {
|
|||
if (this.task.metadata.completedDate) {
|
||||
this.renderDateMetadata(
|
||||
"completed",
|
||||
this.task.metadata.completedDate,
|
||||
this.task.metadata.completedDate
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -494,7 +510,7 @@ export class TaskListItemComponent extends Component {
|
|||
if (this.task.metadata.createdDate) {
|
||||
this.renderDateMetadata(
|
||||
"created",
|
||||
this.task.metadata.createdDate,
|
||||
this.task.metadata.createdDate
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -534,6 +550,123 @@ export class TaskListItemComponent extends Component {
|
|||
this.renderAddMetadataButton();
|
||||
}
|
||||
|
||||
private renderTimerMetadata() {
|
||||
const blockId = this.task.metadata?.id;
|
||||
if (!blockId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const timerManager = new TaskTimerManager(
|
||||
this.plugin.settings.taskTimer
|
||||
);
|
||||
const timer = timerManager.getTimerByFileAndBlock(
|
||||
this.task.filePath,
|
||||
blockId
|
||||
);
|
||||
if (!timer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const duration = timerManager.getCurrentDuration(timer.taskId);
|
||||
const formatted = timerManager.formatDuration(duration);
|
||||
|
||||
this.timerEl = this.metadataEl.createDiv({
|
||||
cls: "task-timer-meta",
|
||||
});
|
||||
this.timerEl.createSpan({
|
||||
cls: "task-timer-duration",
|
||||
text: "⏱ " + formatted,
|
||||
});
|
||||
this.timerEl.createSpan({
|
||||
cls: "task-timer-status",
|
||||
text: ` (${t(
|
||||
timer.status === "running" ? "Running" : "Paused"
|
||||
)})`,
|
||||
});
|
||||
|
||||
// Start periodic updates if timer is running
|
||||
if (timer.status === "running") {
|
||||
this.startTimerUpdateInterval();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[TaskListItem] Failed to render timer metadata", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic timer updates using registerInterval (auto-cleaned on unload)
|
||||
*/
|
||||
private startTimerUpdateInterval() {
|
||||
// Only register once - registerInterval handles cleanup automatically
|
||||
if (this.hasActiveTimerInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hasActiveTimerInterval = true;
|
||||
|
||||
// Update every second - registerInterval auto-cleans on component unload
|
||||
this.registerInterval(
|
||||
window.setInterval(() => {
|
||||
this.updateTimerDisplay();
|
||||
}, 1000)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the timer display without re-rendering the entire metadata
|
||||
*/
|
||||
private updateTimerDisplay() {
|
||||
const blockId = this.task.metadata?.id;
|
||||
if (!blockId || !this.timerEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const timerManager = new TaskTimerManager(
|
||||
this.plugin.settings.taskTimer
|
||||
);
|
||||
const timer = timerManager.getTimerByFileAndBlock(
|
||||
this.task.filePath,
|
||||
blockId
|
||||
);
|
||||
|
||||
if (!timer) {
|
||||
// Timer was stopped/completed - show "Stopped" status with last known duration
|
||||
const statusSpan =
|
||||
this.timerEl.querySelector(".task-timer-status");
|
||||
if (statusSpan) {
|
||||
statusSpan.textContent = ` (${t("Stopped")})`;
|
||||
statusSpan.addClass("task-timer-stopped");
|
||||
}
|
||||
// Keep the element visible with last duration shown
|
||||
return;
|
||||
}
|
||||
|
||||
// Update duration display
|
||||
const duration = timerManager.getCurrentDuration(timer.taskId);
|
||||
const formatted = timerManager.formatDuration(duration);
|
||||
|
||||
const durationSpan = this.timerEl.querySelector(
|
||||
".task-timer-duration"
|
||||
);
|
||||
if (durationSpan) {
|
||||
durationSpan.textContent = "⏱ " + formatted;
|
||||
}
|
||||
|
||||
// Update status display
|
||||
const statusSpan = this.timerEl.querySelector(".task-timer-status");
|
||||
if (statusSpan) {
|
||||
statusSpan.textContent = ` (${t(
|
||||
timer.status === "running" ? "Running" : "Paused"
|
||||
)})`;
|
||||
statusSpan.removeClass("task-timer-stopped");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[TaskListItem] Failed to update timer display", e);
|
||||
}
|
||||
}
|
||||
|
||||
private renderDateMetadata(
|
||||
type:
|
||||
| "due"
|
||||
|
|
@ -542,7 +675,7 @@ export class TaskListItemComponent extends Component {
|
|||
| "completed"
|
||||
| "cancelled"
|
||||
| "created",
|
||||
dateValue: number,
|
||||
dateValue: number
|
||||
) {
|
||||
const dateEl = this.metadataEl.createEl("div", {
|
||||
cls: ["task-date", `task-${type}-date`],
|
||||
|
|
@ -593,7 +726,7 @@ export class TaskListItemComponent extends Component {
|
|||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (cssClass) {
|
||||
|
|
@ -613,20 +746,20 @@ export class TaskListItemComponent extends Component {
|
|||
type === "due"
|
||||
? "dueDate"
|
||||
: type === "scheduled"
|
||||
? "scheduledDate"
|
||||
: type === "start"
|
||||
? "startDate"
|
||||
: type === "cancelled"
|
||||
? "cancelledDate"
|
||||
: type === "completed"
|
||||
? "completedDate"
|
||||
: null;
|
||||
? "scheduledDate"
|
||||
: type === "start"
|
||||
? "startDate"
|
||||
: type === "cancelled"
|
||||
? "cancelledDate"
|
||||
: type === "completed"
|
||||
? "completedDate"
|
||||
: null;
|
||||
|
||||
if (fieldType) {
|
||||
this.getInlineEditor().showMetadataEditor(
|
||||
dateEl,
|
||||
fieldType,
|
||||
dateString,
|
||||
dateString
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -672,7 +805,7 @@ export class TaskListItemComponent extends Component {
|
|||
this.getInlineEditor().showMetadataEditor(
|
||||
projectEl,
|
||||
"project",
|
||||
this.task.metadata.project || "",
|
||||
this.task.metadata.project || ""
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -702,7 +835,7 @@ export class TaskListItemComponent extends Component {
|
|||
this.getInlineEditor().showMetadataEditor(
|
||||
tagsContainer,
|
||||
"tags",
|
||||
tagsString,
|
||||
tagsString
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -724,7 +857,7 @@ export class TaskListItemComponent extends Component {
|
|||
this.getInlineEditor().showMetadataEditor(
|
||||
recurrenceEl,
|
||||
"recurrence",
|
||||
this.task.metadata.recurrence || "",
|
||||
this.task.metadata.recurrence || ""
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -745,7 +878,7 @@ export class TaskListItemComponent extends Component {
|
|||
this.getInlineEditor().showMetadataEditor(
|
||||
onCompletionEl,
|
||||
"onCompletion",
|
||||
this.task.metadata.onCompletion || "",
|
||||
this.task.metadata.onCompletion || ""
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -757,7 +890,7 @@ export class TaskListItemComponent extends Component {
|
|||
cls: "task-dependson",
|
||||
});
|
||||
dependsOnEl.textContent = `⛔ ${this.task.metadata.dependsOn?.join(
|
||||
", ",
|
||||
", "
|
||||
)}`;
|
||||
|
||||
// Make dependsOn clickable for editing only if inline editor is enabled
|
||||
|
|
@ -768,7 +901,7 @@ export class TaskListItemComponent extends Component {
|
|||
this.getInlineEditor().showMetadataEditor(
|
||||
dependsOnEl,
|
||||
"dependsOn",
|
||||
this.task.metadata.dependsOn?.join(", ") || "",
|
||||
this.task.metadata.dependsOn?.join(", ") || ""
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -789,7 +922,7 @@ export class TaskListItemComponent extends Component {
|
|||
this.getInlineEditor().showMetadataEditor(
|
||||
idEl,
|
||||
"id",
|
||||
this.task.metadata.id || "",
|
||||
this.task.metadata.id || ""
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -886,7 +1019,7 @@ export class TaskListItemComponent extends Component {
|
|||
if (fieldsToShow.length === 0) {
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(
|
||||
"All metadata fields are already set",
|
||||
"All metadata fields are already set"
|
||||
).setDisabled(true);
|
||||
});
|
||||
} else {
|
||||
|
|
@ -903,7 +1036,7 @@ export class TaskListItemComponent extends Component {
|
|||
|
||||
editor.showMetadataEditor(
|
||||
tempContainer,
|
||||
field.key as any,
|
||||
field.key as any
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -994,7 +1127,7 @@ export class TaskListItemComponent extends Component {
|
|||
this.markdownRenderer = new MarkdownRendererComponent(
|
||||
this.app,
|
||||
this.contentEl,
|
||||
this.task.filePath,
|
||||
this.task.filePath
|
||||
);
|
||||
this.addChild(this.markdownRenderer);
|
||||
|
||||
|
|
@ -1021,11 +1154,11 @@ export class TaskListItemComponent extends Component {
|
|||
// If disabled, always use multi-line (traditional) layout
|
||||
this.contentMetadataContainer.toggleClass(
|
||||
"multi-line-content",
|
||||
true,
|
||||
true
|
||||
);
|
||||
this.contentMetadataContainer.toggleClass(
|
||||
"single-line-content",
|
||||
false,
|
||||
false
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1036,11 +1169,11 @@ export class TaskListItemComponent extends Component {
|
|||
// Apply appropriate layout class using Obsidian's toggleClass method
|
||||
this.contentMetadataContainer.toggleClass(
|
||||
"multi-line-content",
|
||||
isMultiLine,
|
||||
isMultiLine
|
||||
);
|
||||
this.contentMetadataContainer.toggleClass(
|
||||
"single-line-content",
|
||||
!isMultiLine,
|
||||
!isMultiLine
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1069,7 +1202,7 @@ export class TaskListItemComponent extends Component {
|
|||
|
||||
// Method 4: Check for elements that typically cause multi-line layout
|
||||
const hasBlockElements = this.contentEl.querySelector(
|
||||
"br, div, p, ul, ol, li, blockquote",
|
||||
"br, div, p, ul, ol, li, blockquote"
|
||||
);
|
||||
if (hasBlockElements) {
|
||||
return true;
|
||||
|
|
@ -1203,6 +1336,9 @@ export class TaskListItemComponent extends Component {
|
|||
}
|
||||
|
||||
onunload() {
|
||||
// Timer interval is auto-cleaned by registerInterval
|
||||
this.timerEl = null;
|
||||
|
||||
// Release editor from manager if this task was being edited
|
||||
if (
|
||||
TaskListItemComponent.editorManager?.hasActiveEditor(this.task.id)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { sortTasks } from "@/commands/sortTaskCommands";
|
|||
import { TaskSelectionManager } from "@/components/features/task/selection/TaskSelectionManager";
|
||||
import { showBulkOperationsMenu } from "./BulkOperationsMenu";
|
||||
import { TaskStatusIndicator } from "./TaskStatusIndicator";
|
||||
import { TaskTimerManager } from "@/managers/timer-manager";
|
||||
|
||||
export class TaskTreeItemComponent extends Component {
|
||||
public element: HTMLElement;
|
||||
|
|
@ -50,6 +51,10 @@ export class TaskTreeItemComponent extends Component {
|
|||
private taskMap: Map<string, Task>;
|
||||
private statusIndicator: TaskStatusIndicator | null = null;
|
||||
|
||||
// Timer update interval (managed by registerInterval, auto-cleaned on unload)
|
||||
private timerEl: HTMLElement | null = null;
|
||||
private hasActiveTimerInterval: boolean = false;
|
||||
|
||||
// Use shared editor manager instead of individual editors
|
||||
private static editorManager: InlineEditorManager | null = null;
|
||||
|
||||
|
|
@ -503,8 +508,19 @@ export class TaskTreeItemComponent extends Component {
|
|||
}
|
||||
|
||||
private renderMetadata(metadataEl: HTMLElement) {
|
||||
// Clear timer element reference before re-rendering
|
||||
this.timerEl = null;
|
||||
|
||||
metadataEl.empty();
|
||||
|
||||
// Working-on timer display: show elapsed time if timer exists
|
||||
if (
|
||||
this.viewMode === "working-on" &&
|
||||
this.plugin.settings.taskTimer?.enabled
|
||||
) {
|
||||
this.renderTimerMetadata(metadataEl);
|
||||
}
|
||||
|
||||
// For cancelled tasks, show cancelled date (independent of completion status)
|
||||
if (this.task.metadata.cancelledDate) {
|
||||
this.renderDateMetadata(
|
||||
|
|
@ -898,6 +914,123 @@ export class TaskTreeItemComponent extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
private renderTimerMetadata(metadataEl: HTMLElement) {
|
||||
const blockId = this.task.metadata?.id;
|
||||
if (!blockId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const timerManager = new TaskTimerManager(
|
||||
this.plugin.settings.taskTimer
|
||||
);
|
||||
const timer = timerManager.getTimerByFileAndBlock(
|
||||
this.task.filePath,
|
||||
blockId
|
||||
);
|
||||
if (!timer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const duration = timerManager.getCurrentDuration(timer.taskId);
|
||||
const formatted = timerManager.formatDuration(duration);
|
||||
|
||||
this.timerEl = metadataEl.createDiv({
|
||||
cls: "task-timer-meta",
|
||||
});
|
||||
this.timerEl.createSpan({
|
||||
cls: "task-timer-duration",
|
||||
text: "⏱ " + formatted,
|
||||
});
|
||||
this.timerEl.createSpan({
|
||||
cls: "task-timer-status",
|
||||
text: ` (${t(
|
||||
timer.status === "running" ? "Running" : "Paused"
|
||||
)})`,
|
||||
});
|
||||
|
||||
// Start periodic updates if timer is running
|
||||
if (timer.status === "running") {
|
||||
this.startTimerUpdateInterval();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[TaskTreeItem] Failed to render timer metadata", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic timer updates using registerInterval (auto-cleaned on unload)
|
||||
*/
|
||||
private startTimerUpdateInterval() {
|
||||
// Only register once - registerInterval handles cleanup automatically
|
||||
if (this.hasActiveTimerInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hasActiveTimerInterval = true;
|
||||
|
||||
// Update every second - registerInterval auto-cleans on component unload
|
||||
this.registerInterval(
|
||||
window.setInterval(() => {
|
||||
this.updateTimerDisplay();
|
||||
}, 1000)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the timer display without re-rendering the entire metadata
|
||||
*/
|
||||
private updateTimerDisplay() {
|
||||
const blockId = this.task.metadata?.id;
|
||||
if (!blockId || !this.timerEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const timerManager = new TaskTimerManager(
|
||||
this.plugin.settings.taskTimer
|
||||
);
|
||||
const timer = timerManager.getTimerByFileAndBlock(
|
||||
this.task.filePath,
|
||||
blockId
|
||||
);
|
||||
|
||||
if (!timer) {
|
||||
// Timer was stopped/completed - show "Stopped" status with last known duration
|
||||
const statusSpan =
|
||||
this.timerEl.querySelector(".task-timer-status");
|
||||
if (statusSpan) {
|
||||
statusSpan.textContent = ` (${t("Stopped")})`;
|
||||
statusSpan.addClass("task-timer-stopped");
|
||||
}
|
||||
// Keep the element visible with last duration shown
|
||||
return;
|
||||
}
|
||||
|
||||
// Update duration display
|
||||
const duration = timerManager.getCurrentDuration(timer.taskId);
|
||||
const formatted = timerManager.formatDuration(duration);
|
||||
|
||||
const durationSpan = this.timerEl.querySelector(
|
||||
".task-timer-duration"
|
||||
);
|
||||
if (durationSpan) {
|
||||
durationSpan.textContent = "⏱ " + formatted;
|
||||
}
|
||||
|
||||
// Update status display
|
||||
const statusSpan = this.timerEl.querySelector(".task-timer-status");
|
||||
if (statusSpan) {
|
||||
statusSpan.textContent = ` (${t(
|
||||
timer.status === "running" ? "Running" : "Paused"
|
||||
)})`;
|
||||
statusSpan.removeClass("task-timer-stopped");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[TaskTreeItem] Failed to update timer display", e);
|
||||
}
|
||||
}
|
||||
|
||||
private showMetadataMenu(buttonEl: HTMLElement): void {
|
||||
const editor = this.getInlineEditor();
|
||||
|
||||
|
|
@ -1585,6 +1718,9 @@ export class TaskTreeItemComponent extends Component {
|
|||
}
|
||||
|
||||
onunload() {
|
||||
// Timer interval is auto-cleaned by registerInterval
|
||||
this.timerEl = null;
|
||||
|
||||
// Release editor from manager if this task was being edited
|
||||
if (
|
||||
TaskTreeItemComponent.editorManager?.hasActiveEditor(this.task.id)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
191
src/index.ts
191
src/index.ts
|
|
@ -230,7 +230,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
this.changelogManager = new ChangelogManager(this);
|
||||
this.registerView(
|
||||
CHANGELOG_VIEW_TYPE,
|
||||
(leaf) => new ChangelogView(leaf, this),
|
||||
(leaf) => new ChangelogView(leaf, this)
|
||||
);
|
||||
|
||||
// Initialize onboarding config manager
|
||||
|
|
@ -273,18 +273,18 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
item.setTitle(
|
||||
`${t("Set priority")}: ${
|
||||
priority.text
|
||||
}`,
|
||||
}`
|
||||
);
|
||||
item.setIcon("arrow-big-up-dash");
|
||||
item.onClick(() => {
|
||||
setPriorityAtCursor(
|
||||
editor,
|
||||
priority.emoji,
|
||||
priority.emoji
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
submenu.addSeparator();
|
||||
|
|
@ -294,17 +294,17 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
([key, priority]) => {
|
||||
submenu.addItem((item) => {
|
||||
item.setTitle(
|
||||
`${t("Set priority")}: ${key}`,
|
||||
`${t("Set priority")}: ${key}`
|
||||
);
|
||||
item.setIcon("a-arrow-up");
|
||||
item.onClick(() => {
|
||||
setPriorityAtCursor(
|
||||
editor,
|
||||
`[#${key}]`,
|
||||
`[#${key}]`
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Remove priority command
|
||||
|
|
@ -324,7 +324,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
if (this.settings.workflow.enableWorkflow) {
|
||||
updateWorkflowContextMenu(menu, editor, this);
|
||||
}
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
|
|
@ -336,7 +336,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
const deferWorkspaceLeaves =
|
||||
this.app.workspace.getLeavesOfType(TASK_VIEW_TYPE);
|
||||
const deferSpecificLeaves = this.app.workspace.getLeavesOfType(
|
||||
TASK_SPECIFIC_VIEW_TYPE,
|
||||
TASK_SPECIFIC_VIEW_TYPE
|
||||
);
|
||||
const deferTaskGeniusLeaves =
|
||||
this.app.workspace.getLeavesOfType(FLUENT_TASK_VIEW);
|
||||
|
|
@ -364,8 +364,8 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
this.registerEvent(
|
||||
this.app.workspace.on(
|
||||
Events.TASK_CACHE_UPDATED as any,
|
||||
() => this.notificationManager?.onTaskCacheUpdated(),
|
||||
),
|
||||
() => this.notificationManager?.onTaskCacheUpdated()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -414,7 +414,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
this.icsManager = new IcsManager(
|
||||
this.settings.icsIntegration,
|
||||
this.settings,
|
||||
this,
|
||||
this
|
||||
);
|
||||
this.addChild(this.icsManager);
|
||||
|
||||
|
|
@ -434,7 +434,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
this.activateTimelineSidebarView().catch((error) => {
|
||||
console.error(
|
||||
"Failed to auto-open timeline sidebar:",
|
||||
error,
|
||||
error
|
||||
);
|
||||
});
|
||||
}, 1000);
|
||||
|
|
@ -483,7 +483,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
} catch (error) {
|
||||
console.error(
|
||||
"[Plugin] Dataflow version check failed during startup:",
|
||||
error,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -521,12 +521,12 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
|
||||
this.registerView(
|
||||
TASK_SPECIFIC_VIEW_TYPE,
|
||||
(leaf) => new TaskSpecificView(leaf, this),
|
||||
(leaf) => new TaskSpecificView(leaf, this)
|
||||
);
|
||||
|
||||
this.registerView(
|
||||
TIMELINE_SIDEBAR_VIEW_TYPE,
|
||||
(leaf) => new TimelineSidebarView(leaf, this),
|
||||
(leaf) => new TimelineSidebarView(leaf, this)
|
||||
);
|
||||
|
||||
try {
|
||||
|
|
@ -548,7 +548,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
new OnboardingView(leaf, this, () => {
|
||||
console.log("Onboarding completed successfully");
|
||||
leaf.detach();
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -623,7 +623,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
t("Open Task Genius view"),
|
||||
() => {
|
||||
this.activateTaskView();
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -681,16 +681,16 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
detectionMethods:
|
||||
this.settings.projectConfig?.metadataConfig
|
||||
?.detectionMethods || [],
|
||||
},
|
||||
}
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[Plugin] Failed to initialize dataflow orchestrator:",
|
||||
error,
|
||||
error
|
||||
);
|
||||
new Notice(
|
||||
t("Failed to initialize task system. Please restart Obsidian."),
|
||||
t("Failed to initialize task system. Please restart Obsidian.")
|
||||
);
|
||||
this.dataflowOrchestrator = undefined;
|
||||
return false;
|
||||
|
|
@ -720,7 +720,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
this.app.vault,
|
||||
this.app.metadataCache,
|
||||
this,
|
||||
getTaskById,
|
||||
getTaskById
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -758,7 +758,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
} catch (error) {
|
||||
console.error(
|
||||
"[Plugin] Failed registering deferred commands:",
|
||||
error,
|
||||
error
|
||||
);
|
||||
}
|
||||
}, 100);
|
||||
|
|
@ -815,7 +815,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
}
|
||||
view.dispatch({
|
||||
effects: toggleTaskFilter.of(
|
||||
!view.state.field(taskFilterState),
|
||||
!view.state.field(taskFilterState)
|
||||
),
|
||||
});
|
||||
},
|
||||
|
|
@ -835,7 +835,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
preset.options = migrateOldFilterOptions(preset.options);
|
||||
}
|
||||
return preset;
|
||||
},
|
||||
}
|
||||
);
|
||||
await this.saveSettings();
|
||||
console.timeEnd("[Task Genius] migratePresetTaskFilters");
|
||||
|
|
@ -853,14 +853,14 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
const changes = sortTasksInDocument(
|
||||
editorView,
|
||||
this,
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
if (changes) {
|
||||
new Notice(
|
||||
t(
|
||||
"Tasks sorted (using settings). Change application needs refinement.",
|
||||
),
|
||||
"Tasks sorted (using settings). Change application needs refinement."
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Notice is already handled within sortTasksInDocument if no changes or sorting disabled
|
||||
|
|
@ -884,11 +884,11 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
return changes;
|
||||
});
|
||||
new Notice(
|
||||
t("Entire document sorted (using settings)."),
|
||||
t("Entire document sorted (using settings).")
|
||||
);
|
||||
} else {
|
||||
new Notice(
|
||||
t("Tasks already sorted or no tasks found."),
|
||||
t("Tasks already sorted or no tasks found.")
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
@ -989,12 +989,10 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
) {
|
||||
// Use dataflow orchestrator for force reindex
|
||||
console.log(
|
||||
"[Command] Force reindexing via dataflow",
|
||||
"[Command] Force reindexing via dataflow"
|
||||
);
|
||||
new Notice(
|
||||
t(
|
||||
"Clearing task cache and rebuilding index...",
|
||||
),
|
||||
t("Clearing task cache and rebuilding index...")
|
||||
);
|
||||
|
||||
// Clear all caches and rebuild from scratch
|
||||
|
|
@ -1094,7 +1092,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
editor,
|
||||
ctx,
|
||||
this,
|
||||
"allCompleted",
|
||||
"allCompleted"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -1109,7 +1107,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
editor,
|
||||
ctx,
|
||||
this,
|
||||
"directChildren",
|
||||
"directChildren"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -1124,7 +1122,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
editor,
|
||||
ctx,
|
||||
this,
|
||||
"all",
|
||||
"all"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -1140,7 +1138,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
editor,
|
||||
ctx,
|
||||
this,
|
||||
"allCompleted",
|
||||
"allCompleted"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -1148,7 +1146,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
this.addCommand({
|
||||
id: "auto-move-direct-completed-subtasks",
|
||||
name: t(
|
||||
"Auto-move direct completed subtasks to default file",
|
||||
"Auto-move direct completed subtasks to default file"
|
||||
),
|
||||
editorCheckCallback: (checking, editor, ctx) => {
|
||||
return autoMoveCompletedTasksCommand(
|
||||
|
|
@ -1156,7 +1154,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
editor,
|
||||
ctx,
|
||||
this,
|
||||
"directChildren",
|
||||
"directChildren"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -1170,7 +1168,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
editor,
|
||||
ctx,
|
||||
this,
|
||||
"all",
|
||||
"all"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -1189,7 +1187,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
editor,
|
||||
ctx,
|
||||
this,
|
||||
"allIncompleted",
|
||||
"allIncompleted"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -1204,7 +1202,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
editor,
|
||||
ctx,
|
||||
this,
|
||||
"directIncompletedChildren",
|
||||
"directIncompletedChildren"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -1220,7 +1218,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
editor,
|
||||
ctx,
|
||||
this,
|
||||
"allIncompleted",
|
||||
"allIncompleted"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -1228,7 +1226,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
this.addCommand({
|
||||
id: "auto-move-direct-incomplete-subtasks",
|
||||
name: t(
|
||||
"Auto-move direct incomplete subtasks to default file",
|
||||
"Auto-move direct incomplete subtasks to default file"
|
||||
),
|
||||
editorCheckCallback: (checking, editor, ctx) => {
|
||||
return autoMoveCompletedTasksCommand(
|
||||
|
|
@ -1236,7 +1234,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
editor,
|
||||
ctx,
|
||||
this,
|
||||
"directIncompletedChildren",
|
||||
"directIncompletedChildren"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -1302,8 +1300,8 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
} catch (e) {
|
||||
new Notice(
|
||||
t(
|
||||
"Could not open quick capture panel in the current editor",
|
||||
),
|
||||
"Could not open quick capture panel in the current editor"
|
||||
)
|
||||
);
|
||||
}
|
||||
}, 100);
|
||||
|
|
@ -1322,7 +1320,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
checking,
|
||||
editor,
|
||||
ctx,
|
||||
this,
|
||||
this
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -1335,7 +1333,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
checking,
|
||||
editor,
|
||||
ctx,
|
||||
this,
|
||||
this
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -1348,7 +1346,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
checking,
|
||||
editor,
|
||||
ctx,
|
||||
this,
|
||||
this
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -1361,7 +1359,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
checking,
|
||||
editor,
|
||||
ctx,
|
||||
this,
|
||||
this
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -1374,7 +1372,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
checking,
|
||||
editor,
|
||||
ctx,
|
||||
this,
|
||||
this
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -1387,13 +1385,26 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
checking,
|
||||
editor,
|
||||
ctx,
|
||||
this,
|
||||
this
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Task timer export/import commands
|
||||
// Ensure timer manager and exporter are initialized if timer is enabled
|
||||
if (this.settings.taskTimer?.enabled) {
|
||||
if (!this.taskTimerManager) {
|
||||
this.taskTimerManager = new TaskTimerManager(
|
||||
this.settings.taskTimer
|
||||
);
|
||||
}
|
||||
if (!this.taskTimerExporter) {
|
||||
this.taskTimerExporter = new TaskTimerExporter(
|
||||
this.taskTimerManager
|
||||
);
|
||||
}
|
||||
}
|
||||
if (this.settings.taskTimer?.enabled && this.taskTimerExporter) {
|
||||
this.addCommand({
|
||||
id: "export-task-timer-data",
|
||||
|
|
@ -1425,7 +1436,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
URL.revokeObjectURL(url);
|
||||
|
||||
new Notice(
|
||||
`Exported ${stats.activeTimers} timer records`,
|
||||
`Exported ${stats.activeTimers} timer records`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error exporting timer data:", error);
|
||||
|
|
@ -1456,17 +1467,17 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
|
||||
if (success) {
|
||||
new Notice(
|
||||
"Timer data imported successfully",
|
||||
"Timer data imported successfully"
|
||||
);
|
||||
} else {
|
||||
new Notice(
|
||||
"Failed to import timer data - invalid format",
|
||||
"Failed to import timer data - invalid format"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Error importing timer data:",
|
||||
error,
|
||||
error
|
||||
);
|
||||
new Notice("Failed to import timer data");
|
||||
}
|
||||
|
|
@ -1510,12 +1521,12 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
URL.revokeObjectURL(url);
|
||||
|
||||
new Notice(
|
||||
`Exported ${stats.activeTimers} timer records to YAML`,
|
||||
`Exported ${stats.activeTimers} timer records to YAML`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Error exporting timer data to YAML:",
|
||||
error,
|
||||
error
|
||||
);
|
||||
new Notice("Failed to export timer data to YAML");
|
||||
}
|
||||
|
|
@ -1563,7 +1574,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
let message = `Task Timer Statistics:\n`;
|
||||
message += `Active timers: ${stats.activeTimers}\n`;
|
||||
message += `Total duration: ${Math.round(
|
||||
stats.totalDuration / 60000,
|
||||
stats.totalDuration / 60000
|
||||
)} minutes\n`;
|
||||
|
||||
if (stats.oldestTimer) {
|
||||
|
|
@ -1593,12 +1604,12 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
// Initialize task timer manager and exporter
|
||||
if (!this.taskTimerManager) {
|
||||
this.taskTimerManager = new TaskTimerManager(
|
||||
this.settings.taskTimer,
|
||||
this.settings.taskTimer
|
||||
);
|
||||
}
|
||||
if (!this.taskTimerExporter) {
|
||||
this.taskTimerExporter = new TaskTimerExporter(
|
||||
this.taskTimerManager,
|
||||
this.taskTimerManager
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1672,7 +1683,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
if (this.settings.quickCapture.enableMinimalMode) {
|
||||
this.minimalQuickCaptureSuggest = new MinimalQuickCaptureSuggest(
|
||||
this.app,
|
||||
this,
|
||||
this
|
||||
);
|
||||
this.registerEditorSuggest(this.minimalQuickCaptureSuggest);
|
||||
}
|
||||
|
|
@ -1699,7 +1710,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
this.dataflowOrchestrator.cleanup().catch((error) => {
|
||||
console.error(
|
||||
"Error cleaning up dataflow orchestrator:",
|
||||
error,
|
||||
error
|
||||
);
|
||||
});
|
||||
// Set to undefined to prevent any further access
|
||||
|
|
@ -1768,11 +1779,11 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
const v2Leaves = workspace.getLeavesOfType(FLUENT_TASK_VIEW);
|
||||
v2Leaves.forEach((leaf) => leaf.detach());
|
||||
const specificLeaves = workspace.getLeavesOfType(
|
||||
TASK_SPECIFIC_VIEW_TYPE,
|
||||
TASK_SPECIFIC_VIEW_TYPE
|
||||
);
|
||||
specificLeaves.forEach((leaf) => leaf.detach());
|
||||
const timelineLeaves = workspace.getLeavesOfType(
|
||||
TIMELINE_SIDEBAR_VIEW_TYPE,
|
||||
TIMELINE_SIDEBAR_VIEW_TYPE
|
||||
);
|
||||
timelineLeaves.forEach((leaf) => leaf.detach());
|
||||
const changelogLeaves = workspace.getLeavesOfType(CHANGELOG_VIEW_TYPE);
|
||||
|
|
@ -1831,7 +1842,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
lastVersion !== "9.9.0"
|
||||
) {
|
||||
console.log(
|
||||
`[Task Genius] Migration detected: ${previousVersion} -> ${currentVersion}, opening onboarding`,
|
||||
`[Task Genius] Migration detected: ${previousVersion} -> ${currentVersion}, opening onboarding`
|
||||
);
|
||||
|
||||
// Directly open onboarding view (same pattern as maybeShowChangelog)
|
||||
|
|
@ -1844,7 +1855,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
} catch (error) {
|
||||
console.error(
|
||||
"[Task Genius] Failed to check migration onboarding:",
|
||||
error,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1885,16 +1896,16 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
enabled: true,
|
||||
lastVersion: "",
|
||||
},
|
||||
this.settings.changelog ?? {},
|
||||
this.settings.changelog ?? {}
|
||||
);
|
||||
try {
|
||||
console.debug(
|
||||
"[Plugin][loadSettings] fileMetadataInheritance (raw):",
|
||||
savedData?.fileMetadataInheritance,
|
||||
savedData?.fileMetadataInheritance
|
||||
);
|
||||
console.debug(
|
||||
"[Plugin][loadSettings] fileMetadataInheritance (effective):",
|
||||
this.settings.fileMetadataInheritance,
|
||||
this.settings.fileMetadataInheritance
|
||||
);
|
||||
} catch {}
|
||||
|
||||
|
|
@ -1904,7 +1915,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
// Repair and validate status cycles
|
||||
if (this.settings.statusCycles) {
|
||||
this.settings.statusCycles = repairStatusCycles(
|
||||
this.settings.statusCycles,
|
||||
this.settings.statusCycles
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1946,7 +1957,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
try {
|
||||
console.debug(
|
||||
"[Plugin][saveSettings] fileMetadataInheritance:",
|
||||
this.settings?.fileMetadataInheritance,
|
||||
this.settings?.fileMetadataInheritance
|
||||
);
|
||||
} catch {}
|
||||
await this.saveData(this.settings);
|
||||
|
|
@ -1963,7 +1974,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
// Add any missing default views to user settings
|
||||
defaultViews.forEach((defaultView) => {
|
||||
const existingView = this.settings.viewConfiguration.find(
|
||||
(v) => v.id === defaultView.id,
|
||||
(v) => v.id === defaultView.id
|
||||
);
|
||||
if (!existingView) {
|
||||
this.settings.viewConfiguration.push({ ...defaultView });
|
||||
|
|
@ -2027,7 +2038,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
|
||||
// Check if view is already open
|
||||
const existingLeaves = workspace.getLeavesOfType(
|
||||
TIMELINE_SIDEBAR_VIEW_TYPE,
|
||||
TIMELINE_SIDEBAR_VIEW_TYPE
|
||||
);
|
||||
|
||||
if (existingLeaves.length > 0) {
|
||||
|
|
@ -2073,7 +2084,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
|
||||
// Update Timeline Sidebar Views
|
||||
const timelineViewLeaves = this.app.workspace.getLeavesOfType(
|
||||
TIMELINE_SIDEBAR_VIEW_TYPE,
|
||||
TIMELINE_SIDEBAR_VIEW_TYPE
|
||||
);
|
||||
if (timelineViewLeaves.length > 0) {
|
||||
for (const leaf of timelineViewLeaves) {
|
||||
|
|
@ -2107,7 +2118,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
|
||||
if (!diagnosticInfo.canWrite) {
|
||||
throw new Error(
|
||||
"Cannot write to version storage - storage may be corrupted",
|
||||
"Cannot write to version storage - storage may be corrupted"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -2116,7 +2127,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
diagnosticInfo.previousVersion
|
||||
) {
|
||||
console.warn(
|
||||
"Invalid version data detected, attempting recovery",
|
||||
"Invalid version data detected, attempting recovery"
|
||||
);
|
||||
await this.versionManager.recoverFromCorruptedVersion();
|
||||
}
|
||||
|
|
@ -2127,7 +2138,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
|
||||
if (versionResult.requiresRebuild) {
|
||||
console.log(
|
||||
`Task Genius (Dataflow): ${versionResult.rebuildReason}`,
|
||||
`Task Genius (Dataflow): ${versionResult.rebuildReason}`
|
||||
);
|
||||
|
||||
// Get all supported files for progress tracking
|
||||
|
|
@ -2136,13 +2147,13 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
.filter(
|
||||
(file) =>
|
||||
file.extension === "md" ||
|
||||
file.extension === "canvas",
|
||||
file.extension === "canvas"
|
||||
);
|
||||
|
||||
// Start rebuild progress tracking
|
||||
this.rebuildProgressManager.startRebuild(
|
||||
allFiles.length,
|
||||
versionResult.rebuildReason,
|
||||
versionResult.rebuildReason
|
||||
);
|
||||
|
||||
// After dataflow rebuild, refresh habits to keep in sync
|
||||
|
|
@ -2168,20 +2179,20 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
} else {
|
||||
// No rebuild needed, dataflow already initialized during creation
|
||||
console.log(
|
||||
"Task Genius (Dataflow): No rebuild needed, using existing cache",
|
||||
"Task Genius (Dataflow): No rebuild needed, using existing cache"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Error during dataflow initialization with version check:",
|
||||
error,
|
||||
error
|
||||
);
|
||||
|
||||
// Trigger emergency rebuild for dataflow
|
||||
try {
|
||||
const emergencyResult =
|
||||
await this.versionManager.handleEmergencyRebuild(
|
||||
`Dataflow initialization failed: ${error.message}`,
|
||||
`Dataflow initialization failed: ${error.message}`
|
||||
);
|
||||
|
||||
// Get all supported files for progress tracking
|
||||
|
|
@ -2190,13 +2201,13 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
.filter(
|
||||
(file) =>
|
||||
file.extension === "md" ||
|
||||
file.extension === "canvas",
|
||||
file.extension === "canvas"
|
||||
);
|
||||
|
||||
// Start emergency rebuild
|
||||
this.rebuildProgressManager.startRebuild(
|
||||
allFiles.length,
|
||||
emergencyResult.rebuildReason,
|
||||
emergencyResult.rebuildReason
|
||||
);
|
||||
|
||||
// Force rebuild dataflow
|
||||
|
|
@ -2214,12 +2225,12 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
await this.versionManager.markVersionProcessed();
|
||||
|
||||
console.log(
|
||||
"Emergency dataflow rebuild completed successfully",
|
||||
"Emergency dataflow rebuild completed successfully"
|
||||
);
|
||||
} catch (emergencyError) {
|
||||
console.error(
|
||||
"Emergency dataflow rebuild failed:",
|
||||
emergencyError,
|
||||
emergencyError
|
||||
);
|
||||
throw emergencyError;
|
||||
}
|
||||
|
|
@ -2234,7 +2245,7 @@ export default class TaskProgressBarPlugin extends Plugin {
|
|||
private async initializeTaskManagerWithVersionCheck(): Promise<void> {
|
||||
// This method is deprecated and should not be called
|
||||
console.warn(
|
||||
"initializeTaskManagerWithVersionCheck is deprecated and should not be used",
|
||||
"initializeTaskManagerWithVersionCheck is deprecated and should not be used"
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, App } from "obsidian";
|
||||
import { Component, App, Notice } from "obsidian";
|
||||
import { Task } from "../types/task";
|
||||
import {
|
||||
OnCompletionConfig,
|
||||
|
|
@ -15,11 +15,16 @@ import { CompleteActionExecutor } from "../executors/completion/complete-executo
|
|||
import { MoveActionExecutor } from "../executors/completion/move-executor";
|
||||
import { ArchiveActionExecutor } from "../executors/completion/archive-executor";
|
||||
import { DuplicateActionExecutor } from "../executors/completion/duplicate-executor";
|
||||
import { TaskTimerManager } from "./timer-manager";
|
||||
import { t } from "@/translations/helper";
|
||||
|
||||
export class OnCompletionManager extends Component {
|
||||
private executors: Map<OnCompletionActionType, BaseActionExecutor>;
|
||||
|
||||
constructor(private app: App, private plugin: TaskProgressBarPlugin) {
|
||||
constructor(
|
||||
private app: App,
|
||||
private plugin: TaskProgressBarPlugin,
|
||||
) {
|
||||
super();
|
||||
this.executors = new Map();
|
||||
this.initializeExecutors();
|
||||
|
|
@ -30,8 +35,8 @@ export class OnCompletionManager extends Component {
|
|||
this.plugin.registerEvent(
|
||||
this.app.workspace.on(
|
||||
"task-genius:task-completed",
|
||||
this.handleTaskCompleted.bind(this)
|
||||
)
|
||||
this.handleTaskCompleted.bind(this),
|
||||
),
|
||||
);
|
||||
|
||||
console.log("OnCompletionManager loaded");
|
||||
|
|
@ -40,32 +45,36 @@ export class OnCompletionManager extends Component {
|
|||
private initializeExecutors() {
|
||||
this.executors.set(
|
||||
OnCompletionActionType.DELETE,
|
||||
new DeleteActionExecutor()
|
||||
new DeleteActionExecutor(),
|
||||
);
|
||||
this.executors.set(
|
||||
OnCompletionActionType.KEEP,
|
||||
new KeepActionExecutor()
|
||||
new KeepActionExecutor(),
|
||||
);
|
||||
this.executors.set(
|
||||
OnCompletionActionType.COMPLETE,
|
||||
new CompleteActionExecutor()
|
||||
new CompleteActionExecutor(),
|
||||
);
|
||||
this.executors.set(
|
||||
OnCompletionActionType.MOVE,
|
||||
new MoveActionExecutor()
|
||||
new MoveActionExecutor(),
|
||||
);
|
||||
this.executors.set(
|
||||
OnCompletionActionType.ARCHIVE,
|
||||
new ArchiveActionExecutor()
|
||||
new ArchiveActionExecutor(),
|
||||
);
|
||||
this.executors.set(
|
||||
OnCompletionActionType.DUPLICATE,
|
||||
new DuplicateActionExecutor()
|
||||
new DuplicateActionExecutor(),
|
||||
);
|
||||
}
|
||||
|
||||
private async handleTaskCompleted(task: Task) {
|
||||
console.log("handleTaskCompleted", task);
|
||||
|
||||
// Auto-stop timer when task is completed (if timer is enabled)
|
||||
await this.autoStopTimerForCompletedTask(task);
|
||||
|
||||
// 检查是否存在 onCompletion 属性,但允许空值进入解析逻辑
|
||||
if (!task.metadata.hasOwnProperty("onCompletion")) {
|
||||
return;
|
||||
|
|
@ -73,7 +82,7 @@ export class OnCompletionManager extends Component {
|
|||
|
||||
try {
|
||||
const parseResult = this.parseOnCompletion(
|
||||
task.metadata.onCompletion || ""
|
||||
task.metadata.onCompletion || "",
|
||||
);
|
||||
|
||||
console.log("parseResult", parseResult);
|
||||
|
|
@ -81,7 +90,7 @@ export class OnCompletionManager extends Component {
|
|||
if (!parseResult.isValid || !parseResult.config) {
|
||||
console.warn(
|
||||
"Invalid onCompletion configuration:",
|
||||
parseResult.error
|
||||
parseResult.error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -93,7 +102,7 @@ export class OnCompletionManager extends Component {
|
|||
}
|
||||
|
||||
public parseOnCompletion(
|
||||
onCompletionValue: string
|
||||
onCompletionValue: string,
|
||||
): OnCompletionParseResult {
|
||||
if (!onCompletionValue || typeof onCompletionValue !== "string") {
|
||||
return {
|
||||
|
|
@ -110,7 +119,7 @@ export class OnCompletionManager extends Component {
|
|||
// Try to parse as JSON first (structured format)
|
||||
if (trimmedValue.startsWith("{")) {
|
||||
const config = JSON.parse(
|
||||
onCompletionValue
|
||||
onCompletionValue,
|
||||
) as OnCompletionConfig;
|
||||
return {
|
||||
config,
|
||||
|
|
@ -216,7 +225,7 @@ export class OnCompletionManager extends Component {
|
|||
|
||||
public async executeOnCompletion(
|
||||
task: Task,
|
||||
config: OnCompletionConfig
|
||||
config: OnCompletionConfig,
|
||||
): Promise<OnCompletionExecutionResult> {
|
||||
const executor = this.executors.get(config.type);
|
||||
|
||||
|
|
@ -243,6 +252,55 @@ export class OnCompletionManager extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-stop timer when a task is completed
|
||||
* Checks if the task has an active timer and completes it
|
||||
*/
|
||||
private async autoStopTimerForCompletedTask(task: Task): Promise<void> {
|
||||
// Check if timer feature is enabled
|
||||
if (!this.plugin.settings.taskTimer?.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get block ID from task metadata
|
||||
const blockId = task.metadata?.id;
|
||||
if (!blockId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const timerManager = new TaskTimerManager(
|
||||
this.plugin.settings.taskTimer,
|
||||
);
|
||||
|
||||
// Check if there's an active timer for this task
|
||||
const timer = timerManager.getTimerByFileAndBlock(
|
||||
task.filePath,
|
||||
blockId,
|
||||
);
|
||||
|
||||
if (
|
||||
timer &&
|
||||
(timer.status === "running" || timer.status === "paused")
|
||||
) {
|
||||
// Complete the timer and get duration
|
||||
const duration = timerManager.completeTimer(timer.taskId);
|
||||
|
||||
if (duration) {
|
||||
console.log(
|
||||
`[OnCompletionManager] Timer auto-stopped for completed task. Duration: ${duration}`,
|
||||
);
|
||||
new Notice(t("Timer stopped") + `: ${duration}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[OnCompletionManager] Error auto-stopping timer:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onunload() {
|
||||
this.executors.clear();
|
||||
console.log("OnCompletionManager unloaded");
|
||||
|
|
|
|||
|
|
@ -40,6 +40,19 @@ export interface LegacyTimerState {
|
|||
createdAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Completed timer record for history display
|
||||
*/
|
||||
export interface CompletedTimerRecord {
|
||||
taskId: string;
|
||||
filePath: string;
|
||||
blockId: string;
|
||||
duration: number;
|
||||
completedAt: number;
|
||||
createdAt: number;
|
||||
segments: TimeSegment[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Manager for task timer state and localStorage operations
|
||||
*/
|
||||
|
|
@ -47,11 +60,39 @@ export class TaskTimerManager {
|
|||
private settings: TaskTimerSettings;
|
||||
private readonly STORAGE_PREFIX = "taskTimer_";
|
||||
private readonly TIMER_LIST_KEY = "taskTimer_activeList";
|
||||
private readonly COMPLETED_LIST_KEY = "taskTimer_completedList";
|
||||
private readonly MAX_COMPLETED_HISTORY = 200;
|
||||
|
||||
constructor(settings: TaskTimerSettings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
private loadFromStorage(key: string): string | null {
|
||||
const appInstance = (window as any).app as App | undefined;
|
||||
if (appInstance?.loadLocalStorage) {
|
||||
return appInstance.loadLocalStorage(key);
|
||||
}
|
||||
if (typeof localStorage !== "undefined") {
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private saveToStorage(key: string, value: string | null): void {
|
||||
const appInstance = (window as any).app as App | undefined;
|
||||
if (appInstance?.saveLocalStorage) {
|
||||
appInstance.saveLocalStorage(key, value);
|
||||
return;
|
||||
}
|
||||
if (typeof localStorage !== "undefined") {
|
||||
if (value === null) {
|
||||
localStorage.removeItem(key);
|
||||
} else {
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique block reference ID
|
||||
* @param prefix Optional prefix to use (defaults to settings)
|
||||
|
|
@ -85,7 +126,9 @@ export class TaskTimerManager {
|
|||
startTimer(filePath: string, existingBlockId?: string): string {
|
||||
try {
|
||||
console.log(
|
||||
`[TaskTimerManager] Starting timer for file: ${filePath}, blockId: ${existingBlockId || "new"}`,
|
||||
`[TaskTimerManager] Starting timer for file: ${filePath}, blockId: ${
|
||||
existingBlockId || "new"
|
||||
}`
|
||||
);
|
||||
|
||||
const blockId = existingBlockId || this.generateBlockId();
|
||||
|
|
@ -94,7 +137,7 @@ export class TaskTimerManager {
|
|||
|
||||
if (!blockId) {
|
||||
console.error(
|
||||
"[TaskTimerManager] Failed to generate or use block ID",
|
||||
"[TaskTimerManager] Failed to generate or use block ID"
|
||||
);
|
||||
throw new Error("Block ID generation failed");
|
||||
}
|
||||
|
|
@ -104,7 +147,7 @@ export class TaskTimerManager {
|
|||
|
||||
if (existingTimer) {
|
||||
console.log(
|
||||
`[TaskTimerManager] Found existing timer with status: ${existingTimer.status}`,
|
||||
`[TaskTimerManager] Found existing timer with status: ${existingTimer.status}`
|
||||
);
|
||||
// Resume existing timer
|
||||
if (existingTimer.status === "paused") {
|
||||
|
|
@ -129,21 +172,18 @@ export class TaskTimerManager {
|
|||
|
||||
// Save timer state
|
||||
try {
|
||||
((window as any).app as App).saveLocalStorage(
|
||||
taskId,
|
||||
JSON.stringify(timerState),
|
||||
);
|
||||
this.saveToStorage(taskId, JSON.stringify(timerState));
|
||||
this.addToActiveList(taskId);
|
||||
console.log(
|
||||
`[TaskTimerManager] Successfully created new timer: ${taskId}`,
|
||||
`[TaskTimerManager] Successfully created new timer: ${taskId}`
|
||||
);
|
||||
} catch (storageError) {
|
||||
console.error(
|
||||
"[TaskTimerManager] Failed to save timer to localStorage:",
|
||||
storageError,
|
||||
storageError
|
||||
);
|
||||
throw new Error(
|
||||
"Failed to save timer state - localStorage may be full or unavailable",
|
||||
"Failed to save timer state - localStorage may be full or unavailable"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +191,7 @@ export class TaskTimerManager {
|
|||
} catch (error) {
|
||||
console.error(
|
||||
"[TaskTimerManager] Critical error starting timer:",
|
||||
error,
|
||||
error
|
||||
);
|
||||
throw error; // Re-throw to let caller handle
|
||||
}
|
||||
|
|
@ -179,10 +219,7 @@ export class TaskTimerManager {
|
|||
|
||||
timerState.status = "paused";
|
||||
|
||||
(window as any).app.saveLocalStorage(
|
||||
taskId,
|
||||
JSON.stringify(timerState),
|
||||
);
|
||||
this.saveToStorage(taskId, JSON.stringify(timerState));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -204,10 +241,7 @@ export class TaskTimerManager {
|
|||
|
||||
timerState.status = "running";
|
||||
|
||||
(window as any).app.saveLocalStorage(
|
||||
taskId,
|
||||
JSON.stringify(timerState),
|
||||
);
|
||||
this.saveToStorage(taskId, JSON.stringify(timerState));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -230,10 +264,7 @@ export class TaskTimerManager {
|
|||
];
|
||||
timerState.status = "running";
|
||||
|
||||
(window as any).app.saveLocalStorage(
|
||||
taskId,
|
||||
JSON.stringify(timerState),
|
||||
);
|
||||
this.saveToStorage(taskId, JSON.stringify(timerState));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -248,7 +279,7 @@ export class TaskTimerManager {
|
|||
const timerState = this.getTimerState(taskId);
|
||||
if (!timerState) {
|
||||
console.warn(
|
||||
`[TaskTimerManager] Timer not found for completion: ${taskId}`,
|
||||
`[TaskTimerManager] Timer not found for completion: ${taskId}`
|
||||
);
|
||||
return "";
|
||||
}
|
||||
|
|
@ -268,27 +299,38 @@ export class TaskTimerManager {
|
|||
// Calculate total duration from all segments
|
||||
const totalDuration = this.calculateTotalDuration(timerState);
|
||||
console.log(
|
||||
`[TaskTimerManager] Total duration from ${timerState.segments.length} segments: ${totalDuration}ms`,
|
||||
`[TaskTimerManager] Total duration from ${timerState.segments.length} segments: ${totalDuration}ms`
|
||||
);
|
||||
|
||||
// Validate duration
|
||||
if (totalDuration < 0) {
|
||||
console.error(
|
||||
`[TaskTimerManager] Invalid duration calculated: ${totalDuration}ms`,
|
||||
`[TaskTimerManager] Invalid duration calculated: ${totalDuration}ms`
|
||||
);
|
||||
return this.formatDuration(0);
|
||||
}
|
||||
|
||||
// Record completion history before removing timer
|
||||
this.recordCompletedTimer({
|
||||
taskId: timerState.taskId,
|
||||
filePath: timerState.filePath,
|
||||
blockId: timerState.blockId,
|
||||
duration: totalDuration,
|
||||
completedAt: now,
|
||||
createdAt: timerState.createdAt,
|
||||
segments: timerState.segments,
|
||||
});
|
||||
|
||||
// Remove from storage
|
||||
try {
|
||||
this.removeTimer(taskId);
|
||||
console.log(
|
||||
`[TaskTimerManager] Successfully removed completed timer from storage`,
|
||||
`[TaskTimerManager] Successfully removed completed timer from storage`
|
||||
);
|
||||
} catch (removalError) {
|
||||
console.error(
|
||||
"[TaskTimerManager] Failed to remove timer from storage:",
|
||||
removalError,
|
||||
removalError
|
||||
);
|
||||
// Continue anyway - we can still return the duration
|
||||
}
|
||||
|
|
@ -296,13 +338,13 @@ export class TaskTimerManager {
|
|||
// Format and return duration
|
||||
const formattedDuration = this.formatDuration(totalDuration);
|
||||
console.log(
|
||||
`[TaskTimerManager] Timer completed successfully, duration: ${formattedDuration}`,
|
||||
`[TaskTimerManager] Timer completed successfully, duration: ${formattedDuration}`
|
||||
);
|
||||
return formattedDuration;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[TaskTimerManager] Critical error completing timer:",
|
||||
error,
|
||||
error
|
||||
);
|
||||
// Return empty string to prevent crashes, but log the issue
|
||||
return "";
|
||||
|
|
@ -316,7 +358,7 @@ export class TaskTimerManager {
|
|||
*/
|
||||
getTimerState(taskId: string): TimerState | null {
|
||||
try {
|
||||
const stored = (window as any).app.localStorage(taskId);
|
||||
const stored = this.loadFromStorage(taskId);
|
||||
if (!stored) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -326,16 +368,13 @@ export class TaskTimerManager {
|
|||
// Check if this is a legacy format that needs migration
|
||||
if (this.isLegacyFormat(parsed)) {
|
||||
console.log(
|
||||
`[TaskTimerManager] Migrating legacy timer state for ${taskId}`,
|
||||
`[TaskTimerManager] Migrating legacy timer state for ${taskId}`
|
||||
);
|
||||
const migrated = this.migrateLegacyState(
|
||||
parsed as LegacyTimerState,
|
||||
parsed as LegacyTimerState
|
||||
);
|
||||
// Save migrated state
|
||||
(window as any).app.saveLocalStorage(
|
||||
taskId,
|
||||
JSON.stringify(migrated),
|
||||
);
|
||||
this.saveToStorage(taskId, JSON.stringify(migrated));
|
||||
return migrated;
|
||||
}
|
||||
|
||||
|
|
@ -343,10 +382,10 @@ export class TaskTimerManager {
|
|||
if (!this.validateTimerState(parsed)) {
|
||||
console.error(
|
||||
`[TaskTimerManager] Invalid timer state structure for ${taskId}:`,
|
||||
parsed,
|
||||
parsed
|
||||
);
|
||||
// Clean up corrupted data
|
||||
(window as any).app.saveLocalStorage(taskId, null);
|
||||
this.saveToStorage(taskId, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -354,15 +393,15 @@ export class TaskTimerManager {
|
|||
} catch (error) {
|
||||
console.error(
|
||||
`[TaskTimerManager] Error retrieving timer state for ${taskId}:`,
|
||||
error,
|
||||
error
|
||||
);
|
||||
// Clean up corrupted data
|
||||
try {
|
||||
(window as any).app.saveLocalStorage(taskId, null);
|
||||
this.saveToStorage(taskId, null);
|
||||
} catch (cleanupError) {
|
||||
console.error(
|
||||
"[TaskTimerManager] Failed to clean up corrupted timer data:",
|
||||
cleanupError,
|
||||
cleanupError
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
|
@ -469,7 +508,7 @@ export class TaskTimerManager {
|
|||
*/
|
||||
getTimerByFileAndBlock(
|
||||
filePath: string,
|
||||
blockId: string,
|
||||
blockId: string
|
||||
): TimerState | null {
|
||||
const taskId = this.getStorageKey(filePath, blockId);
|
||||
return this.getTimerState(taskId);
|
||||
|
|
@ -480,7 +519,7 @@ export class TaskTimerManager {
|
|||
* @param taskId Timer task ID
|
||||
*/
|
||||
removeTimer(taskId: string): void {
|
||||
(window as any).app.saveLocalStorage(taskId, null);
|
||||
this.saveToStorage(taskId, null);
|
||||
this.removeFromActiveList(taskId);
|
||||
}
|
||||
|
||||
|
|
@ -590,9 +629,7 @@ export class TaskTimerManager {
|
|||
* @returns Array of active timer task IDs
|
||||
*/
|
||||
private getActiveList(): string[] {
|
||||
const stored = (window as any).app.loadLocalStorage(
|
||||
this.TIMER_LIST_KEY,
|
||||
);
|
||||
const stored = this.loadFromStorage(this.TIMER_LIST_KEY);
|
||||
if (!stored) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -613,10 +650,7 @@ export class TaskTimerManager {
|
|||
const activeList = this.getActiveList();
|
||||
if (!activeList.includes(taskId)) {
|
||||
activeList.push(taskId);
|
||||
(window as any).app.saveLocalStorage(
|
||||
this.TIMER_LIST_KEY,
|
||||
JSON.stringify(activeList),
|
||||
);
|
||||
this.saveToStorage(this.TIMER_LIST_KEY, JSON.stringify(activeList));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -627,10 +661,53 @@ export class TaskTimerManager {
|
|||
private removeFromActiveList(taskId: string): void {
|
||||
const activeList = this.getActiveList();
|
||||
const filtered = activeList.filter((id) => id !== taskId);
|
||||
(window as any).app.saveLocalStorage(
|
||||
this.TIMER_LIST_KEY,
|
||||
JSON.stringify(filtered),
|
||||
);
|
||||
this.saveToStorage(this.TIMER_LIST_KEY, JSON.stringify(filtered));
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a completed timer into history (bounded list)
|
||||
*/
|
||||
private recordCompletedTimer(record: CompletedTimerRecord): void {
|
||||
const completed = this.getCompletedList();
|
||||
// Newest first
|
||||
completed.unshift(record);
|
||||
|
||||
// Trim history to avoid unbounded growth
|
||||
if (completed.length > this.MAX_COMPLETED_HISTORY) {
|
||||
completed.length = this.MAX_COMPLETED_HISTORY;
|
||||
}
|
||||
|
||||
this.saveToStorage(this.COMPLETED_LIST_KEY, JSON.stringify(completed));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve completed timer history
|
||||
* @returns Ordered list (newest first)
|
||||
*/
|
||||
getRecentCompletedTimers(limit: number = 50): CompletedTimerRecord[] {
|
||||
const completed = this.getCompletedList();
|
||||
if (limit && limit > 0) {
|
||||
return completed.slice(0, limit);
|
||||
}
|
||||
return completed;
|
||||
}
|
||||
|
||||
private getCompletedList(): CompletedTimerRecord[] {
|
||||
const stored = this.loadFromStorage(this.COMPLETED_LIST_KEY);
|
||||
if (!stored) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(stored) as CompletedTimerRecord[];
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.error("Error parsing completed timer list:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -570,11 +570,13 @@ export class FluentTaskView extends ItemView {
|
|||
this.updateView();
|
||||
},
|
||||
onNavigateToView: (viewId) => {
|
||||
console.log(`[Task Genius] onNavigateToView called with viewId: ${viewId}, previous: ${this.currentViewId}`);
|
||||
this.recordViewModeForView(
|
||||
this.currentViewId,
|
||||
this.viewState.viewMode
|
||||
);
|
||||
this.currentViewId = viewId;
|
||||
console.log(`[Task Genius] currentViewId updated to: ${this.currentViewId}`);
|
||||
|
||||
// When navigating to projects view directly, clear project selection
|
||||
// This enables the full project overview mode
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export class TaskTimerExporter {
|
|||
activeTimers: activeTimers.map((timer) => ({
|
||||
...timer,
|
||||
currentDuration: this.timerManager.getCurrentDuration(
|
||||
timer.taskId,
|
||||
timer.taskId
|
||||
),
|
||||
})),
|
||||
};
|
||||
|
|
@ -114,19 +114,35 @@ export class TaskTimerExporter {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Get current active list or create empty array
|
||||
const activeListKey = "taskTimer_activeList";
|
||||
const existingListRaw = (window as any).app.loadLocalStorage(
|
||||
activeListKey
|
||||
);
|
||||
let activeList: string[] = [];
|
||||
try {
|
||||
activeList = existingListRaw ? JSON.parse(existingListRaw) : [];
|
||||
} catch {
|
||||
activeList = [];
|
||||
}
|
||||
|
||||
// Restore each timer
|
||||
for (const timerData of backup.activeTimers) {
|
||||
// Recreate timer state in localStorage
|
||||
// Convert old format to new segments format
|
||||
const segments = [];
|
||||
if (timerData.startTime) {
|
||||
// Handle both old format and new segments format
|
||||
let segments = [];
|
||||
if (timerData.segments && Array.isArray(timerData.segments)) {
|
||||
// New format - use segments directly
|
||||
segments = timerData.segments;
|
||||
} else if (timerData.startTime) {
|
||||
// Convert old format to new segments format
|
||||
segments.push({
|
||||
startTime: timerData.startTime,
|
||||
endTime: timerData.pausedTime,
|
||||
duration: timerData.pausedTime
|
||||
? timerData.pausedTime -
|
||||
timerData.startTime -
|
||||
(timerData.totalPausedDuration || 0)
|
||||
timerData.startTime -
|
||||
(timerData.totalPausedDuration || 0)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
|
@ -145,12 +161,27 @@ export class TaskTimerExporter {
|
|||
timerData.totalPausedDuration || 0,
|
||||
};
|
||||
|
||||
// Save timer to storage
|
||||
(window as any).app.saveLocalStorage(
|
||||
timerData.taskId,
|
||||
JSON.stringify(restoredTimer),
|
||||
JSON.stringify(restoredTimer)
|
||||
);
|
||||
|
||||
// Add to active list if not already present
|
||||
if (!activeList.includes(timerData.taskId)) {
|
||||
activeList.push(timerData.taskId);
|
||||
}
|
||||
}
|
||||
|
||||
// Save updated active list
|
||||
(window as any).app.saveLocalStorage(
|
||||
activeListKey,
|
||||
JSON.stringify(activeList)
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Restored ${backup.activeTimers.length} timers from backup`
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error restoring from backup:", error);
|
||||
|
|
@ -218,7 +249,7 @@ export class TaskTimerExporter {
|
|||
}
|
||||
|
||||
const currentDuration = this.timerManager.getCurrentDuration(
|
||||
timer.taskId,
|
||||
timer.taskId
|
||||
);
|
||||
|
||||
// Get the first and last segments for export
|
||||
|
|
@ -260,20 +291,77 @@ export class TaskTimerExporter {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Get current active list or create empty array
|
||||
const activeListKey = "taskTimer_activeList";
|
||||
const existingListRaw = (window as any).app.loadLocalStorage(
|
||||
activeListKey
|
||||
);
|
||||
let activeList: string[] = [];
|
||||
try {
|
||||
activeList = existingListRaw ? JSON.parse(existingListRaw) : [];
|
||||
} catch {
|
||||
activeList = [];
|
||||
}
|
||||
|
||||
let importedCount = 0;
|
||||
let activeCount = 0;
|
||||
|
||||
for (const timerData of data.timers) {
|
||||
try {
|
||||
// Only import completed timers to avoid conflicts
|
||||
if (timerData.status === "idle" || timerData.endTime) {
|
||||
// Store as historical data (could be extended for analytics)
|
||||
// Import all timers, not just completed ones
|
||||
if (
|
||||
timerData.status === "running" ||
|
||||
timerData.status === "paused"
|
||||
) {
|
||||
// Convert to TimerState format and restore as active timer
|
||||
const segments = [];
|
||||
if (timerData.startTime) {
|
||||
segments.push({
|
||||
startTime: timerData.startTime,
|
||||
endTime: timerData.endTime,
|
||||
duration: timerData.endTime
|
||||
? timerData.endTime -
|
||||
timerData.startTime -
|
||||
(timerData.totalPausedDuration || 0)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
const restoredTimer: TimerState = {
|
||||
taskId: timerData.taskId,
|
||||
filePath: timerData.filePath,
|
||||
blockId: timerData.blockId,
|
||||
segments: segments,
|
||||
status: timerData.status as "running" | "paused",
|
||||
createdAt: timerData.createdAt || Date.now(),
|
||||
legacyStartTime: timerData.startTime,
|
||||
legacyPausedTime: timerData.endTime,
|
||||
legacyTotalPausedDuration:
|
||||
timerData.totalPausedDuration || 0,
|
||||
};
|
||||
|
||||
// Save timer to storage
|
||||
(window as any).app.saveLocalStorage(
|
||||
timerData.taskId,
|
||||
JSON.stringify(restoredTimer)
|
||||
);
|
||||
|
||||
// Add to active list if not already present
|
||||
if (!activeList.includes(timerData.taskId)) {
|
||||
activeList.push(timerData.taskId);
|
||||
}
|
||||
|
||||
activeCount++;
|
||||
importedCount++;
|
||||
} else if (timerData.status === "idle" || timerData.endTime) {
|
||||
// Store completed timers as historical data
|
||||
const historyKey = `taskTimer_history_${timerData.blockId}_${timerData.startTime}`;
|
||||
(window as any).app.saveLocalStorage(
|
||||
historyKey,
|
||||
JSON.stringify({
|
||||
...timerData,
|
||||
importedAt: Date.now(),
|
||||
}),
|
||||
})
|
||||
);
|
||||
importedCount++;
|
||||
}
|
||||
|
|
@ -281,12 +369,20 @@ export class TaskTimerExporter {
|
|||
console.warn(
|
||||
"Failed to import timer:",
|
||||
timerData.taskId,
|
||||
error,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Successfully imported ${importedCount} timer records`);
|
||||
// Save updated active list
|
||||
(window as any).app.saveLocalStorage(
|
||||
activeListKey,
|
||||
JSON.stringify(activeList)
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Successfully imported ${importedCount} timer records (${activeCount} active)`
|
||||
);
|
||||
return importedCount > 0;
|
||||
}
|
||||
|
||||
|
|
@ -331,7 +427,10 @@ export class TaskTimerExporter {
|
|||
|
||||
if (Array.isArray(obj)) {
|
||||
for (const item of obj) {
|
||||
yaml += `${spaces}- ${this.convertToYAML(item, indent + 1).trim()}\n`;
|
||||
yaml += `${spaces}- ${this.convertToYAML(
|
||||
item,
|
||||
indent + 1
|
||||
).trim()}\n`;
|
||||
}
|
||||
} else if (obj !== null && typeof obj === "object") {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
|
|
|
|||
335
src/styles/timer-statistics.css
Normal file
335
src/styles/timer-statistics.css
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
/* Timer Statistics Panel Styles */
|
||||
|
||||
.timer-statistics-panel {
|
||||
--timer-card-bg: var(--background-secondary);
|
||||
--timer-running-color: var(--color-green);
|
||||
--timer-paused-color: var(--color-orange);
|
||||
--timer-stopped-color: var(--color-red);
|
||||
padding: var(--size-4-4);
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.timer-stats-header {
|
||||
margin-bottom: var(--size-4-4);
|
||||
}
|
||||
|
||||
.timer-stats-title {
|
||||
font-size: var(--font-ui-large);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-normal);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Summary Cards */
|
||||
.timer-stats-summary {
|
||||
margin-bottom: var(--size-4-6);
|
||||
}
|
||||
|
||||
.timer-stats-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: var(--size-4-3);
|
||||
}
|
||||
|
||||
.timer-stats-card {
|
||||
background: var(--timer-card-bg);
|
||||
border-radius: var(--radius-m);
|
||||
padding: var(--size-4-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.timer-stats-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.timer-stats-card-icon {
|
||||
color: var(--text-muted);
|
||||
margin-bottom: var(--size-4-2);
|
||||
}
|
||||
|
||||
.timer-stats-card.running .timer-stats-card-icon {
|
||||
color: var(--timer-running-color);
|
||||
}
|
||||
|
||||
.timer-stats-card.paused .timer-stats-card-icon {
|
||||
color: var(--timer-paused-color);
|
||||
}
|
||||
|
||||
.timer-stats-card-value {
|
||||
font-size: var(--font-ui-large);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-normal);
|
||||
margin-bottom: var(--size-4-1);
|
||||
}
|
||||
|
||||
.timer-stats-card-label {
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
.timer-stats-section {
|
||||
margin-bottom: var(--size-4-6);
|
||||
}
|
||||
|
||||
.timer-stats-section h3 {
|
||||
font-size: var(--font-ui-medium);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-normal);
|
||||
margin: 0 0 var(--size-4-3) 0;
|
||||
padding-bottom: var(--size-4-2);
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.timer-stats-empty {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
padding: var(--size-4-4);
|
||||
text-align: center;
|
||||
background: var(--background-secondary);
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
|
||||
/* Timer Groups */
|
||||
.timer-group {
|
||||
margin-bottom: var(--size-4-4);
|
||||
}
|
||||
|
||||
.timer-group-title {
|
||||
font-size: var(--font-ui-small);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: var(--size-4-2);
|
||||
}
|
||||
|
||||
.timer-group-running .timer-group-title {
|
||||
color: var(--timer-running-color);
|
||||
}
|
||||
|
||||
.timer-group-paused .timer-group-title {
|
||||
color: var(--timer-paused-color);
|
||||
}
|
||||
|
||||
/* Timer Items */
|
||||
.timer-group-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-4-2);
|
||||
}
|
||||
|
||||
.timer-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-4-3);
|
||||
padding: var(--size-4-3);
|
||||
background: var(--background-secondary);
|
||||
border-radius: var(--radius-m);
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.timer-item:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.timer-item-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.timer-item-name {
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-normal);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.timer-item-path {
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.timer-item-duration {
|
||||
font-family: var(--font-monospace);
|
||||
font-size: var(--font-ui-medium);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-normal);
|
||||
min-width: 80px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.timer-item-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.timer-status-running {
|
||||
color: var(--timer-running-color);
|
||||
}
|
||||
|
||||
.timer-status-paused {
|
||||
color: var(--timer-paused-color);
|
||||
}
|
||||
|
||||
/* Timer Actions */
|
||||
.timer-item-actions {
|
||||
display: flex;
|
||||
gap: var(--size-4-1);
|
||||
}
|
||||
|
||||
button.timer-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
transition: all 0.15s ease;
|
||||
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
button.timer-action-btn:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.timer-action-stop:hover {
|
||||
background: var(--background-modifier-error);
|
||||
color: var(--text-on-accent);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* Task Timer List */
|
||||
.timer-stats-task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-4-2);
|
||||
}
|
||||
|
||||
.timer-task-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--size-4-3);
|
||||
background: var(--background-secondary);
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
|
||||
.timer-task-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.timer-task-timer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-4-2);
|
||||
margin-left: var(--size-4-3);
|
||||
}
|
||||
|
||||
.timer-task-duration {
|
||||
font-family: var(--font-monospace);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.timer-task-status {
|
||||
font-size: var(--font-ui-smaller);
|
||||
}
|
||||
|
||||
.timer-task-status.timer-status-running {
|
||||
color: var(--timer-running-color);
|
||||
}
|
||||
|
||||
.timer-task-status.timer-status-paused {
|
||||
color: var(--timer-paused-color);
|
||||
}
|
||||
|
||||
/* Task Timer Meta Stopped State */
|
||||
.task-timer-stopped {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Completed Timers */
|
||||
.timer-completed-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-4-2);
|
||||
}
|
||||
|
||||
.timer-completed-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--size-4-3);
|
||||
background: var(--background-secondary);
|
||||
border-radius: var(--radius-m);
|
||||
gap: var(--size-4-3);
|
||||
}
|
||||
|
||||
.timer-completed-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.timer-completed-name {
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-normal);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.timer-completed-path {
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.timer-completed-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: var(--size-2-2);
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.timer-completed-duration {
|
||||
font-family: var(--font-monospace);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.timer-completed-time {
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
text-align: right;
|
||||
}
|
||||
|
|
@ -2590,6 +2590,46 @@ const translations = {
|
|||
"Columns that are currently hidden from view. Click the eye icon to show them again.":
|
||||
"Columns that are currently hidden from view. Click the eye icon to show them again.",
|
||||
"Unhide column": "Unhide column",
|
||||
|
||||
// Timer feature
|
||||
"Timer Statistics": "Timer Statistics",
|
||||
"Active Timers": "Active Timers",
|
||||
"Active timers": "Active timers",
|
||||
"No active timers": "No active timers",
|
||||
"Tasks in Current View": "Tasks in Current View",
|
||||
"No tasks with active timers in this view":
|
||||
"No tasks with active timers in this view",
|
||||
"Total Time": "Total Time",
|
||||
"Total Timers": "Total Timers",
|
||||
Paused: "Paused",
|
||||
"Completed Timers": "Completed Timers",
|
||||
"No completed timers": "No completed timers",
|
||||
"Completed at": "Completed at",
|
||||
Pause: "Pause",
|
||||
Resume: "Resume",
|
||||
Stop: "Stop",
|
||||
Untitled: "Untitled",
|
||||
"Start Timer": "Start Timer",
|
||||
"Pause Timer": "Pause Timer",
|
||||
"Resume Timer": "Resume Timer",
|
||||
"Stop Timer": "Stop Timer",
|
||||
"Timer started": "Timer started",
|
||||
"Timer paused": "Timer paused",
|
||||
"Timer resumed": "Timer resumed",
|
||||
"Timer stopped": "Timer stopped",
|
||||
"Failed to start timer": "Failed to start timer",
|
||||
"Block ID added": "Block ID added",
|
||||
"Task status updated": "Task status updated",
|
||||
|
||||
// Working On view
|
||||
"Working On": "Working On",
|
||||
|
||||
// Quick Capture Timer Integration
|
||||
"Timer Integration": "Timer Integration",
|
||||
"Auto-start timer": "Auto-start timer",
|
||||
"Automatically start the timer when creating a new task via quick capture (checkbox mode only)":
|
||||
"Automatically start the timer when creating a new task via quick capture (checkbox mode only)",
|
||||
"Timer started for new task": "Timer started for new task",
|
||||
};
|
||||
|
||||
export default translations;
|
||||
|
|
|
|||
|
|
@ -2381,6 +2381,45 @@ const translations = {
|
|||
"Columns that are currently hidden from view. Click the eye icon to show them again.":
|
||||
"当前隐藏的列。点击眼睛图标可重新显示。",
|
||||
"Unhide column": "显示列",
|
||||
|
||||
// Timer feature
|
||||
"Timer Statistics": "计时统计",
|
||||
"Active Timers": "活跃计时器",
|
||||
"Active timers": "活跃计时器",
|
||||
"No active timers": "暂无活跃计时器",
|
||||
"Tasks in Current View": "当前视图中的任务",
|
||||
"No tasks with active timers in this view": "此视图中没有正在计时的任务",
|
||||
"Total Time": "总计时",
|
||||
"Total Timers": "计时器总数",
|
||||
Paused: "已暂停",
|
||||
"Completed Timers": "已完成的计时",
|
||||
"No completed timers": "暂无已完成的计时",
|
||||
"Completed at": "完成于",
|
||||
Pause: "暂停",
|
||||
Resume: "继续",
|
||||
Stop: "停止",
|
||||
Untitled: "未命名",
|
||||
"Start Timer": "开始计时",
|
||||
"Pause Timer": "暂停计时",
|
||||
"Resume Timer": "继续计时",
|
||||
"Stop Timer": "停止计时",
|
||||
"Timer started": "计时已开始",
|
||||
"Timer paused": "计时已暂停",
|
||||
"Timer resumed": "计时已继续",
|
||||
"Timer stopped": "计时已停止",
|
||||
"Failed to start timer": "启动计时器失败",
|
||||
"Block ID added": "块 ID 已添加",
|
||||
"Task status updated": "任务状态已更新",
|
||||
|
||||
// Working On view
|
||||
"Working On": "进行中",
|
||||
|
||||
// Quick Capture Timer Integration
|
||||
"Timer Integration": "计时器集成",
|
||||
"Auto-start timer": "自动启动计时器",
|
||||
"Automatically start the timer when creating a new task via quick capture (checkbox mode only)":
|
||||
"通过快速捕获创建新任务时自动启动计时器(仅限复选框模式)",
|
||||
"Timer started for new task": "已为新任务启动计时器",
|
||||
};
|
||||
|
||||
export default translations;
|
||||
|
|
|
|||
279
styles.css
279
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue