refactor: clean up pomodoro view (#103)

- Remove quick action buttons and complex task selector for cleaner UI
- Add functional +/- 30-second time adjustment buttons
- Implement skip break functionality for better session flow
- Fix daily note data appending vs overwriting issue
- Update progress circle to use actual active time accounting for pauses
- Redesign with larger timer (300px), minimalist buttons, and simplified
layout
- Fix session completion flow to automatically prepare next session
timer
- Improve button styling with borderless design and subtle hover effects
This commit is contained in:
Callum Alpass 2025-07-06 13:28:53 +10:00
parent 83dfc36639
commit bfef3d09cc
3 changed files with 454 additions and 469 deletions

View file

@ -475,10 +475,7 @@ export class PomodoroService {
}
}
// Update daily note with pomodoro count for work sessions
if (session.type === 'work') {
await this.updateDailyNotePomodoroCount();
}
// Daily note update is now handled by addSessionToHistory method
// Determine next session based on session history
let shouldTakeLongBreak = false;
@ -518,79 +515,44 @@ export class PomodoroService {
this.playCompletionSound();
}
// Clear current session
// Clear current session and set up for next session
this.state.currentSession = undefined;
this.state.isRunning = false;
this.state.timeRemaining = 0;
// Set up appropriate timer for next session
if (session.type === 'work') {
// After work session, prepare break timer
const breakDuration = shouldTakeLongBreak
? this.plugin.settings.pomodoroLongBreakDuration
: this.plugin.settings.pomodoroShortBreakDuration;
this.state.timeRemaining = breakDuration * 60;
// Auto-start break if configured, otherwise just prepare the timer
if (this.plugin.settings.pomodoroAutoStartBreaks) {
const timeout = setTimeout(() => this.startBreak(shouldTakeLongBreak), 1000) as unknown as number;
this.cleanupTimeouts.add(timeout);
}
} else {
// After break session, prepare work timer
this.state.timeRemaining = this.plugin.settings.pomodoroWorkDuration * 60;
// Auto-start work if configured, otherwise just prepare the timer
if (this.plugin.settings.pomodoroAutoStartWork) {
const timeout = setTimeout(() => this.startPomodoro(), 1000) as unknown as number;
this.cleanupTimeouts.add(timeout);
}
}
await this.saveState();
// Auto-start next session if configured
if (session.type === 'work' && this.plugin.settings.pomodoroAutoStartBreaks) {
const timeout = setTimeout(() => this.startBreak(shouldTakeLongBreak), 1000) as unknown as number;
this.cleanupTimeouts.add(timeout);
} else if (session.type !== 'work' && this.plugin.settings.pomodoroAutoStartWork) {
const timeout = setTimeout(() => this.startPomodoro(), 1000) as unknown as number;
this.cleanupTimeouts.add(timeout);
}
// Emit tick event to update UI with new timer duration
this.plugin.emitter.trigger(EVENT_POMODORO_TICK, {
timeRemaining: this.state.timeRemaining,
session: this.state.currentSession
});
}
private async updateDailyNotePomodoroCount() {
try {
// Check if Daily Notes plugin is enabled
if (!appHasDailyNotesPluginLoaded()) {
console.warn('Daily Notes core plugin is not enabled, skipping pomodoro update');
return;
}
// Convert date to moment for the API
const moment = (window as any).moment(new Date());
// Get all daily notes to check if one exists for today
const allDailyNotes = getAllDailyNotes();
let file = getDailyNote(moment, allDailyNotes);
if (!file) {
// Daily note does not exist, create it using the core plugin
try {
file = await createDailyNote(moment);
// Created daily note with 1 pomodoro
} catch (createError: any) {
if (createError.message.includes('File already exists')) {
// File was created between our check and create attempt
// Daily note was created by another process, try to get it again
file = getDailyNote(moment, getAllDailyNotes());
if (file instanceof TFile) {
await this.plugin.app.fileManager.processFrontMatter(file, (frontmatter) => {
const pomodoroField = this.plugin.fieldMapper.toUserField('pomodoros');
if (!frontmatter[pomodoroField] || typeof frontmatter[pomodoroField] !== 'number') {
frontmatter[pomodoroField] = 0;
}
frontmatter[pomodoroField]++;
// Updated pomodoro count
});
}
} else {
throw createError;
}
}
} else {
// Daily note exists, updating pomodoro count
// Update existing daily note
await this.plugin.app.fileManager.processFrontMatter(file, (frontmatter) => {
const pomodoroField = this.plugin.fieldMapper.toUserField('pomodoros');
if (!frontmatter[pomodoroField]) {
frontmatter[pomodoroField] = 0;
}
frontmatter[pomodoroField]++;
// Updated pomodoro count
});
}
} catch (error) {
console.error('Failed to update daily note pomodoros:', error);
}
}
private playCompletionSound() {
try {
@ -645,6 +607,33 @@ export class PomodoroService {
return { ...this.state };
}
adjustSessionTime(newTimeInSeconds: number): void {
if (this.state.currentSession) {
this.state.timeRemaining = newTimeInSeconds;
this.state.currentSession.plannedDuration = Math.ceil(newTimeInSeconds / 60);
this.saveState();
// Emit tick event to update UI
this.plugin.emitter.trigger(EVENT_POMODORO_TICK, {
timeRemaining: this.state.timeRemaining,
session: this.state.currentSession
});
}
}
adjustPreparedTimer(newTimeInSeconds: number): void {
if (!this.state.currentSession) {
this.state.timeRemaining = newTimeInSeconds;
this.saveState();
// Emit tick event to update UI
this.plugin.emitter.trigger(EVENT_POMODORO_TICK, {
timeRemaining: this.state.timeRemaining,
session: this.state.currentSession
});
}
}
isRunning(): boolean {
return this.state.isRunning;
}
@ -768,9 +757,15 @@ export class PomodoroService {
};
try {
const history = await this.getSessionHistory();
history.push(historyEntry);
await this.saveSessionHistory(history);
if (this.plugin.settings.pomodoroStorageLocation === 'daily-notes') {
// For daily notes, add only this session to the appropriate daily note
await this.addSingleSessionToDailyNote(historyEntry);
} else {
// For plugin storage, add to the full history
const history = await this.getSessionHistory();
history.push(historyEntry);
await this.saveSessionHistory(history);
}
} catch (error) {
console.error('Failed to add session to history:', error);
}
@ -933,6 +928,40 @@ export class PomodoroService {
return grouped;
}
/**
* Add a single session to the appropriate daily note
*/
private async addSingleSessionToDailyNote(session: PomodoroSessionHistory): Promise<void> {
try {
const sessionDate = new Date(session.startTime);
const moment = (window as any).moment(sessionDate);
// Get or create daily note
const allDailyNotes = getAllDailyNotes();
let dailyNote = getDailyNote(moment, allDailyNotes);
if (!dailyNote) {
dailyNote = await createDailyNote(moment);
}
// Update frontmatter
const pomodoroField = this.plugin.fieldMapper.toUserField('pomodoros');
await this.plugin.app.fileManager.processFrontMatter(dailyNote, (frontmatter) => {
// Get existing sessions
const existingSessions = frontmatter[pomodoroField] || [];
const existingIds = new Set(existingSessions.map((s: any) => s.id));
// Only add session if it doesn't already exist
if (!existingIds.has(session.id)) {
frontmatter[pomodoroField] = [...existingSessions, session];
}
});
} catch (error) {
console.error(`Failed to add session to daily note:`, error);
}
}
/**
* Update a specific daily note with pomodoro sessions
*/
@ -953,7 +982,16 @@ export class PomodoroService {
const pomodoroField = this.plugin.fieldMapper.toUserField('pomodoros');
await this.plugin.app.fileManager.processFrontMatter(dailyNote, (frontmatter) => {
frontmatter[pomodoroField] = sessions;
// Get existing sessions and append new ones
const existingSessions = frontmatter[pomodoroField] || [];
const existingIds = new Set(existingSessions.map((s: any) => s.id));
// Only add sessions that don't already exist
const newSessions = sessions.filter(session => !existingIds.has(session.id));
if (newSessions.length > 0) {
frontmatter[pomodoroField] = [...existingSessions, ...newSessions];
}
});
} catch (error) {
console.error(`Failed to update daily note for ${dateStr}:`, error);

View file

@ -1,4 +1,4 @@
import { ItemView, WorkspaceLeaf, Notice, EventRef, Setting } from 'obsidian';
import { ItemView, WorkspaceLeaf, Notice, EventRef } from 'obsidian';
import TaskNotesPlugin from '../main';
import {
POMODORO_VIEW_TYPE,
@ -27,6 +27,9 @@ export class PomodoroView extends ItemView {
private statsDisplay: HTMLElement | null = null;
private taskSelectButton: HTMLButtonElement | null = null;
private currentSelectedTask: TaskInfo | null = null;
private addTimeButton: HTMLButtonElement | null = null;
private subtractTimeButton: HTMLButtonElement | null = null;
private skipBreakButton: HTMLButtonElement | null = null;
// Cache stat elements to avoid innerHTML
private statElements: {
@ -116,6 +119,8 @@ export class PomodoroView extends ItemView {
async render() {
const container = this.contentEl.createDiv({ cls: 'tasknotes-plugin pomodoro-view' });
// Status display at the top
this.statusDisplay = container.createDiv({ cls: 'pomodoro-view__status', text: 'Focus' });
// Timer display with progress circle
const timerSection = container.createDiv({ cls: 'pomodoro-view__timer-section' });
@ -126,34 +131,34 @@ export class PomodoroView extends ItemView {
// Create SVG progress circle
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('class', 'pomodoro-view__progress-svg');
svg.setAttribute('width', '240');
svg.setAttribute('height', '240');
svg.setAttribute('viewBox', '0 0 240 240');
svg.setAttribute('width', '300');
svg.setAttribute('height', '300');
svg.setAttribute('viewBox', '0 0 300 300');
this.progressContainer.appendChild(svg);
// Background circle
const bgCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
bgCircle.setAttributeNS(null, 'cx', '120');
bgCircle.setAttributeNS(null, 'cy', '120');
bgCircle.setAttributeNS(null, 'r', '110');
bgCircle.setAttributeNS(null, 'cx', '150');
bgCircle.setAttributeNS(null, 'cy', '150');
bgCircle.setAttributeNS(null, 'r', '140');
bgCircle.setAttributeNS(null, 'fill', 'none');
bgCircle.setAttributeNS(null, 'stroke', 'var(--tn-border-color)');
bgCircle.setAttributeNS(null, 'stroke-width', '4');
bgCircle.setAttributeNS(null, 'stroke-width', '2');
svg.appendChild(bgCircle);
// Progress circle
this.progressCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle') as SVGCircleElement;
this.progressCircle.setAttributeNS(null, 'cx', '120');
this.progressCircle.setAttributeNS(null, 'cy', '120');
this.progressCircle.setAttributeNS(null, 'r', '110');
this.progressCircle.setAttributeNS(null, 'cx', '150');
this.progressCircle.setAttributeNS(null, 'cy', '150');
this.progressCircle.setAttributeNS(null, 'r', '140');
this.progressCircle.setAttributeNS(null, 'fill', 'none');
this.progressCircle.setAttributeNS(null, 'stroke', 'var(--tn-interactive-accent)');
this.progressCircle.setAttributeNS(null, 'stroke-width', '6');
this.progressCircle.setAttributeNS(null, 'stroke-width', '4');
this.progressCircle.setAttributeNS(null, 'stroke-linecap', 'round');
// Calculate circumference: 2 * π * radius
const radius = 110;
const circumference = 2 * Math.PI * radius; // ≈ 691.15
const radius = 140;
const circumference = 2 * Math.PI * radius;
this.progressCircle.setAttributeNS(null, 'stroke-dasharray', circumference.toString());
this.progressCircle.setAttributeNS(null, 'stroke-dashoffset', circumference.toString());
@ -164,44 +169,37 @@ export class PomodoroView extends ItemView {
const timerOverlay = this.progressContainer.createDiv({ cls: 'pomodoro-view__timer-overlay' });
// Timer display
this.timerDisplay = timerOverlay.createDiv({ cls: 'pomodoro-view__timer-display', text: '25:00' });
const defaultDuration = this.plugin.settings.pomodoroWorkDuration;
const defaultTime = `${defaultDuration.toString().padStart(2, '0')}:00`;
this.timerDisplay = timerOverlay.createDiv({ cls: 'pomodoro-view__timer-display', text: defaultTime });
// Status display
this.statusDisplay = timerSection.createDiv({ cls: 'pomodoro-view__status', text: 'Ready to start' });
// Time adjustment controls
const timeControls = timerOverlay.createDiv({ cls: 'pomodoro-view__time-controls' });
// Task display
this.subtractTimeButton = timeControls.createEl('button', {
cls: 'pomodoro-view__time-adjust-button pomodoro-view__subtract-time',
text: '-'
});
// Don't hide initially since we want them always visible
this.addTimeButton = timeControls.createEl('button', {
cls: 'pomodoro-view__time-adjust-button pomodoro-view__add-time',
text: '+'
});
// Don't hide initially since we want them always visible
// Task display (minimal)
this.taskDisplay = container.createDiv({ cls: 'pomodoro-view__task-display' });
// Task selector
// Simplified task selector
const taskSelectorSection = container.createDiv({ cls: 'pomodoro-view__task-selector' });
taskSelectorSection.createEl('label', { cls: 'pomodoro-view__task-selector-label', text: 'Select task (optional):' });
const taskSelectorContainer = taskSelectorSection.createDiv({ cls: 'pomodoro-view__task-selector-container' });
this.taskSelectButton = taskSelectorContainer.createEl('button', {
this.taskSelectButton = taskSelectorSection.createEl('button', {
cls: 'pomodoro-view__task-select-button',
text: 'Choose task...'
});
// Add click handler for task selector button
this.registerDomEvent(this.taskSelectButton, 'click', async () => {
await this.openTaskSelector();
});
// Add clear button
const clearButton = taskSelectorContainer.createEl('button', {
cls: 'pomodoro-view__task-clear-button',
text: 'Clear'
});
this.registerDomEvent(clearButton, 'click', async () => {
await this.selectTask(null);
});
// Load and restore last selected task
this.restoreLastSelectedTask();
// Main control section
// Main control section - simplified
const controlSection = container.createDiv({ cls: 'pomodoro-view__control-section' });
// Primary controls (main timer controls)
@ -224,61 +222,25 @@ export class PomodoroView extends ItemView {
});
this.stopButton.addClass('pomodoro-view__stop-button--hidden');
// Quick start actions (grouped together)
const quickStartSection = controlSection.createDiv({ cls: 'pomodoro-view__quick-start-section' });
quickStartSection.createDiv({ cls: 'pomodoro-view__section-label', text: 'Quick start' });
const quickActions = quickStartSection.createDiv({ cls: 'pomodoro-view__quick-actions' });
const workButton = quickActions.createEl('button', {
text: 'Work session',
cls: 'pomodoro-view__quick-button pomodoro-view__work-button'
// Skip break button (only shown after sessions)
this.skipBreakButton = controlSection.createEl('button', {
cls: 'pomodoro-view__skip-break-button',
text: 'Skip break'
});
this.skipBreakButton.addClass('pomodoro-view__skip-break-button--hidden');
const shortBreakButton = quickActions.createEl('button', {
text: 'Short break',
cls: 'pomodoro-view__quick-button pomodoro-view__short-break-button'
});
const longBreakButton = quickActions.createEl('button', {
text: 'Long break',
cls: 'pomodoro-view__quick-button pomodoro-view__long-break-button'
});
// Statistics
// Minimal stats at the bottom
const statsSection = container.createDiv({ cls: 'pomodoro-view__stats-section' });
const statsHeader = statsSection.createDiv({ cls: 'pomodoro-view__stats-header' });
new Setting(statsHeader)
.setName('Today\'s progress')
.setHeading();
const viewStatsButton = statsHeader.createEl('button', {
cls: 'pomodoro-view__view-stats-button',
text: 'View all stats'
});
this.registerDomEvent(viewStatsButton, 'click', async () => {
await this.plugin.activatePomodoroStatsView();
});
this.statsDisplay = statsSection.createDiv({ cls: 'pomodoro-view__stats' });
// Create stat elements and cache references
// Create minimal stat elements
const pomodoroStat = this.statsDisplay.createDiv({ cls: 'pomodoro-view__stat' });
this.statElements.pomodoros = pomodoroStat.createSpan({ cls: 'pomodoro-view__stat-value', text: '0' });
pomodoroStat.createSpan({ cls: 'pomodoro-view__stat-label', text: 'Pomodoros completed' });
const streakStat = this.statsDisplay.createDiv({ cls: 'pomodoro-view__stat' });
this.statElements.streak = streakStat.createSpan({ cls: 'pomodoro-view__stat-value', text: '0' });
streakStat.createSpan({ cls: 'pomodoro-view__stat-label', text: 'Current streak' });
const minutesStat = this.statsDisplay.createDiv({ cls: 'pomodoro-view__stat' });
this.statElements.minutes = minutesStat.createSpan({ cls: 'pomodoro-view__stat-value', text: '0' });
minutesStat.createSpan({ cls: 'pomodoro-view__stat-label', text: 'Minutes focused' });
pomodoroStat.createSpan({ cls: 'pomodoro-view__stat-label', text: 'completed today' });
// Add event listeners
this.registerDomEvent(this.startButton, 'click', async () => {
// Prevent double clicks
if (this.startButton?.hasClass('is-loading')) return;
this.startButton?.addClass('pomodoro-view__start-button--loading');
@ -287,7 +249,17 @@ export class PomodoroView extends ItemView {
if (state.currentSession && !state.isRunning) {
await this.plugin.pomodoroService.resumePomodoro();
} else {
await this.plugin.pomodoroService.startPomodoro(this.currentSelectedTask || undefined);
// Determine what to start based on current timer value
const shortBreakDuration = this.plugin.settings.pomodoroShortBreakDuration * 60;
const longBreakDuration = this.plugin.settings.pomodoroLongBreakDuration * 60;
if (state.timeRemaining === shortBreakDuration) {
this.plugin.pomodoroService.startBreak(false); // Short break
} else if (state.timeRemaining === longBreakDuration) {
this.plugin.pomodoroService.startBreak(true); // Long break
} else {
await this.plugin.pomodoroService.startPomodoro(this.currentSelectedTask || undefined);
}
}
} finally {
this.startButton?.removeClass('pomodoro-view__start-button--loading');
@ -302,18 +274,32 @@ export class PomodoroView extends ItemView {
this.plugin.pomodoroService.stopPomodoro();
});
this.registerDomEvent(workButton, 'click', async () => {
await this.plugin.pomodoroService.startPomodoro(this.currentSelectedTask || undefined);
this.registerDomEvent(this.skipBreakButton, 'click', () => {
const state = this.plugin.pomodoroService.getState();
if (state.currentSession) {
// Currently in a break session, stop it
this.plugin.pomodoroService.stopPomodoro();
} else {
// Ready to start break but user wants to skip, go directly to work
this.plugin.pomodoroService.startPomodoro(this.currentSelectedTask || undefined);
}
});
this.registerDomEvent(shortBreakButton, 'click', () => {
this.plugin.pomodoroService.startBreak(false);
this.registerDomEvent(this.addTimeButton, 'click', () => {
this.adjustSessionTime(30);
});
this.registerDomEvent(longBreakButton, 'click', () => {
this.plugin.pomodoroService.startBreak(true);
this.registerDomEvent(this.subtractTimeButton, 'click', () => {
this.adjustSessionTime(-30);
});
this.registerDomEvent(this.taskSelectButton, 'click', async () => {
await this.openTaskSelector();
});
// Load and restore last selected task
this.restoreLastSelectedTask();
// Initial display update
this.updateDisplay();
this.updateStats().catch(error => {
@ -354,8 +340,8 @@ export class PomodoroView extends ItemView {
// Update button text
if (this.taskSelectButton) {
if (task) {
const displayText = task.title.length > 30
? task.title.substring(0, 27) + '...'
const displayText = task.title.length > 40
? task.title.substring(0, 37) + '...'
: task.title;
this.taskSelectButton.textContent = displayText;
this.taskSelectButton.title = task.title; // Full title in tooltip
@ -492,14 +478,53 @@ export class PomodoroView extends ItemView {
this.pauseButton.addClass('pomodoro-view__pause-button--hidden');
this.stopButton.removeClass('pomodoro-view__stop-button--hidden');
} else {
// Idle
// Idle - check if we're in post-completion state
this.startButton.removeClass('pomodoro-view__start-button--hidden');
this.startButton.textContent = 'Start';
// Determine what type of session to start next based on timer value
const workDuration = this.plugin.settings.pomodoroWorkDuration * 60;
const shortBreakDuration = this.plugin.settings.pomodoroShortBreakDuration * 60;
const longBreakDuration = this.plugin.settings.pomodoroLongBreakDuration * 60;
if (state.timeRemaining === shortBreakDuration || state.timeRemaining === longBreakDuration) {
// Timer is set to break duration, show start break
this.startButton.textContent = state.timeRemaining === longBreakDuration ? 'Start Long Break' : 'Start Short Break';
} else {
// Timer is set to work duration or other, show start work
this.startButton.textContent = state.timeRemaining === workDuration ? 'Start' : 'Start Work';
}
this.pauseButton.addClass('pomodoro-view__pause-button--hidden');
this.stopButton.addClass('pomodoro-view__stop-button--hidden');
}
}
// Update skip break button visibility
if (this.skipBreakButton) {
// Show skip break when:
// 1. Currently in a break session
// 2. Timer is ready to start a break (post work completion)
const workDuration = this.plugin.settings.pomodoroWorkDuration * 60;
const shortBreakDuration = this.plugin.settings.pomodoroShortBreakDuration * 60;
const longBreakDuration = this.plugin.settings.pomodoroLongBreakDuration * 60;
const isBreakTimerReady = !state.currentSession && (state.timeRemaining === shortBreakDuration || state.timeRemaining === longBreakDuration);
if ((state.currentSession && (state.currentSession.type === 'short-break' || state.currentSession.type === 'long-break')) || isBreakTimerReady) {
this.skipBreakButton.removeClass('pomodoro-view__skip-break-button--hidden');
if (isBreakTimerReady) {
this.skipBreakButton.textContent = 'Skip break';
}
} else {
this.skipBreakButton.addClass('pomodoro-view__skip-break-button--hidden');
}
}
// Update time adjustment button visibility - always show them
if (this.addTimeButton && this.subtractTimeButton) {
this.addTimeButton.removeClass('pomodoro-view__time-adjust-button--hidden');
this.subtractTimeButton.removeClass('pomodoro-view__time-adjust-button--hidden');
}
this.updateStats().catch(error => {
console.error('Failed to update stats:', error);
});
@ -523,28 +548,57 @@ export class PomodoroView extends ItemView {
}
private updateProgress(state: PomodoroState) {
if (!this.progressCircle || !state.currentSession) {
// No session active, reset progress
if (this.progressCircle) {
const radius = 110;
const circumference = 2 * Math.PI * radius;
this.progressCircle.setAttributeNS(null, 'stroke-dashoffset', circumference.toString());
// Legacy classes already removed during BEM cleanup
this.progressCircle.removeClass('pomodoro-view__progress-circle--work');
this.progressCircle.removeClass('pomodoro-view__progress-circle--short-break');
this.progressCircle.removeClass('pomodoro-view__progress-circle--long-break');
}
if (!this.progressCircle) return;
const radius = 140;
const circumference = 2 * Math.PI * radius;
if (!state.currentSession) {
// No session active - show full circle (ready to start)
this.progressCircle.setAttributeNS(null, 'stroke-dashoffset', circumference.toString());
this.progressCircle.removeClass('pomodoro-view__progress-circle--work');
this.progressCircle.removeClass('pomodoro-view__progress-circle--short-break');
this.progressCircle.removeClass('pomodoro-view__progress-circle--long-break');
this.progressCircle.removeClass('pomodoro-view__progress-circle--warning');
return;
}
// Calculate progress
const totalDuration = state.currentSession.plannedDuration * 60; // Convert to seconds
const elapsed = totalDuration - state.timeRemaining;
const progress = Math.max(0, Math.min(1, elapsed / totalDuration));
// Calculate progress based on actual active time (accounting for pauses)
const activePeriods = state.currentSession.activePeriods || [];
let totalActiveSeconds = 0;
// Calculate stroke-dashoffset
const radius = 110;
const circumference = 2 * Math.PI * radius;
// Sum up all completed active periods
for (const period of activePeriods) {
if (period.endTime) {
// Completed period
const start = new Date(period.startTime).getTime();
const end = new Date(period.endTime).getTime();
totalActiveSeconds += Math.floor((end - start) / 1000);
} else if (state.isRunning) {
// Current running period
const start = new Date(period.startTime).getTime();
const now = Date.now();
totalActiveSeconds += Math.floor((now - start) / 1000);
}
}
// Use current planned duration (which gets updated when user adjusts time)
const totalDuration = state.currentSession.plannedDuration * 60;
// Progress based on actual active time vs current planned duration
const progress = Math.max(0, Math.min(1, totalActiveSeconds / totalDuration));
// Debug logging
console.log('Circle progress:', {
totalDuration: totalDuration/60,
timeRemaining: state.timeRemaining/60,
actualActiveTime: totalActiveSeconds/60,
activePeriods: activePeriods.length,
isRunning: state.isRunning,
progress: Math.round(progress * 100) + '%'
});
// Calculate stroke-dashoffset (progress goes clockwise)
const offset = circumference - (progress * circumference);
// Update progress circle
@ -574,26 +628,37 @@ export class PomodoroView extends ItemView {
this.statElements.pomodoros.textContent = stats.pomodorosCompleted.toString();
}
if (this.statElements.streak && this.statElements.streak.textContent !== stats.currentStreak.toString()) {
this.statElements.streak.textContent = stats.currentStreak.toString();
}
if (this.statElements.minutes && this.statElements.minutes.textContent !== stats.totalMinutes.toString()) {
this.statElements.minutes.textContent = stats.totalMinutes.toString();
}
} catch (error) {
console.error('Failed to update stats:', error);
// Fallback to show zeros if stats loading fails
if (this.statElements.pomodoros) this.statElements.pomodoros.textContent = '0';
if (this.statElements.streak) this.statElements.streak.textContent = '0';
if (this.statElements.minutes) this.statElements.minutes.textContent = '0';
}
}
private adjustSessionTime(seconds: number) {
const state = this.plugin.pomodoroService.getState();
// Allow adjustment at any time - calculate new time
const newTime = Math.max(60, state.timeRemaining + seconds); // Minimum 1 minute
if (state.currentSession) {
// Session exists (running or paused), adjust the current session
this.plugin.pomodoroService.adjustSessionTime(newTime);
} else {
// No session (ready to start), adjust the prepared timer
this.plugin.pomodoroService.adjustPreparedTimer(newTime);
}
// Force an immediate update to ensure UI reflects changes
const updatedState = this.plugin.pomodoroService.getState();
this.updateTimer(updatedState.timeRemaining);
this.updateProgress(updatedState);
}
private onPomodoroComplete(session: PomodoroSession, nextType: string) {
this.updateDisplay();
// Show completion message
// Show completion message and skip break option
if (this.statusDisplay) {
if (session.type === 'work') {
const isLongBreak = nextType === 'long-break';

View file

@ -4,15 +4,17 @@
/* Pomodoro View Container - Root Block */
.tasknotes-plugin .pomodoro-view {
padding: var(--tn-spacing-md);
padding: var(--tn-spacing-xl);
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
gap: var(--tn-spacing-lg);
gap: var(--tn-spacing-xl);
max-width: 100%;
align-items: center;
justify-content: flex-start;
justify-content: center;
background: var(--tn-bg-primary);
min-height: 100vh;
}
/* ================================================
@ -38,9 +40,9 @@
display: flex;
align-items: center;
justify-content: center;
width: 240px;
height: 240px;
margin: 0 auto var(--tn-spacing-sm) auto;
width: 300px;
height: 300px;
margin: 0 auto;
}
/* Progress SVG */
@ -96,13 +98,14 @@
/* Timer Display */
.tasknotes-plugin .pomodoro-view__timer-display {
font-size: 2.5rem;
font-size: 3.5rem;
color: var(--tn-text-normal);
text-align: center;
font-family: var(--tn-font-mono);
letter-spacing: 0.05em;
transition: color var(--tn-transition-fast);
font-weight: var(--tn-font-weight-bold);
margin-bottom: var(--tn-spacing-md);
}
.tasknotes-plugin .pomodoro-view__timer-display--warning {
@ -117,12 +120,13 @@
/* Status Display */
.tasknotes-plugin .pomodoro-view__status {
font-size: var(--tn-font-size-lg);
font-size: var(--tn-font-size-xl);
font-weight: var(--tn-font-weight-medium);
text-align: center;
color: var(--tn-text-muted);
text-transform: capitalize;
margin-top: var(--tn-spacing-sm);
margin-bottom: var(--tn-spacing-lg);
order: -1;
}
.tasknotes-plugin .pomodoro-view__status--work {
@ -147,20 +151,20 @@
/* Task Display Container */
.tasknotes-plugin .pomodoro-view__task-display {
min-height: 60px;
min-width: 300px;
min-height: 40px;
min-width: 200px;
width: auto;
max-width: 500px;
max-width: 400px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
padding: var(--tn-spacing-md);
background: var(--tn-bg-secondary);
border-radius: var(--tn-radius-md);
border: 1px solid var(--tn-border-color);
padding: var(--tn-spacing-sm);
background: transparent;
border-radius: var(--tn-radius-sm);
border: none;
overflow: hidden;
max-height: 200px;
max-height: 100px;
opacity: 1;
transform: translateY(0);
transition: all var(--tn-transition-medium);
@ -194,18 +198,13 @@
text-align: center;
}
.tasknotes-plugin .pomodoro-view__task-label {
.tasknotes-plugin .pomodoro-view__task-title {
font-size: var(--tn-font-size-md);
color: var(--tn-text-muted);
font-weight: var(--tn-font-weight-medium);
}
.tasknotes-plugin .pomodoro-view__task-title {
font-size: var(--tn-font-size-lg);
color: var(--tn-text-normal);
font-weight: var(--tn-font-weight-semibold);
max-width: 300px;
max-width: 400px;
word-wrap: break-word;
text-align: center;
}
/* ================================================
@ -217,41 +216,31 @@
display: flex;
flex-direction: column;
align-items: center;
gap: var(--tn-spacing-md);
max-width: 500px;
gap: var(--tn-spacing-sm);
max-width: 400px;
width: 100%;
margin: 0 auto;
padding: var(--tn-spacing-md) 0;
}
.tasknotes-plugin .pomodoro-view__task-selector-label {
font-size: var(--tn-font-size-md);
font-weight: var(--tn-font-weight-medium);
color: var(--tn-text-normal);
}
.tasknotes-plugin .pomodoro-view__task-selector-container {
display: flex;
gap: var(--tn-spacing-sm);
align-items: center;
padding: 0;
}
/* Task Select Button */
.tasknotes-plugin .pomodoro-view__task-select-button {
display: flex;
align-items: center;
flex: 1;
text-align: left;
padding: var(--tn-spacing-sm) var(--tn-spacing-md);
justify-content: center;
text-align: center;
padding: var(--tn-spacing-sm) var(--tn-spacing-lg);
border: 1px solid var(--tn-border-color);
border-radius: var(--tn-radius-sm);
background: var(--tn-bg-primary);
border-radius: var(--tn-radius-lg);
background: var(--tn-bg-secondary);
color: var(--tn-text-normal);
cursor: pointer;
font-size: var(--tn-font-size-lg);
font-size: var(--tn-font-size-sm);
font-weight: var(--tn-font-weight-normal);
transition: all var(--tn-transition-fast);
min-height: var(--tn-button-height-md);
min-height: var(--tn-button-height-sm);
width: 100%;
max-width: 300px;
}
.tasknotes-plugin .pomodoro-view__task-select-button:hover {
@ -264,24 +253,6 @@
font-style: italic;
}
/* Task Clear Button */
.tasknotes-plugin .pomodoro-view__task-clear-button {
padding: var(--tn-spacing-sm) var(--tn-spacing-md);
border: 1px solid var(--tn-border-color);
border-radius: var(--tn-radius-sm);
background: var(--tn-bg-secondary);
color: var(--tn-text-normal);
cursor: pointer;
font-size: var(--tn-font-size-md);
font-weight: var(--tn-font-weight-medium);
transition: all var(--tn-transition-fast);
min-height: var(--tn-button-height-md);
}
.tasknotes-plugin .pomodoro-view__task-clear-button:hover {
background: var(--tn-interactive-hover);
border-color: var(--tn-border-color-hover);
}
/* ================================================
CONTROL SECTION - Main Controls Container
@ -293,13 +264,12 @@
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--tn-spacing-lg);
padding: var(--tn-spacing-lg) var(--tn-spacing-md);
border-top: 1px solid var(--tn-border-color);
border-bottom: 1px solid var(--tn-border-color);
margin: var(--tn-spacing-md) auto;
gap: var(--tn-spacing-md);
padding: var(--tn-spacing-md);
border: none;
margin: 0 auto;
width: 100%;
max-width: 500px;
max-width: 400px;
}
/* Primary Controls Container */
@ -316,12 +286,12 @@
/* Primary Control Buttons */
.tasknotes-plugin .pomodoro-view__start-button {
min-width: 120px;
padding: var(--tn-spacing-md) var(--tn-spacing-lg);
min-width: 200px;
padding: var(--tn-spacing-lg) var(--tn-spacing-xl);
background: var(--tn-interactive-accent);
color: var(--tn-bg-primary);
border: 1px solid var(--tn-interactive-accent);
border-radius: var(--tn-radius-md);
border: 2px solid var(--tn-interactive-accent);
border-radius: var(--tn-radius-lg);
font-size: var(--tn-font-size-lg);
font-weight: var(--tn-font-weight-semibold);
cursor: pointer;
@ -336,12 +306,12 @@
}
.tasknotes-plugin .pomodoro-view__pause-button {
min-width: 120px;
padding: var(--tn-spacing-md) var(--tn-spacing-lg);
background: var(--tn-color-warning);
color: var(--tn-bg-primary);
border: 1px solid var(--tn-color-warning);
border-radius: var(--tn-radius-md);
min-width: 200px;
padding: var(--tn-spacing-lg) var(--tn-spacing-xl);
background: transparent;
color: var(--tn-interactive-accent);
border: 2px solid var(--tn-interactive-accent);
border-radius: var(--tn-radius-lg);
font-size: var(--tn-font-size-lg);
font-weight: var(--tn-font-weight-semibold);
cursor: pointer;
@ -350,33 +320,96 @@
}
.tasknotes-plugin .pomodoro-view__pause-button:hover {
background: var(--tn-color-warning);
opacity: 0.9;
background: var(--tn-interactive-accent);
color: var(--tn-bg-primary);
transform: translateY(-1px);
box-shadow: var(--tn-shadow-medium);
}
.tasknotes-plugin .pomodoro-view__stop-button {
min-width: 120px;
padding: var(--tn-spacing-md) var(--tn-spacing-lg);
background: var(--tn-color-error);
color: var(--tn-bg-primary);
border: 1px solid var(--tn-color-error);
padding: var(--tn-spacing-sm) var(--tn-spacing-md);
background: transparent;
color: var(--tn-text-muted);
border: 1px solid var(--tn-border-color);
border-radius: var(--tn-radius-md);
font-size: var(--tn-font-size-lg);
font-weight: var(--tn-font-weight-semibold);
font-size: var(--tn-font-size-md);
font-weight: var(--tn-font-weight-medium);
cursor: pointer;
transition: all var(--tn-transition-fast);
min-height: var(--tn-button-height-lg);
min-height: var(--tn-button-height-md);
}
.tasknotes-plugin .pomodoro-view__stop-button:hover {
background: var(--tn-color-error);
opacity: 0.9;
color: var(--tn-bg-primary);
border-color: var(--tn-color-error);
transform: translateY(-1px);
box-shadow: var(--tn-shadow-medium);
}
/* Skip Break Button */
.tasknotes-plugin .pomodoro-view__skip-break-button {
min-width: 120px;
padding: var(--tn-spacing-sm) var(--tn-spacing-md);
background: transparent;
color: var(--tn-text-muted);
border: 1px solid var(--tn-border-color);
border-radius: var(--tn-radius-md);
font-size: var(--tn-font-size-md);
font-weight: var(--tn-font-weight-medium);
cursor: pointer;
transition: all var(--tn-transition-fast);
min-height: var(--tn-button-height-md);
}
.tasknotes-plugin .pomodoro-view__skip-break-button:hover {
background: var(--tn-interactive-hover);
border-color: var(--tn-interactive-accent);
color: var(--tn-interactive-accent);
}
/* Time Adjustment Controls */
.tasknotes-plugin .pomodoro-view__time-controls {
display: flex;
gap: var(--tn-spacing-md);
align-items: center;
justify-content: center;
margin-top: var(--tn-spacing-sm);
pointer-events: auto; /* Re-enable pointer events for the buttons */
}
.tasknotes-plugin .pomodoro-view__time-adjust-button {
width: 28px;
height: 28px;
border-radius: 50%;
background: transparent;
color: var(--tn-text-muted);
border: none;
cursor: pointer;
font-size: var(--tn-font-size-md);
font-weight: var(--tn-font-weight-normal);
transition: all var(--tn-transition-fast);
display: flex;
align-items: center;
justify-content: center;
opacity: 0.6;
}
.tasknotes-plugin .pomodoro-view__time-adjust-button:hover {
background: var(--tn-bg-secondary);
color: var(--tn-interactive-accent);
opacity: 1;
transform: scale(1.1);
}
.tasknotes-plugin .pomodoro-view__time-adjust-button:focus {
outline: none;
background: var(--tn-bg-secondary);
color: var(--tn-interactive-accent);
opacity: 1;
}
/* Button States */
.tasknotes-plugin .pomodoro-view__start-button--loading {
opacity: 0.7;
@ -385,109 +418,12 @@
.tasknotes-plugin .pomodoro-view__start-button--hidden,
.tasknotes-plugin .pomodoro-view__pause-button--hidden,
.tasknotes-plugin .pomodoro-view__stop-button--hidden {
.tasknotes-plugin .pomodoro-view__stop-button--hidden,
.tasknotes-plugin .pomodoro-view__skip-break-button--hidden,
.tasknotes-plugin .pomodoro-view__time-adjust-button--hidden {
display: none;
}
/* ================================================
QUICK START SECTION
================================================ */
/* Quick Start Container */
.tasknotes-plugin .pomodoro-view__quick-start-section {
display: flex;
flex-direction: column;
gap: var(--tn-spacing-sm);
align-items: center;
justify-content: center;
width: 100%;
}
.tasknotes-plugin .pomodoro-view__section-label {
font-size: var(--tn-font-size-md);
font-weight: var(--tn-font-weight-semibold);
color: var(--tn-text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
text-align: center;
margin-bottom: var(--tn-spacing-xs);
}
/* Quick Actions Container */
.tasknotes-plugin .pomodoro-view__quick-actions {
display: flex;
justify-content: center;
align-items: center;
gap: var(--tn-spacing-sm);
flex-wrap: wrap;
max-width: 400px;
margin: 0 auto;
width: 100%;
}
/* Quick Action Buttons */
.tasknotes-plugin .pomodoro-view__quick-button {
flex: 1;
min-width: 120px;
padding: var(--tn-spacing-sm) var(--tn-spacing-md);
border: 1px solid var(--tn-border-color);
border-radius: var(--tn-radius-md);
background: var(--tn-bg-secondary);
color: var(--tn-text-normal);
font-size: var(--tn-font-size-md);
font-weight: var(--tn-font-weight-medium);
cursor: pointer;
transition: all var(--tn-transition-fast);
min-height: var(--tn-button-height-md);
}
.tasknotes-plugin .pomodoro-view__quick-button:hover {
background: var(--tn-interactive-hover);
border-color: var(--tn-interactive-accent);
transform: translateY(-1px);
}
.tasknotes-plugin .pomodoro-view__work-button {
background: var(--tn-status-in-progress-color, var(--tn-interactive-accent));
color: var(--tn-bg-primary);
border-color: var(--tn-status-in-progress-color, var(--tn-interactive-accent));
}
.tasknotes-plugin .pomodoro-view__work-button:hover {
background: var(--tn-status-in-progress-color, var(--tn-interactive-accent));
color: var(--tn-bg-primary);
opacity: 0.9;
transform: translateY(-1px);
box-shadow: var(--tn-shadow-medium);
}
.tasknotes-plugin .pomodoro-view__short-break-button {
background: var(--tn-priority-medium-color, #10B981);
color: var(--tn-bg-primary);
border-color: var(--tn-priority-medium-color, #10B981);
}
.tasknotes-plugin .pomodoro-view__short-break-button:hover {
background: var(--tn-priority-medium-color, #10B981);
color: var(--tn-bg-primary);
opacity: 0.9;
transform: translateY(-1px);
box-shadow: var(--tn-shadow-medium);
}
.tasknotes-plugin .pomodoro-view__long-break-button {
background: var(--tn-priority-high-color, #3B82F6);
color: var(--tn-bg-primary);
border-color: var(--tn-priority-high-color, #3B82F6);
}
.tasknotes-plugin .pomodoro-view__long-break-button:hover {
background: var(--tn-priority-high-color, #3B82F6);
color: var(--tn-bg-primary);
opacity: 0.9;
transform: translateY(-1px);
box-shadow: var(--tn-shadow-medium);
}
/* ================================================
STATISTICS SECTION
@ -495,77 +431,45 @@
/* Statistics Section Container */
.tasknotes-plugin .pomodoro-view__stats-section {
border-top: 1px solid var(--tn-border-color);
padding: var(--tn-spacing-xl) var(--tn-spacing-md) 0;
border: none;
padding: var(--tn-spacing-md) 0;
width: 100%;
max-width: 600px;
max-width: 300px;
margin: 0 auto;
box-sizing: border-box;
}
/* Statistics Header */
.tasknotes-plugin .pomodoro-view__stats-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--tn-spacing-md);
}
/* Statistics Title */
.tasknotes-plugin .pomodoro-view__stats-title {
font-size: var(--tn-font-size-lg);
font-weight: var(--tn-font-weight-semibold);
color: var(--tn-text-normal);
margin: 0;
}
.tasknotes-plugin .pomodoro-view__view-stats-button {
padding: var(--tn-spacing-xs) var(--tn-spacing-sm);
border: 1px solid var(--tn-border-color);
border-radius: var(--tn-radius-sm);
background: var(--tn-bg-primary);
color: var(--tn-text-normal);
cursor: pointer;
font-size: var(--tn-font-size-sm);
font-weight: var(--tn-font-weight-medium);
transition: all var(--tn-transition-fast);
}
.tasknotes-plugin .pomodoro-view__view-stats-button:hover {
background: var(--tn-interactive-hover);
border-color: var(--tn-interactive-accent);
}
/* Statistics Grid */
.tasknotes-plugin .pomodoro-view__stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: var(--tn-spacing-md);
display: flex;
justify-content: center;
align-items: center;
gap: var(--tn-spacing-sm);
}
/* Individual Stat */
.tasknotes-plugin .pomodoro-view__stat {
display: flex;
flex-direction: column;
align-items: center;
padding: var(--tn-spacing-md);
background: var(--tn-bg-secondary);
border-radius: var(--tn-radius-md);
border: 1px solid var(--tn-border-color);
gap: var(--tn-spacing-xs);
padding: 0;
background: transparent;
border: none;
text-align: center;
}
.tasknotes-plugin .pomodoro-view__stat-value {
font-size: var(--tn-font-size-2xl);
font-size: var(--tn-font-size-lg);
font-weight: var(--tn-font-weight-bold);
color: var(--tn-interactive-accent);
margin-bottom: var(--tn-spacing-xs);
margin: 0;
}
.tasknotes-plugin .pomodoro-view__stat-label {
font-size: var(--tn-font-size-md);
font-size: var(--tn-font-size-sm);
color: var(--tn-text-muted);
font-weight: var(--tn-font-weight-medium);
margin: 0;
}
/* ================================================
@ -579,12 +483,12 @@
}
.tasknotes-plugin .pomodoro-view__progress-container {
width: 200px;
height: 200px;
width: 250px;
height: 250px;
}
.tasknotes-plugin .pomodoro-view__timer-display {
font-size: 2rem;
font-size: 2.5rem;
}
.tasknotes-plugin .pomodoro-view__control-section {
@ -606,41 +510,20 @@
max-width: 250px;
}
.tasknotes-plugin .pomodoro-view__quick-actions {
flex-direction: column;
align-items: center;
gap: var(--tn-spacing-sm);
max-width: 250px;
}
.tasknotes-plugin .pomodoro-view__quick-button {
width: 100%;
min-width: auto;
}
.tasknotes-plugin .pomodoro-view__stats {
grid-template-columns: 1fr;
}
.tasknotes-plugin .pomodoro-view__task-selector-container {
flex-direction: column;
align-items: stretch;
}
.tasknotes-plugin .pomodoro-view__task-clear-button {
align-self: center;
max-width: 100px;
gap: var(--tn-spacing-xs);
}
}
@media (max-width: 480px) {
.tasknotes-plugin .pomodoro-view__progress-container {
width: 180px;
height: 180px;
width: 200px;
height: 200px;
}
.tasknotes-plugin .pomodoro-view__timer-display {
font-size: 1.8rem;
font-size: 2rem;
}
}
@ -665,10 +548,9 @@
.tasknotes-plugin .pomodoro-view__start-button:focus,
.tasknotes-plugin .pomodoro-view__pause-button:focus,
.tasknotes-plugin .pomodoro-view__stop-button:focus,
.tasknotes-plugin .pomodoro-view__quick-button:focus,
.tasknotes-plugin .pomodoro-view__task-select-button:focus,
.tasknotes-plugin .pomodoro-view__task-clear-button:focus,
.tasknotes-plugin .pomodoro-view__view-stats-button:focus {
.tasknotes-plugin .pomodoro-view__skip-break-button:focus,
.tasknotes-plugin .pomodoro-view__time-adjust-button:focus,
.tasknotes-plugin .pomodoro-view__task-select-button:focus {
outline: 2px solid var(--tn-interactive-accent);
outline-offset: 2px;
}