mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
feat: Add Pomodoro stats view and enhance recurring task UI
• Introduce a new PomodoroStatsView to display detailed session statistics. - Created a new file (src/views/PomodoroStatsView.ts) with UI components that render today’s progress, weekly summaries, overall statistics, and a list of recent work sessions. - Integrated refresh functionality and data calculations (e.g., current streak, average session length, completion rate) using data from the pomodoroService. • Update the main plugin registration (src/main.ts): - Import the new PomodoroStatsView. - Register the POMODORO_STATS_VIEW_TYPE. - Add a command (‘open-pomodoro-stats’) to allow users to open the Pomodoro statistics view. - Add an activatePomodoroStatsView method to switch to the new view. - Ensure that when workspace leaves are detached, leaves of type POMODORO_STATS_VIEW_TYPE are also removed. • Update task filtering and status management: - In FilterService.ts, update the “No Due Date” string to “No due date” for consistency. - In StatusManager.ts, add a new getNonCompletionStatuses method to retrieve status configurations excluding completed statuses—this is used to manage recurring tasks. • Enhance recurring task UI and behavior in TaskCard component (src/ui/TaskCard.ts): - Modify task cards to detect recurring tasks and conditionally append a “recurring-task” CSS class. - Insert a recurring indicator icon in the task card for recurring tasks. - Update context menu logic to show only non-completion statuses (via getNonCompletionStatuses) for recurring tasks. - Add a menu item that toggles the completion state for recurring tasks for the current date. - Update the task card DOM structure (both initial creation and update) to add or remove the recurring indicator as needed. - Update the import statements to include new helper functions (shouldUseRecurringTaskUI and getRecurringTaskCompletionText). • Extend helper functions (src/utils/helpers.ts): - Introduce shouldShowRecurringTaskOnDate to determine if a recurring task is due on a target date. - Add getRecurringTaskCompletionText to generate completion state text for a recurring task on a specific date. - Add shouldUseRecurringTaskUI to decide if recurring task UI behavior should be applied. • Update PomodoroView (src/views/PomodoroView.ts): - Add a “View All Stats” button to the stats section that opens the PomodoroStatsView when clicked. • Define new view type (src/types.ts): - Add constant POMODORO_STATS_VIEW_TYPE for consistent usage when registering and activating the stats view. • Introduce new CSS styles (styles.css): - Add styling for recurring tasks (e.g., a colored left border and gradient background for cards marked as recurring). - Define styles for the recurring indicator element. - Implement comprehensive styles for the Pomodoro Stats view, including layout grids, header styling, cards for different statistics (pomodoros, streak, minutes, average session, completion rate), and styling for recent sessions. This commit provides substantial enhancements by adding a focused Pomodoro statistics interface and by improving the handling and visual feedback for recurring tasks throughout the application.
This commit is contained in:
parent
41b9ac8808
commit
deb1afa896
9 changed files with 579 additions and 6 deletions
19
src/main.ts
19
src/main.ts
|
|
@ -12,6 +12,7 @@ import {
|
|||
TASK_LIST_VIEW_TYPE,
|
||||
AGENDA_VIEW_TYPE,
|
||||
POMODORO_VIEW_TYPE,
|
||||
POMODORO_STATS_VIEW_TYPE,
|
||||
KANBAN_VIEW_TYPE,
|
||||
TimeInfo,
|
||||
TaskInfo,
|
||||
|
|
@ -26,6 +27,7 @@ import { TaskListView } from './views/TaskListView';
|
|||
import { NotesView } from './views/NotesView';
|
||||
import { AgendaView } from './views/AgendaView';
|
||||
import { PomodoroView } from './views/PomodoroView';
|
||||
import { PomodoroStatsView } from './views/PomodoroStatsView';
|
||||
import { KanbanView } from './views/KanbanView';
|
||||
import { TaskCreationModal } from './modals/TaskCreationModal';
|
||||
import { TaskEditModal } from './modals/TaskEditModal';
|
||||
|
|
@ -173,6 +175,10 @@ export default class TaskNotesPlugin extends Plugin {
|
|||
POMODORO_VIEW_TYPE,
|
||||
(leaf) => new PomodoroView(leaf, this)
|
||||
);
|
||||
this.registerView(
|
||||
POMODORO_STATS_VIEW_TYPE,
|
||||
(leaf) => new PomodoroStatsView(leaf, this)
|
||||
);
|
||||
this.registerView(
|
||||
KANBAN_VIEW_TYPE,
|
||||
(leaf) => new KanbanView(leaf, this)
|
||||
|
|
@ -430,6 +436,14 @@ export default class TaskNotesPlugin extends Plugin {
|
|||
}
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'open-pomodoro-stats',
|
||||
name: 'Open pomodoro statistics',
|
||||
callback: async () => {
|
||||
await this.activatePomodoroStatsView();
|
||||
}
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'open-kanban-popout',
|
||||
name: 'Open kanban board in new window',
|
||||
|
|
@ -535,6 +549,10 @@ export default class TaskNotesPlugin extends Plugin {
|
|||
return this.activateView(POMODORO_VIEW_TYPE);
|
||||
}
|
||||
|
||||
async activatePomodoroStatsView() {
|
||||
return this.activateView(POMODORO_STATS_VIEW_TYPE);
|
||||
}
|
||||
|
||||
async activateKanbanView() {
|
||||
return this.activateView(KANBAN_VIEW_TYPE);
|
||||
}
|
||||
|
|
@ -578,6 +596,7 @@ export default class TaskNotesPlugin extends Plugin {
|
|||
workspace.detachLeavesOfType(NOTES_VIEW_TYPE);
|
||||
workspace.detachLeavesOfType(AGENDA_VIEW_TYPE);
|
||||
workspace.detachLeavesOfType(POMODORO_VIEW_TYPE);
|
||||
workspace.detachLeavesOfType(POMODORO_STATS_VIEW_TYPE);
|
||||
workspace.detachLeavesOfType(KANBAN_VIEW_TYPE);
|
||||
|
||||
// Create a calendar view
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ export class FilterService extends EventEmitter {
|
|||
* Get due date group for task (Today, Tomorrow, This Week, etc.)
|
||||
*/
|
||||
private getDueDateGroup(dueDate?: string): string {
|
||||
if (!dueDate) return 'No Due Date';
|
||||
if (!dueDate) return 'No due date';
|
||||
|
||||
const due = new Date(dueDate);
|
||||
const today = new Date();
|
||||
|
|
|
|||
|
|
@ -92,6 +92,13 @@ export class StatusManager {
|
|||
return [...this.statuses];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get non-completion status configurations (for recurring tasks)
|
||||
*/
|
||||
getNonCompletionStatuses(): StatusConfig[] {
|
||||
return this.statuses.filter(s => !s.isCompleted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status configurations
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export const TASK_LIST_VIEW_TYPE = 'tasknotes-task-list-view';
|
|||
export const NOTES_VIEW_TYPE = 'tasknotes-notes-view';
|
||||
export const AGENDA_VIEW_TYPE = 'tasknotes-agenda-view';
|
||||
export const POMODORO_VIEW_TYPE = 'tasknotes-pomodoro-view';
|
||||
export const POMODORO_STATS_VIEW_TYPE = 'tasknotes-pomodoro-stats-view';
|
||||
export const KANBAN_VIEW_TYPE = 'tasknotes-kanban-view';
|
||||
|
||||
// Event types
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { format } from 'date-fns';
|
|||
import { TFile, Menu } from 'obsidian';
|
||||
import { TaskInfo } from '../types';
|
||||
import TaskNotesPlugin from '../main';
|
||||
import { calculateTotalTimeSpent, isRecurringTaskDueOn, getEffectiveTaskStatus } from '../utils/helpers';
|
||||
import { calculateTotalTimeSpent, isRecurringTaskDueOn, getEffectiveTaskStatus, shouldUseRecurringTaskUI, getRecurringTaskCompletionText } from '../utils/helpers';
|
||||
|
||||
export interface TaskCardOptions {
|
||||
showDueDate: boolean;
|
||||
|
|
@ -39,7 +39,8 @@ export function createTaskCard(task: TaskInfo, plugin: TaskNotesPlugin, options:
|
|||
const card = document.createElement('div');
|
||||
const isActivelyTracked = plugin.getActiveTimeSession(task) !== null;
|
||||
const isCompleted = plugin.statusManager.isCompletedStatus(effectiveStatus);
|
||||
card.className = `tasknotes-card tasknotes-card--normal tasknotes-card--flex task-card ${effectiveStatus} ${task.archived ? 'archived' : ''} ${isActivelyTracked ? 'actively-tracked' : ''} ${isCompleted ? 'task-completed' : ''}`;
|
||||
const isRecurring = !!task.recurrence;
|
||||
card.className = `tasknotes-card tasknotes-card--normal tasknotes-card--flex task-card ${effectiveStatus} ${task.archived ? 'archived' : ''} ${isActivelyTracked ? 'actively-tracked' : ''} ${isCompleted ? 'task-completed' : ''} ${isRecurring ? 'recurring-task' : ''}`;
|
||||
card.dataset.taskPath = task.path;
|
||||
|
||||
// Apply priority as left border color
|
||||
|
|
@ -79,6 +80,14 @@ export function createTaskCard(task: TaskInfo, plugin: TaskNotesPlugin, options:
|
|||
statusDot.style.backgroundColor = statusConfig.color;
|
||||
}
|
||||
|
||||
// Recurring task indicator
|
||||
if (task.recurrence) {
|
||||
const recurringIndicator = card.createEl('span', {
|
||||
cls: 'recurring-indicator',
|
||||
attr: { 'aria-label': `Recurring: ${task.recurrence.frequency}` }
|
||||
});
|
||||
}
|
||||
|
||||
// Main content container
|
||||
const contentContainer = card.createEl('div', { cls: 'task-content' });
|
||||
|
||||
|
|
@ -202,8 +211,14 @@ async function showTaskContextMenu(event: MouseEvent, taskPath: string, plugin:
|
|||
|
||||
const menu = new Menu();
|
||||
|
||||
// For recurring tasks, only show non-completion statuses
|
||||
// For regular tasks, show all statuses
|
||||
const availableStatuses = task.recurrence
|
||||
? plugin.statusManager.getNonCompletionStatuses()
|
||||
: plugin.statusManager.getAllStatuses();
|
||||
|
||||
// Direct status options (no submenu to avoid issues)
|
||||
plugin.statusManager.getAllStatuses().forEach(statusConfig => {
|
||||
availableStatuses.forEach(statusConfig => {
|
||||
menu.addItem((item) => {
|
||||
const isSelected = task.status === statusConfig.value;
|
||||
item.setTitle(`${isSelected ? '✓ ' : ''}${statusConfig.label}`);
|
||||
|
|
@ -223,6 +238,27 @@ async function showTaskContextMenu(event: MouseEvent, taskPath: string, plugin:
|
|||
});
|
||||
});
|
||||
|
||||
// Add completion toggle for recurring tasks
|
||||
if (task.recurrence) {
|
||||
menu.addSeparator();
|
||||
|
||||
// Check current completion status for this date
|
||||
const dateStr = format(targetDate, 'yyyy-MM-dd');
|
||||
const isCompletedForDate = task.complete_instances?.includes(dateStr) || false;
|
||||
|
||||
menu.addItem((item) => {
|
||||
item.setTitle(isCompletedForDate ? 'Mark incomplete for this date' : 'Mark complete for this date');
|
||||
item.setIcon(isCompletedForDate ? 'x' : 'check');
|
||||
item.onClick(async () => {
|
||||
try {
|
||||
await plugin.toggleRecurringTaskComplete(task, targetDate);
|
||||
} catch (error) {
|
||||
console.error('Error toggling recurring task completion:', error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
// Direct priority options (no submenu to avoid issues)
|
||||
|
|
@ -336,7 +372,8 @@ export function updateTaskCard(element: HTMLElement, task: TaskInfo, plugin: Tas
|
|||
// Update main element classes
|
||||
const isActivelyTracked = plugin.getActiveTimeSession(task) !== null;
|
||||
const isCompleted = plugin.statusManager.isCompletedStatus(effectiveStatus);
|
||||
element.className = `tasknotes-card tasknotes-card--normal tasknotes-card--flex task-card ${effectiveStatus} ${task.archived ? 'archived' : ''} ${isActivelyTracked ? 'actively-tracked' : ''} ${isCompleted ? 'task-completed' : ''}`;
|
||||
const isRecurring = !!task.recurrence;
|
||||
element.className = `tasknotes-card tasknotes-card--normal tasknotes-card--flex task-card ${effectiveStatus} ${task.archived ? 'archived' : ''} ${isActivelyTracked ? 'actively-tracked' : ''} ${isCompleted ? 'task-completed' : ''} ${isRecurring ? 'recurring-task' : ''}`;
|
||||
|
||||
// Update priority border color
|
||||
const priorityConfig = plugin.priorityManager.getPriorityConfig(task.priority);
|
||||
|
|
@ -360,6 +397,23 @@ export function updateTaskCard(element: HTMLElement, task: TaskInfo, plugin: Tas
|
|||
}
|
||||
}
|
||||
|
||||
// Update recurring indicator
|
||||
const existingRecurringIndicator = element.querySelector('.recurring-indicator');
|
||||
if (task.recurrence && !existingRecurringIndicator) {
|
||||
// Add recurring indicator if task is now recurring but didn't have one
|
||||
const recurringIndicator = element.createEl('span', {
|
||||
cls: 'recurring-indicator',
|
||||
attr: { 'aria-label': `Recurring: ${task.recurrence.frequency}` }
|
||||
});
|
||||
statusDot.insertAdjacentElement('afterend', recurringIndicator);
|
||||
} else if (!task.recurrence && existingRecurringIndicator) {
|
||||
// Remove recurring indicator if task is no longer recurring
|
||||
existingRecurringIndicator.remove();
|
||||
} else if (task.recurrence && existingRecurringIndicator) {
|
||||
// Update existing recurring indicator
|
||||
existingRecurringIndicator.setAttribute('aria-label', `Recurring: ${task.recurrence.frequency}`);
|
||||
}
|
||||
|
||||
// Update title
|
||||
const titleEl = element.querySelector('.task-title') as HTMLElement;
|
||||
if (titleEl) {
|
||||
|
|
|
|||
|
|
@ -537,6 +537,34 @@ export function getEffectiveTaskStatus(task: any, date: Date): string {
|
|||
return completedDates.includes(dateStr) ? 'done' : 'open';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a recurring task should be due on the current target date
|
||||
*/
|
||||
export function shouldShowRecurringTaskOnDate(task: TaskInfo, targetDate: Date): boolean {
|
||||
if (!task.recurrence) return true; // Non-recurring tasks are always shown
|
||||
|
||||
return isRecurringTaskDueOn(task, targetDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the completion state text for a recurring task on a specific date
|
||||
*/
|
||||
export function getRecurringTaskCompletionText(task: TaskInfo, targetDate: Date): string {
|
||||
if (!task.recurrence) return '';
|
||||
|
||||
const dateStr = format(targetDate, 'yyyy-MM-dd');
|
||||
const isCompleted = task.complete_instances?.includes(dateStr) || false;
|
||||
|
||||
return isCompleted ? 'Completed for this date' : 'Not completed for this date';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a task should use recurring task UI behavior
|
||||
*/
|
||||
export function shouldUseRecurringTaskUI(task: TaskInfo): boolean {
|
||||
return !!task.recurrence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts note information from a note file's content
|
||||
*/
|
||||
|
|
|
|||
250
src/views/PomodoroStatsView.ts
Normal file
250
src/views/PomodoroStatsView.ts
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
import { ItemView, WorkspaceLeaf, Setting } from 'obsidian';
|
||||
import { format, subDays, startOfWeek, endOfWeek } from 'date-fns';
|
||||
import TaskNotesPlugin from '../main';
|
||||
import {
|
||||
POMODORO_STATS_VIEW_TYPE,
|
||||
PomodoroHistoryStats,
|
||||
PomodoroSessionHistory
|
||||
} from '../types';
|
||||
|
||||
export class PomodoroStatsView extends ItemView {
|
||||
plugin: TaskNotesPlugin;
|
||||
|
||||
// UI elements
|
||||
private todayStatsEl: HTMLElement | null = null;
|
||||
private weekStatsEl: HTMLElement | null = null;
|
||||
private recentSessionsEl: HTMLElement | null = null;
|
||||
private overallStatsEl: HTMLElement | null = null;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: TaskNotesPlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return POMODORO_STATS_VIEW_TYPE;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return 'Pomodoro Stats';
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return 'bar-chart';
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
await this.plugin.onReady();
|
||||
await this.render();
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
async render() {
|
||||
const container = this.contentEl.createDiv({ cls: 'tasknotes-container pomodoro-stats-container' });
|
||||
|
||||
// Header
|
||||
const header = container.createDiv({ cls: 'pomodoro-stats-header' });
|
||||
new Setting(header)
|
||||
.setName('Pomodoro Statistics')
|
||||
.setHeading();
|
||||
|
||||
// Refresh button
|
||||
const refreshButton = header.createEl('button', {
|
||||
cls: 'pomodoro-stats-refresh-button',
|
||||
text: 'Refresh'
|
||||
});
|
||||
this.registerDomEvent(refreshButton, 'click', () => {
|
||||
this.refreshStats();
|
||||
});
|
||||
|
||||
// Today's stats
|
||||
const todaySection = container.createDiv({ cls: 'pomodoro-stats-section' });
|
||||
new Setting(todaySection)
|
||||
.setName('Today')
|
||||
.setHeading();
|
||||
this.todayStatsEl = todaySection.createDiv({ cls: 'pomodoro-stats-grid' });
|
||||
|
||||
// This week's stats
|
||||
const weekSection = container.createDiv({ cls: 'pomodoro-stats-section' });
|
||||
new Setting(weekSection)
|
||||
.setName('This Week')
|
||||
.setHeading();
|
||||
this.weekStatsEl = weekSection.createDiv({ cls: 'pomodoro-stats-grid' });
|
||||
|
||||
// Overall stats
|
||||
const overallSection = container.createDiv({ cls: 'pomodoro-stats-section' });
|
||||
new Setting(overallSection)
|
||||
.setName('All Time')
|
||||
.setHeading();
|
||||
this.overallStatsEl = overallSection.createDiv({ cls: 'pomodoro-stats-grid' });
|
||||
|
||||
// Recent sessions
|
||||
const recentSection = container.createDiv({ cls: 'pomodoro-stats-section' });
|
||||
new Setting(recentSection)
|
||||
.setName('Recent Sessions')
|
||||
.setHeading();
|
||||
this.recentSessionsEl = recentSection.createDiv({ cls: 'pomodoro-recent-sessions' });
|
||||
|
||||
// Initial load
|
||||
await this.refreshStats();
|
||||
}
|
||||
|
||||
private async refreshStats() {
|
||||
try {
|
||||
await Promise.all([
|
||||
this.updateTodayStats(),
|
||||
this.updateWeekStats(),
|
||||
this.updateOverallStats(),
|
||||
this.updateRecentSessions()
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateTodayStats() {
|
||||
if (!this.todayStatsEl) return;
|
||||
|
||||
const stats = await this.plugin.pomodoroService.getTodayStats();
|
||||
this.renderStatsGrid(this.todayStatsEl, stats);
|
||||
}
|
||||
|
||||
private async updateWeekStats() {
|
||||
if (!this.weekStatsEl) return;
|
||||
|
||||
const today = new Date();
|
||||
const weekStart = startOfWeek(today, { weekStartsOn: 1 }); // Monday
|
||||
const weekEnd = endOfWeek(today, { weekStartsOn: 1 });
|
||||
|
||||
const stats = await this.calculateStatsForRange(weekStart, weekEnd);
|
||||
this.renderStatsGrid(this.weekStatsEl, stats);
|
||||
}
|
||||
|
||||
private async updateOverallStats() {
|
||||
if (!this.overallStatsEl) return;
|
||||
|
||||
const history = await this.plugin.pomodoroService.getSessionHistory();
|
||||
const stats = this.calculateOverallStats(history);
|
||||
this.renderStatsGrid(this.overallStatsEl, stats);
|
||||
}
|
||||
|
||||
private async updateRecentSessions() {
|
||||
if (!this.recentSessionsEl) return;
|
||||
|
||||
const history = await this.plugin.pomodoroService.getSessionHistory();
|
||||
const recentSessions = history
|
||||
.filter(session => session.type === 'work')
|
||||
.slice(-10)
|
||||
.reverse();
|
||||
|
||||
this.recentSessionsEl.empty();
|
||||
|
||||
if (recentSessions.length === 0) {
|
||||
this.recentSessionsEl.createDiv({
|
||||
cls: 'pomodoro-no-sessions',
|
||||
text: 'No sessions recorded yet'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (const session of recentSessions) {
|
||||
const sessionEl = this.recentSessionsEl.createDiv({ cls: 'pomodoro-session-item' });
|
||||
|
||||
const dateEl = sessionEl.createSpan({ cls: 'session-date' });
|
||||
dateEl.textContent = format(new Date(session.startTime), 'MMM d, HH:mm');
|
||||
|
||||
const durationEl = sessionEl.createSpan({ cls: 'session-duration' });
|
||||
durationEl.textContent = `${session.duration}min`;
|
||||
|
||||
const statusEl = sessionEl.createSpan({ cls: 'session-status' });
|
||||
statusEl.textContent = session.completed ? 'Completed' : 'Interrupted';
|
||||
statusEl.addClass(session.completed ? 'status-completed' : 'status-interrupted');
|
||||
|
||||
if (session.taskPath) {
|
||||
const taskEl = sessionEl.createSpan({ cls: 'session-task' });
|
||||
const taskName = session.taskPath.split('/').pop()?.replace('.md', '') || '';
|
||||
taskEl.textContent = taskName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private renderStatsGrid(container: HTMLElement, stats: PomodoroHistoryStats) {
|
||||
container.empty();
|
||||
|
||||
// Completed pomodoros
|
||||
const pomodorosCard = container.createDiv({ cls: 'pomodoro-stat-card' });
|
||||
pomodorosCard.createDiv({ cls: 'stat-value', text: stats.pomodorosCompleted.toString() });
|
||||
pomodorosCard.createDiv({ cls: 'stat-label', text: 'Pomodoros' });
|
||||
|
||||
// Current streak
|
||||
const streakCard = container.createDiv({ cls: 'pomodoro-stat-card' });
|
||||
streakCard.createDiv({ cls: 'stat-value', text: stats.currentStreak.toString() });
|
||||
streakCard.createDiv({ cls: 'stat-label', text: 'Streak' });
|
||||
|
||||
// Total minutes
|
||||
const minutesCard = container.createDiv({ cls: 'pomodoro-stat-card' });
|
||||
minutesCard.createDiv({ cls: 'stat-value', text: stats.totalMinutes.toString() });
|
||||
minutesCard.createDiv({ cls: 'stat-label', text: 'Minutes' });
|
||||
|
||||
// Average session length
|
||||
const avgCard = container.createDiv({ cls: 'pomodoro-stat-card' });
|
||||
avgCard.createDiv({ cls: 'stat-value', text: stats.averageSessionLength.toString() });
|
||||
avgCard.createDiv({ cls: 'stat-label', text: 'Avg Length' });
|
||||
|
||||
// Completion rate
|
||||
const rateCard = container.createDiv({ cls: 'pomodoro-stat-card' });
|
||||
rateCard.createDiv({ cls: 'stat-value', text: `${stats.completionRate}%` });
|
||||
rateCard.createDiv({ cls: 'stat-label', text: 'Completion' });
|
||||
}
|
||||
|
||||
private async calculateStatsForRange(startDate: Date, endDate: Date): Promise<PomodoroHistoryStats> {
|
||||
const history = await this.plugin.pomodoroService.getSessionHistory();
|
||||
|
||||
// Filter sessions within date range
|
||||
const rangeSessions = history.filter(session => {
|
||||
const sessionDate = new Date(session.startTime);
|
||||
return sessionDate >= startDate && sessionDate <= endDate;
|
||||
});
|
||||
|
||||
return this.calculateStatsFromSessions(rangeSessions);
|
||||
}
|
||||
|
||||
private calculateOverallStats(history: PomodoroSessionHistory[]): PomodoroHistoryStats {
|
||||
return this.calculateStatsFromSessions(history);
|
||||
}
|
||||
|
||||
private calculateStatsFromSessions(sessions: PomodoroSessionHistory[]): PomodoroHistoryStats {
|
||||
// Filter work sessions only
|
||||
const workSessions = sessions.filter(session => session.type === 'work');
|
||||
const completedWork = workSessions.filter(session => session.completed);
|
||||
|
||||
// Calculate streak from most recent sessions
|
||||
let currentStreak = 0;
|
||||
for (let i = workSessions.length - 1; i >= 0; i--) {
|
||||
if (workSessions[i].completed) {
|
||||
currentStreak++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const totalMinutes = completedWork.reduce((sum, session) => sum + session.duration, 0);
|
||||
const averageSessionLength = completedWork.length > 0
|
||||
? totalMinutes / completedWork.length
|
||||
: 0;
|
||||
const completionRate = workSessions.length > 0
|
||||
? (completedWork.length / workSessions.length) * 100
|
||||
: 0;
|
||||
|
||||
return {
|
||||
pomodorosCompleted: completedWork.length,
|
||||
currentStreak,
|
||||
totalMinutes,
|
||||
averageSessionLength: Math.round(averageSessionLength),
|
||||
completionRate: Math.round(completionRate)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -241,9 +241,20 @@ export class PomodoroView extends ItemView {
|
|||
|
||||
// Statistics
|
||||
const statsSection = container.createDiv({ cls: 'pomodoro-stats-section' });
|
||||
new Setting(statsSection)
|
||||
const statsHeader = statsSection.createDiv({ cls: 'pomodoro-stats-header' });
|
||||
new Setting(statsHeader)
|
||||
.setName('Today\'s progress')
|
||||
.setHeading();
|
||||
|
||||
const viewStatsButton = statsHeader.createEl('button', {
|
||||
cls: 'pomodoro-view-stats-button',
|
||||
text: 'View All Stats'
|
||||
});
|
||||
|
||||
this.registerDomEvent(viewStatsButton, 'click', async () => {
|
||||
await this.plugin.activatePomodoroStatsView();
|
||||
});
|
||||
|
||||
this.statsDisplay = statsSection.createDiv({ cls: 'pomodoro-stats' });
|
||||
|
||||
// Create stat elements and cache references
|
||||
|
|
|
|||
203
styles.css
203
styles.css
|
|
@ -4299,6 +4299,40 @@
|
|||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Recurring task styles */
|
||||
.task-card.recurring-task {
|
||||
border-left: 3px solid var(--priority-color, var(--interactive-accent));
|
||||
background: linear-gradient(135deg, var(--background-primary) 95%, var(--interactive-accent-hover) 100%);
|
||||
}
|
||||
|
||||
.task-card.recurring-task:hover {
|
||||
background: linear-gradient(135deg, var(--background-modifier-hover) 95%, var(--interactive-accent-hover) 100%);
|
||||
}
|
||||
|
||||
/* Recurring indicator */
|
||||
.task-card .recurring-indicator {
|
||||
flex-shrink: 0;
|
||||
width: var(--cs-indicator-md);
|
||||
height: var(--cs-indicator-md);
|
||||
border-radius: 50%;
|
||||
background: var(--interactive-accent);
|
||||
border: 2px solid var(--background-primary);
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.task-card .recurring-indicator::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
background: var(--background-primary);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Update animation */
|
||||
.task-card.task-updated {
|
||||
background: var(--interactive-accent);
|
||||
|
|
@ -5889,3 +5923,172 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Pomodoro Stats View Styles */
|
||||
.pomodoro-stats-container {
|
||||
padding: var(--cs-spacing-lg);
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.pomodoro-stats-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--cs-spacing-xl);
|
||||
}
|
||||
|
||||
.pomodoro-stats-refresh-button {
|
||||
padding: var(--cs-spacing-xs) var(--cs-spacing-md);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--cs-radius-md);
|
||||
background: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
font-size: var(--cs-text-sm);
|
||||
cursor: pointer;
|
||||
transition: all var(--cs-transition-fast);
|
||||
}
|
||||
|
||||
.pomodoro-stats-refresh-button:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.pomodoro-stats-section {
|
||||
margin-bottom: var(--cs-spacing-xl);
|
||||
}
|
||||
|
||||
.pomodoro-stats-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--cs-spacing-md);
|
||||
}
|
||||
|
||||
.pomodoro-view-stats-button {
|
||||
padding: var(--cs-spacing-xs) var(--cs-spacing-sm);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--cs-radius-md);
|
||||
background: var(--background-primary);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--cs-text-xs);
|
||||
cursor: pointer;
|
||||
transition: all var(--cs-transition-fast);
|
||||
}
|
||||
|
||||
.pomodoro-view-stats-button:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.pomodoro-stats-section h3 {
|
||||
font-size: var(--cs-text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
margin: 0 0 var(--cs-spacing-md) 0;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
padding-bottom: var(--cs-spacing-xs);
|
||||
}
|
||||
|
||||
.pomodoro-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: var(--cs-spacing-md);
|
||||
margin-bottom: var(--cs-spacing-lg);
|
||||
}
|
||||
|
||||
.pomodoro-stat-card {
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--cs-radius-md);
|
||||
padding: var(--cs-spacing-md);
|
||||
text-align: center;
|
||||
transition: all var(--cs-transition-fast);
|
||||
}
|
||||
|
||||
.pomodoro-stat-card:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.pomodoro-stat-card .stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-accent);
|
||||
line-height: 1;
|
||||
margin-bottom: var(--cs-spacing-xs);
|
||||
}
|
||||
|
||||
.pomodoro-stat-card .stat-label {
|
||||
font-size: var(--cs-text-sm);
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.pomodoro-recent-sessions {
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--cs-radius-md);
|
||||
padding: var(--cs-spacing-md);
|
||||
}
|
||||
|
||||
.pomodoro-no-sessions {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
padding: var(--cs-spacing-lg);
|
||||
}
|
||||
|
||||
.pomodoro-session-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--cs-spacing-md);
|
||||
padding: var(--cs-spacing-sm) 0;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.pomodoro-session-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.session-date {
|
||||
font-size: var(--cs-text-sm);
|
||||
color: var(--text-muted);
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.session-duration {
|
||||
font-size: var(--cs-text-sm);
|
||||
font-weight: 500;
|
||||
color: var(--text-normal);
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.session-status {
|
||||
font-size: var(--cs-text-xs);
|
||||
padding: 2px var(--cs-spacing-xs);
|
||||
border-radius: var(--cs-radius-sm);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: rgba(46, 204, 113, 0.1);
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
.status-interrupted {
|
||||
background: rgba(231, 76, 60, 0.1);
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.session-task {
|
||||
font-size: var(--cs-text-sm);
|
||||
color: var(--text-normal);
|
||||
flex-grow: 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue