import { App, Notice, Plugin, PluginSettingTab, Setting, TFile, WorkspaceLeaf, } from 'obsidian'; import { FocusTask, FocusTaskSettings, FocusTaskData, DEFAULT_SETTINGS, DEFAULT_DATA, VIEW_TYPE_FOCUS_TASK, CELEBRATION_MESSAGES, EARLY_FINISH_MESSAGES, OVERTIME_MESSAGES, } from './types'; import { FocusTaskView } from './view'; import { QuickAddTaskModal } from './modals'; // ============ Main Plugin Class ============ export default class FocusTaskPlugin extends Plugin { settings: FocusTaskSettings; data: FocusTaskData; // Timer state timerInterval: number | null = null; currentTimerSeconds: number = 0; isTimerRunning: boolean = false; isBreakMode: boolean = false; activeTaskId: string | null = null; pomodoroCount: number = 0; // Timestamp-based tracking for reliable background timing private timerStartTimestamp: number = 0; private pausedTimeRemaining: number = 0; // Focus time tracking (in seconds for accuracy) private focusSecondsToday: number = 0; private secondsWorkedOnCurrentTask: number = 0; // Status bar element statusBarEl: HTMLElement | null = null; async onload() { await this.loadAllData(); // Check and reset daily stats this.checkDailyReset(); // Handle visibility changes to sync timer when app comes back to foreground document.addEventListener('visibilitychange', () => { if (!document.hidden && this.isTimerRunning) { this.syncTimerFromTimestamp(); } }); // Register the main view this.registerView( VIEW_TYPE_FOCUS_TASK, (leaf) => new FocusTaskView(leaf, this) ); // Add ribbon icon this.addRibbonIcon('zap', 'Open Focus Task', () => { this.activateView(); }); // Add commands this.addCommand({ id: 'open-focus-task', name: 'Open Focus Task Panel', callback: () => this.activateView(), }); this.addCommand({ id: 'quick-add-task', name: 'Quick Add Task', callback: () => new QuickAddTaskModal(this.app, this).open(), }); this.addCommand({ id: 'start-focus-mode', name: 'Start Focus Mode on Next Task', callback: () => this.startFocusOnNextTask(), }); this.addCommand({ id: 'toggle-timer', name: 'Toggle Timer (Play/Pause)', callback: () => this.toggleTimer(), }); this.addCommand({ id: 'complete-current-task', name: 'Complete Current Task', callback: () => this.completeActiveTask(), }); // Add settings tab this.addSettingTab(new FocusTaskSettingTab(this.app, this)); // Create status bar timer this.createStatusBar(); } onunload() { this.stopTimer(); } async loadAllData() { const loaded = await this.loadData(); // Merge loaded data with defaults (defaults first, then override with loaded) // This ensures new fields get default values even for existing installs this.data = Object.assign({}, DEFAULT_DATA, loaded?.data || {}); this.settings = Object.assign({}, DEFAULT_SETTINGS, loaded?.settings || {}); // Ensure lists array exists and has at least the default lists if (!this.settings.lists || this.settings.lists.length === 0) { this.settings.lists = DEFAULT_SETTINGS.lists; } // Initialize seconds from stored minutes this.focusSecondsToday = (this.data.totalFocusMinutesToday || 0) * 60; } async saveAllData() { // Sync minutes from seconds before saving this.data.totalFocusMinutesToday = Math.floor(this.focusSecondsToday / 60); await this.saveData({ settings: this.settings, data: this.data, }); } checkDailyReset() { const today = new Date().toDateString(); if (this.data.lastActiveDate !== today) { // Check streak const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); if (this.data.lastActiveDate === yesterday.toDateString()) { this.data.streak++; } else if (this.data.lastActiveDate !== today) { this.data.streak = 0; } // Reset daily stats this.data.completedToday = 0; this.data.totalFocusMinutesToday = 0; this.focusSecondsToday = 0; this.data.lastActiveDate = today; this.saveAllData(); } } async activateView() { const { workspace } = this.app; let leaf: WorkspaceLeaf | null = null; const leaves = workspace.getLeavesOfType(VIEW_TYPE_FOCUS_TASK); if (leaves.length > 0) { leaf = leaves[0]; } else { leaf = workspace.getRightLeaf(false); if (leaf) { await leaf.setViewState({ type: VIEW_TYPE_FOCUS_TASK, active: true }); } } if (leaf) { workspace.revealLeaf(leaf); } } // ============ Task Management ============ createTask(text: string, estimatedMinutes: number = this.settings.defaultEstimateMinutes, list: string = 'work'): FocusTask { return { id: this.generateId(), text, completed: false, estimatedMinutes, actualMinutes: 0, createdAt: Date.now(), list, notes: '', isActive: false, }; } generateId(): string { return Date.now().toString(36) + Math.random().toString(36).substr(2); } addTask(task: FocusTask) { this.data.tasks.push(task); this.saveAllData(); this.refreshView(); } updateTask(taskId: string, updates: Partial) { const task = this.data.tasks.find(t => t.id === taskId); if (task) { Object.assign(task, updates); this.saveAllData(); this.refreshView(); } } deleteTask(taskId: string) { this.data.tasks = this.data.tasks.filter(t => t.id !== taskId); if (this.activeTaskId === taskId) { this.stopTimer(); this.activeTaskId = null; } this.saveAllData(); this.refreshView(); } completeTask(taskId: string) { const task = this.data.tasks.find(t => t.id === taskId); if (task && !task.completed) { task.completed = true; task.completedAt = Date.now(); task.isActive = false; this.data.completedToday++; this.data.lastActiveDate = new Date().toDateString(); // Show celebration if (this.settings.enableCelebrations) { this.showCelebration(task); } // Play sound if (this.settings.enableSounds) { this.playCompletionSound(); } // Log to daily note if (this.settings.logToDaily) { this.logTaskToDailyNote(task); } if (this.activeTaskId === taskId) { this.stopTimer(); this.activeTaskId = null; } this.saveAllData(); this.refreshView(); } } completeActiveTask() { if (this.activeTaskId) { this.completeTask(this.activeTaskId); } else { new Notice('No active task to complete'); } } // ============ Timer Management ============ // Sync timer based on timestamp when app returns from background syncTimerFromTimestamp() { // Since all intervals now calculate from timestamps directly, // we just need to trigger an update when coming back to foreground if (!this.isTimerRunning) return; // The interval will automatically calculate correct values on next tick // Just update the display immediately to show current state this.updateStatusBar(); this.updateTimerDisplay(); } startTimer(taskId: string) { const task = this.data.tasks.find(t => t.id === taskId); if (!task) return; // Stop any existing timer this.stopTimer(); // Set active task this.activeTaskId = taskId; task.isActive = true; this.isBreakMode = false; this.currentTimerSeconds = 0; this.isTimerRunning = true; this.secondsWorkedOnCurrentTask = task.actualMinutes * 60; // Set timestamp for background tracking (stopwatch mode) this.timerStartTimestamp = Date.now(); this.pausedTimeRemaining = 0; // 0 indicates stopwatch mode // Store initial values const initialSecondsWorked = this.secondsWorkedOnCurrentTask; let alertShown = false; // Full refresh to show the active task card this.refreshView(); this.updateStatusBar(); // Start interval (count up mode - stopwatch) this.timerInterval = window.setInterval(() => { // Calculate elapsed time from timestamp const now = Date.now(); const elapsedMs = now - this.timerStartTimestamp; const elapsedSeconds = Math.floor(elapsedMs / 1000); // Update timer (count up) this.currentTimerSeconds = elapsedSeconds; // Update actual time worked this.secondsWorkedOnCurrentTask = initialSecondsWorked + elapsedSeconds; task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60); // Update focus time const newFocusSeconds = Math.floor((this.data.totalFocusMinutesToday || 0) * 60) + elapsedSeconds; this.focusSecondsToday = newFocusSeconds; // Light update - only timer display, no full refresh this.updateStatusBar(); this.updateTimerDisplay(); // Check if over estimate (only alert once) if (!alertShown && this.currentTimerSeconds >= task.estimatedMinutes * 60) { alertShown = true; if (this.settings.enableSounds) { this.playAlertSound(); } new Notice(`⏰ Time's up for: ${task.text}`); } }, 1000); this.saveAllData(); } startPomodoro(taskId: string) { const task = this.data.tasks.find(t => t.id === taskId); if (!task) return; this.stopTimer(); this.activeTaskId = taskId; task.isActive = true; this.isBreakMode = false; this.currentTimerSeconds = this.settings.pomodoroWorkMinutes * 60; this.isTimerRunning = true; // Initialize from existing actual time to preserve progress across breaks // Store as seconds for precision this.secondsWorkedOnCurrentTask = Math.floor(task.actualMinutes * 60); // Set timestamp for background tracking (pomodoro countdown mode) this.timerStartTimestamp = Date.now(); this.pausedTimeRemaining = this.currentTimerSeconds; // Store the initial seconds worked to calculate delta const initialSecondsWorked = this.secondsWorkedOnCurrentTask; // Full refresh to show the active task card this.refreshView(); this.updateStatusBar(); this.timerInterval = window.setInterval(() => { // Calculate elapsed time from timestamp (more accurate than counting ticks) const now = Date.now(); const elapsedMs = now - this.timerStartTimestamp; const elapsedSeconds = Math.floor(elapsedMs / 1000); // Update countdown timer this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds); if (!this.isBreakMode) { // Update actual time worked based on real elapsed time this.secondsWorkedOnCurrentTask = initialSecondsWorked + elapsedSeconds; const actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60); if (task.actualMinutes !== actualMinutes) { task.actualMinutes = actualMinutes; } // Update focus time based on elapsed seconds const newFocusSeconds = Math.floor((this.data.totalFocusMinutesToday || 0) * 60) + elapsedSeconds; this.focusSecondsToday = newFocusSeconds; } // Light update - only timer display, no full refresh this.updateStatusBar(); this.updateTimerDisplay(); if (this.currentTimerSeconds <= 0) { this.handlePomodoroEnd(); } }, 1000); } handlePomodoroEnd() { // Stop the timer interval to prevent going into negative if (this.timerInterval) { window.clearInterval(this.timerInterval); this.timerInterval = null; } // Set timer to 0 to ensure it doesn't show negative this.currentTimerSeconds = 0; this.isTimerRunning = false; // Update displays immediately this.updateStatusBar(); this.updateTimerDisplay(); if (!this.isBreakMode) { // Work session ended this.pomodoroCount++; this.data.pomodorosCompleted++; if (this.settings.enableSounds) { this.playAlertSound(); } new Notice('🍅 Pomodoro complete! Time for a break.'); if (this.settings.autoStartBreak) { this.startBreak(); } else { this.refreshView(); } } else { // Break ended - keep timer at 0 until user resumes if (this.settings.enableSounds) { this.playAlertSound(); } new Notice('⚡ Break over! Ready to focus?'); // Keep the break card visible with timer at 0:00 this.refreshView(); } this.saveAllData(); } startBreak() { this.isBreakMode = true; this.isTimerRunning = true; const isLongBreak = this.pomodoroCount % this.settings.longBreakInterval === 0; this.currentTimerSeconds = (isLongBreak ? this.settings.longBreakMinutes : this.settings.pomodoroBreakMinutes) * 60; // Set timestamp for background tracking (break countdown mode) this.timerStartTimestamp = Date.now(); this.pausedTimeRemaining = this.currentTimerSeconds; new Notice(isLongBreak ? '☕ Long break time!' : '☕ Short break time!'); // Full refresh to show break state this.refreshView(); if (!this.timerInterval) { this.timerInterval = window.setInterval(() => { // Calculate elapsed time from timestamp const now = Date.now(); const elapsedMs = now - this.timerStartTimestamp; const elapsedSeconds = Math.floor(elapsedMs / 1000); // Update countdown timer this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds); // Light update - only timer display this.updateStatusBar(); this.updateTimerDisplay(); if (this.currentTimerSeconds <= 0) { this.handlePomodoroEnd(); } }, 1000); } this.updateStatusBar(); } toggleTimer() { if (this.isTimerRunning && this.timerInterval) { // Pause - save current state window.clearInterval(this.timerInterval); this.timerInterval = null; this.isTimerRunning = false; this.pausedTimeRemaining = this.currentTimerSeconds; } else if (this.activeTaskId) { // Resume - restart with new timestamp this.isTimerRunning = true; this.timerStartTimestamp = Date.now(); const task = this.data.tasks.find(t => t.id === this.activeTaskId); // Store initial values for resume const initialSecondsWorked = this.secondsWorkedOnCurrentTask; this.timerInterval = window.setInterval(() => { // Calculate elapsed time from timestamp const now = Date.now(); const elapsedMs = now - this.timerStartTimestamp; const elapsedSeconds = Math.floor(elapsedMs / 1000); // Update timer (countdown from paused position) this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds); if (task && !this.isBreakMode) { // Update actual time worked this.secondsWorkedOnCurrentTask = initialSecondsWorked + elapsedSeconds; task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60); // Update focus time const newFocusSeconds = Math.floor((this.data.totalFocusMinutesToday || 0) * 60) + elapsedSeconds; this.focusSecondsToday = newFocusSeconds; } // Light update - only timer display this.updateStatusBar(); this.updateTimerDisplay(); if (this.currentTimerSeconds <= 0) { this.handlePomodoroEnd(); } }, 1000); } else { new Notice('No active task. Select a task first.'); } // Full refresh to update pause/resume button state this.updateStatusBar(); this.refreshView(); } stopTimer() { if (this.timerInterval) { window.clearInterval(this.timerInterval); this.timerInterval = null; } if (this.activeTaskId) { const task = this.data.tasks.find(t => t.id === this.activeTaskId); if (task) { task.isActive = false; // Reset actual time when manually stopping (not after a break) // This allows starting fresh next time task.actualMinutes = 0; } } this.isTimerRunning = false; this.activeTaskId = null; this.secondsWorkedOnCurrentTask = 0; this.timerStartTimestamp = 0; this.pausedTimeRemaining = 0; this.updateStatusBar(); this.saveAllData(); this.refreshView(); } startFocusOnNextTask() { const pendingTasks = this.data.tasks.filter(t => !t.completed); if (pendingTasks.length > 0) { this.startPomodoro(pendingTasks[0].id); } else { new Notice('No pending tasks. Add a task first!'); } } // ============ Status Bar Timer ============ createStatusBar() { this.statusBarEl = this.addStatusBarItem(); this.statusBarEl.addClass('focus-task-status-bar'); this.updateStatusBar(); // Click to open panel this.statusBarEl.addEventListener('click', () => { this.activateView(); }); } updateStatusBar() { if (!this.statusBarEl) return; if (this.activeTaskId) { const task = this.data.tasks.find(t => t.id === this.activeTaskId); const taskName = this.isBreakMode ? '☕ Break' : (task?.text.substring(0, 20) || 'Task'); const timeStr = this.formatTime(this.currentTimerSeconds); const icon = this.isTimerRunning ? '▶' : '⏸'; this.statusBarEl.setText(`⚡ ${icon} ${timeStr} - ${taskName}${task && task.text.length > 20 ? '...' : ''}`); this.statusBarEl.addClass('focus-task-status-active'); } else { this.statusBarEl.setText('⚡ Focus Task'); this.statusBarEl.removeClass('focus-task-status-active'); } } // ============ Sounds & Celebrations ============ showCelebration(task: FocusTask) { let messages = CELEBRATION_MESSAGES; let extraMessage = ''; if (task.actualMinutes < task.estimatedMinutes) { const saved = task.estimatedMinutes - task.actualMinutes; messages = EARLY_FINISH_MESSAGES; extraMessage = ` (${saved} min early!)`; } else if (task.actualMinutes > task.estimatedMinutes * 1.5) { messages = OVERTIME_MESSAGES; } const celebration = messages[Math.floor(Math.random() * messages.length)]; new Notice(`${celebration.emoji} ${celebration.message}${extraMessage}`); } playCompletionSound() { try { const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.frequency.setValueAtTime(800, audioContext.currentTime); oscillator.frequency.setValueAtTime(1000, audioContext.currentTime + 0.1); oscillator.frequency.setValueAtTime(1200, audioContext.currentTime + 0.2); gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.3); } catch (e) { console.log('Audio not available'); } } playAlertSound() { try { const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.frequency.setValueAtTime(440, audioContext.currentTime); gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); for (let i = 0; i < 3; i++) { oscillator.frequency.setValueAtTime(440, audioContext.currentTime + i * 0.3); oscillator.frequency.setValueAtTime(550, audioContext.currentTime + i * 0.3 + 0.15); } gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.9); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.9); } catch (e) { console.log('Audio not available'); } } // ============ Daily Note Logging ============ async logTaskToDailyNote(task: FocusTask) { try { const list = this.settings.lists.find(l => l.id === task.list); // Format the task entry const timeDiff = task.actualMinutes - task.estimatedMinutes; let timeComparison = ''; if (timeDiff < 0) { timeComparison = `${Math.abs(timeDiff)}min under estimate ✨`; } else if (timeDiff > 0) { timeComparison = `${timeDiff}min over estimate`; } else { timeComparison = `exactly on target 🎯`; } const completedTime = new Date(task.completedAt || Date.now()).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }); const taskEntry = `- [x] ${task.text} | ${list?.icon || '📋'} ${list?.name || 'Task'} | ⏱️ ${this.formatTimeHuman(task.actualMinutes)} / ${this.formatTimeHuman(task.estimatedMinutes)} (${timeComparison}) | ✅ ${completedTime}`; await this.appendToDailyNote(taskEntry); } catch (e) { console.error('Failed to log task to daily note:', e); new Notice('Failed to log task to daily note. Make sure Daily Notes core plugin is enabled.'); } } getDailyNoteSettings(): { folder: string; format: string; template: string } | null { // Access the core Daily Notes plugin settings const dailyNotesPlugin = (this.app as any).internalPlugins?.plugins?.['daily-notes']; if (!dailyNotesPlugin?.enabled) { return null; } const settings = dailyNotesPlugin.instance?.options; return { folder: settings?.folder || '', format: settings?.format || 'YYYY-MM-DD', template: settings?.template || '', }; } formatDailyNoteDate(format: string): string { const now = new Date(); const year = now.getFullYear(); const month = now.getMonth() + 1; const day = now.getDate(); // Use placeholders to avoid replacement conflicts // Replace longer tokens first, use unique placeholders let result = format; // Year tokens result = result.replace(/YYYY/g, year.toString()); result = result.replace(/YY/g, year.toString().slice(-2)); // Month tokens (longer first) result = result.replace(/MMMM/g, now.toLocaleDateString('en-US', { month: 'long' })); result = result.replace(/MMM/g, now.toLocaleDateString('en-US', { month: 'short' })); result = result.replace(/MM/g, month.toString().padStart(2, '0')); // Only replace standalone M, not part of other tokens result = result.replace(/(? { const { vault } = this.app; const dailySettings = this.getDailyNoteSettings(); if (!dailySettings) { new Notice('Daily Notes core plugin is not enabled. Please enable it in Settings → Core plugins.'); return null; } const filename = this.formatDailyNoteDate(dailySettings.format); const folder = dailySettings.folder ? `${dailySettings.folder}/` : ''; const path = `${folder}${filename}.md`; // Check if daily note exists let file = vault.getAbstractFileByPath(path); if (file && file instanceof TFile) { return file; } // Create the daily note try { // Ensure folder exists if (dailySettings.folder) { const folderExists = vault.getAbstractFileByPath(dailySettings.folder); if (!folderExists) { await vault.createFolder(dailySettings.folder); } } // Check if template exists and use it let content = ''; if (dailySettings.template) { const templatePath = dailySettings.template.endsWith('.md') ? dailySettings.template : `${dailySettings.template}.md`; const templateFile = vault.getAbstractFileByPath(templatePath); if (templateFile && templateFile instanceof TFile) { content = await vault.read(templateFile); // Replace template variables content = content .replace(/{{date}}/g, filename) .replace(/{{time}}/g, new Date().toLocaleTimeString()) .replace(/{{title}}/g, filename); } } // Create the file const newFile = await vault.create(path, content); new Notice(`📝 Created daily note: ${filename}`); return newFile; } catch (e) { console.error('Failed to create daily note:', e); new Notice('Failed to create daily note'); return null; } } async appendToDailyNote(content: string) { const file = await this.getOrCreateDailyNote(); if (!file) { return; } const { vault } = this.app; // Read existing content and append to the end const existingContent = await vault.read(file); const newContent = existingContent.trimEnd() + '\n' + content + '\n'; await vault.modify(file, newContent); new Notice('📝 Task logged to daily note'); } // ============ Utilities ============ formatTime(seconds: number): string { const absSeconds = Math.abs(seconds); const mins = Math.floor(absSeconds / 60); const secs = absSeconds % 60; const sign = seconds < 0 ? '-' : ''; return `${sign}${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } formatTimeHuman(minutes: number): string { if (minutes < 60) return `${minutes}min`; const hours = Math.floor(minutes / 60); const mins = minutes % 60; return mins > 0 ? `${hours}hr ${mins}min` : `${hours}hr`; } refreshView() { const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_FOCUS_TASK); leaves.forEach(leaf => { if (leaf.view instanceof FocusTaskView) { leaf.view.refresh(); } }); } // Light refresh - only updates timer display without rebuilding DOM updateTimerDisplay() { const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_FOCUS_TASK); leaves.forEach(leaf => { if (leaf.view instanceof FocusTaskView) { leaf.view.updateTimerDisplay(); } }); } getTasksByList(listId: string): FocusTask[] { return this.data.tasks.filter(t => t.list === listId); } getPendingTasks(): FocusTask[] { return this.data.tasks.filter(t => !t.completed); } getTodaysTasks(): FocusTask[] { const today = new Date().toDateString(); return this.data.tasks.filter(t => { if (t.scheduledDate === today) return true; if (!t.scheduledDate && !t.completed) return true; return false; }); } getStats() { const pending = this.getPendingTasks(); const totalEstimate = pending.reduce((sum, t) => sum + t.estimatedMinutes, 0); const completedTasks = this.data.tasks.filter(t => t.completed); const avgAccuracy = completedTasks.length > 0 ? completedTasks.reduce((sum, t) => sum + (t.estimatedMinutes / Math.max(t.actualMinutes, 1)), 0) / completedTasks.length : 1; return { pendingCount: pending.length, completedToday: this.data.completedToday, totalEstimatedMinutes: totalEstimate, totalFocusMinutesToday: Math.floor(this.focusSecondsToday / 60), streak: this.data.streak, pomodorosCompleted: this.data.pomodorosCompleted, avgAccuracy: Math.round(avgAccuracy * 100), }; } } // ============ Settings Tab ============ class FocusTaskSettingTab extends PluginSettingTab { plugin: FocusTaskPlugin; constructor(app: App, plugin: FocusTaskPlugin) { super(app, plugin); this.plugin = plugin; } display(): void { const { containerEl } = this; containerEl.empty(); containerEl.createEl('h1', { text: '⚡ Focus Task Settings' }); // Pomodoro Settings containerEl.createEl('h2', { text: '🍅 Pomodoro Timer' }); new Setting(containerEl) .setName('Work Duration') .setDesc('Length of each work session in minutes') .addSlider(slider => slider .setLimits(5, 60, 5) .setValue(this.plugin.settings.pomodoroWorkMinutes) .setDynamicTooltip() .onChange(async value => { this.plugin.settings.pomodoroWorkMinutes = value; await this.plugin.saveAllData(); })); new Setting(containerEl) .setName('Short Break Duration') .setDesc('Length of short breaks in minutes') .addSlider(slider => slider .setLimits(1, 15, 1) .setValue(this.plugin.settings.pomodoroBreakMinutes) .setDynamicTooltip() .onChange(async value => { this.plugin.settings.pomodoroBreakMinutes = value; await this.plugin.saveAllData(); })); new Setting(containerEl) .setName('Long Break Duration') .setDesc('Length of long breaks in minutes') .addSlider(slider => slider .setLimits(5, 30, 5) .setValue(this.plugin.settings.longBreakMinutes) .setDynamicTooltip() .onChange(async value => { this.plugin.settings.longBreakMinutes = value; await this.plugin.saveAllData(); })); new Setting(containerEl) .setName('Long Break Interval') .setDesc('Number of pomodoros before a long break') .addSlider(slider => slider .setLimits(2, 6, 1) .setValue(this.plugin.settings.longBreakInterval) .setDynamicTooltip() .onChange(async value => { this.plugin.settings.longBreakInterval = value; await this.plugin.saveAllData(); })); new Setting(containerEl) .setName('Auto-start Breaks') .setDesc('Automatically start break timer after work session') .addToggle(toggle => toggle .setValue(this.plugin.settings.autoStartBreak) .onChange(async value => { this.plugin.settings.autoStartBreak = value; await this.plugin.saveAllData(); })); // General Settings containerEl.createEl('h2', { text: '⚙️ General' }); new Setting(containerEl) .setName('Default Time Estimate') .setDesc('Default estimated time for new tasks in minutes') .addSlider(slider => slider .setLimits(5, 120, 5) .setValue(this.plugin.settings.defaultEstimateMinutes) .setDynamicTooltip() .onChange(async value => { this.plugin.settings.defaultEstimateMinutes = value; await this.plugin.saveAllData(); })); new Setting(containerEl) .setName('Enable Sounds') .setDesc('Play sounds for timer completion and task completion') .addToggle(toggle => toggle .setValue(this.plugin.settings.enableSounds) .onChange(async value => { this.plugin.settings.enableSounds = value; await this.plugin.saveAllData(); })); new Setting(containerEl) .setName('Enable Celebrations') .setDesc('Show celebration messages when completing tasks') .addToggle(toggle => toggle .setValue(this.plugin.settings.enableCelebrations) .onChange(async value => { this.plugin.settings.enableCelebrations = value; await this.plugin.saveAllData(); })); // Daily Note Integration containerEl.createEl('h2', { text: '📝 Daily Note Integration' }); new Setting(containerEl) .setName('Log completed tasks to daily note') .setDesc('When you complete a task, add an entry to your daily note. Uses the core Daily Notes plugin settings.') .addToggle(toggle => toggle .setValue(this.plugin.settings.logToDaily) .onChange(async value => { this.plugin.settings.logToDaily = value; await this.plugin.saveAllData(); })); // Show info about Daily Notes plugin const infoEl = containerEl.createEl('div', { cls: 'setting-item-description' }); infoEl.style.marginTop = '-10px'; infoEl.style.marginBottom = '20px'; infoEl.innerHTML = ` This feature uses the Daily Notes core plugin. Configure your daily note folder, date format, and template in Settings → Core plugins → Daily notes. `; // Lists Management containerEl.createEl('h2', { text: '📋 Lists' }); this.plugin.settings.lists.forEach((list, index) => { new Setting(containerEl) .setName(`${list.icon} ${list.name}`) .addText(text => text .setValue(list.name) .setPlaceholder('List name') .onChange(async value => { this.plugin.settings.lists[index].name = value; await this.plugin.saveAllData(); })) .addText(text => text .setValue(list.icon) .setPlaceholder('Emoji') .onChange(async value => { this.plugin.settings.lists[index].icon = value; await this.plugin.saveAllData(); })) .addColorPicker(picker => picker .setValue(list.color) .onChange(async value => { this.plugin.settings.lists[index].color = value; await this.plugin.saveAllData(); })) .addButton(btn => btn .setIcon('trash') .setTooltip('Delete list') .onClick(async () => { this.plugin.settings.lists.splice(index, 1); await this.plugin.saveAllData(); this.display(); })); }); new Setting(containerEl) .addButton(btn => btn .setButtonText('+ Add List') .onClick(async () => { this.plugin.settings.lists.push({ id: this.plugin.generateId(), name: 'New List', color: '#6366f1', icon: '📁', }); await this.plugin.saveAllData(); this.display(); })); // About section containerEl.createEl('h2', { text: '📖 About' }); const aboutDiv = containerEl.createDiv({ cls: 'focus-task-about' }); aboutDiv.innerHTML = `

Focus Task is heavily inspired by Blitzit, a fantastic productivity app that combines task management with focused time tracking.

This plugin brings similar functionality directly into Obsidian, allowing you to manage tasks, use the Pomodoro technique, and track your productivity without leaving your notes.

Source Code

`; } }