Initial commit
This commit is contained in:
843
src/main.ts
Normal file
843
src/main.ts
Normal file
@@ -0,0 +1,843 @@
|
||||
import {
|
||||
App,
|
||||
Notice,
|
||||
Plugin,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
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;
|
||||
|
||||
// Floating timer element
|
||||
floatingTimerEl: HTMLElement | null = null;
|
||||
|
||||
async onload() {
|
||||
await this.loadAllData();
|
||||
|
||||
// Check and reset daily stats
|
||||
this.checkDailyReset();
|
||||
|
||||
// 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 floating timer if enabled
|
||||
if (this.settings.showFloatingTimer) {
|
||||
this.createFloatingTimer();
|
||||
}
|
||||
}
|
||||
|
||||
onunload() {
|
||||
this.stopTimer();
|
||||
this.removeFloatingTimer();
|
||||
}
|
||||
|
||||
async loadAllData() {
|
||||
const loaded = await this.loadData();
|
||||
this.data = Object.assign({}, DEFAULT_DATA, loaded?.data || {});
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, loaded?.settings || {});
|
||||
}
|
||||
|
||||
async saveAllData() {
|
||||
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.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<FocusTask>) {
|
||||
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();
|
||||
}
|
||||
|
||||
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 ============
|
||||
|
||||
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;
|
||||
|
||||
// Start interval (count up mode - stopwatch)
|
||||
this.timerInterval = window.setInterval(() => {
|
||||
this.currentTimerSeconds++;
|
||||
task.actualMinutes = Math.floor(this.currentTimerSeconds / 60);
|
||||
|
||||
// Update floating timer
|
||||
this.updateFloatingTimer();
|
||||
this.refreshView();
|
||||
|
||||
// Check if over estimate
|
||||
if (this.currentTimerSeconds === task.estimatedMinutes * 60) {
|
||||
if (this.settings.enableSounds) {
|
||||
this.playAlertSound();
|
||||
}
|
||||
new Notice(`⏰ Time's up for: ${task.text}`);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
this.saveAllData();
|
||||
this.refreshView();
|
||||
this.updateFloatingTimer();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
this.timerInterval = window.setInterval(() => {
|
||||
this.currentTimerSeconds--;
|
||||
|
||||
if (!this.isBreakMode) {
|
||||
task.actualMinutes = Math.floor((this.settings.pomodoroWorkMinutes * 60 - this.currentTimerSeconds) / 60);
|
||||
this.data.totalFocusMinutesToday = Math.floor(this.data.totalFocusMinutesToday + 1/60);
|
||||
}
|
||||
|
||||
this.updateFloatingTimer();
|
||||
this.refreshView();
|
||||
|
||||
if (this.currentTimerSeconds <= 0) {
|
||||
this.handlePomodoroEnd();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
this.updateFloatingTimer();
|
||||
this.refreshView();
|
||||
}
|
||||
|
||||
handlePomodoroEnd() {
|
||||
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.stopTimer();
|
||||
}
|
||||
} else {
|
||||
// Break ended
|
||||
if (this.settings.enableSounds) {
|
||||
this.playAlertSound();
|
||||
}
|
||||
new Notice('⚡ Break over! Ready to focus?');
|
||||
this.isBreakMode = false;
|
||||
this.stopTimer();
|
||||
}
|
||||
|
||||
this.saveAllData();
|
||||
}
|
||||
|
||||
startBreak() {
|
||||
this.isBreakMode = true;
|
||||
const isLongBreak = this.pomodoroCount % this.settings.longBreakInterval === 0;
|
||||
this.currentTimerSeconds = (isLongBreak ? this.settings.longBreakMinutes : this.settings.pomodoroBreakMinutes) * 60;
|
||||
|
||||
new Notice(isLongBreak ? '☕ Long break time!' : '☕ Short break time!');
|
||||
|
||||
if (!this.timerInterval) {
|
||||
this.timerInterval = window.setInterval(() => {
|
||||
this.currentTimerSeconds--;
|
||||
this.updateFloatingTimer();
|
||||
this.refreshView();
|
||||
|
||||
if (this.currentTimerSeconds <= 0) {
|
||||
this.handlePomodoroEnd();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
this.updateFloatingTimer();
|
||||
this.refreshView();
|
||||
}
|
||||
|
||||
toggleTimer() {
|
||||
if (this.isTimerRunning && this.timerInterval) {
|
||||
// Pause
|
||||
window.clearInterval(this.timerInterval);
|
||||
this.timerInterval = null;
|
||||
this.isTimerRunning = false;
|
||||
} else if (this.activeTaskId) {
|
||||
// Resume
|
||||
this.isTimerRunning = true;
|
||||
const task = this.data.tasks.find(t => t.id === this.activeTaskId);
|
||||
|
||||
this.timerInterval = window.setInterval(() => {
|
||||
this.currentTimerSeconds--;
|
||||
if (task && !this.isBreakMode) {
|
||||
task.actualMinutes = Math.floor((this.settings.pomodoroWorkMinutes * 60 - this.currentTimerSeconds) / 60);
|
||||
}
|
||||
this.updateFloatingTimer();
|
||||
this.refreshView();
|
||||
|
||||
if (this.currentTimerSeconds <= 0) {
|
||||
this.handlePomodoroEnd();
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
new Notice('No active task. Select a task first.');
|
||||
}
|
||||
|
||||
this.updateFloatingTimer();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
this.isTimerRunning = false;
|
||||
this.activeTaskId = null;
|
||||
this.updateFloatingTimer();
|
||||
this.saveAllData();
|
||||
}
|
||||
|
||||
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!');
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Floating Timer ============
|
||||
|
||||
createFloatingTimer() {
|
||||
if (this.floatingTimerEl) return;
|
||||
|
||||
this.floatingTimerEl = document.body.createEl('div', {
|
||||
cls: 'focus-task-floating-timer',
|
||||
});
|
||||
|
||||
this.floatingTimerEl.innerHTML = `
|
||||
<div class="focus-task-floating-inner">
|
||||
<div class="focus-task-floating-task">No active task</div>
|
||||
<div class="focus-task-floating-time">00:00</div>
|
||||
<div class="focus-task-floating-controls">
|
||||
<button class="focus-task-float-btn play-pause">▶</button>
|
||||
<button class="focus-task-float-btn complete">✓</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Make draggable
|
||||
this.makeDraggable(this.floatingTimerEl);
|
||||
|
||||
// Add event listeners
|
||||
const playPauseBtn = this.floatingTimerEl.querySelector('.play-pause');
|
||||
const completeBtn = this.floatingTimerEl.querySelector('.complete');
|
||||
|
||||
playPauseBtn?.addEventListener('click', () => this.toggleTimer());
|
||||
completeBtn?.addEventListener('click', () => this.completeActiveTask());
|
||||
}
|
||||
|
||||
removeFloatingTimer() {
|
||||
if (this.floatingTimerEl) {
|
||||
this.floatingTimerEl.remove();
|
||||
this.floatingTimerEl = null;
|
||||
}
|
||||
}
|
||||
|
||||
updateFloatingTimer() {
|
||||
if (!this.floatingTimerEl) return;
|
||||
|
||||
const taskEl = this.floatingTimerEl.querySelector('.focus-task-floating-task');
|
||||
const timeEl = this.floatingTimerEl.querySelector('.focus-task-floating-time');
|
||||
const playPauseBtn = this.floatingTimerEl.querySelector('.play-pause');
|
||||
|
||||
if (this.activeTaskId) {
|
||||
const task = this.data.tasks.find(t => t.id === this.activeTaskId);
|
||||
if (task && taskEl) {
|
||||
taskEl.textContent = this.isBreakMode ? '☕ Break Time' : task.text;
|
||||
}
|
||||
} else if (taskEl) {
|
||||
taskEl.textContent = 'No active task';
|
||||
}
|
||||
|
||||
if (timeEl) {
|
||||
timeEl.textContent = this.formatTime(this.currentTimerSeconds);
|
||||
timeEl.classList.toggle('focus-task-overtime', this.currentTimerSeconds < 0);
|
||||
}
|
||||
|
||||
if (playPauseBtn) {
|
||||
playPauseBtn.textContent = this.isTimerRunning ? '⏸' : '▶';
|
||||
}
|
||||
|
||||
// Update color based on state
|
||||
this.floatingTimerEl.classList.toggle('focus-task-break-mode', this.isBreakMode);
|
||||
this.floatingTimerEl.classList.toggle('focus-task-active', this.isTimerRunning);
|
||||
}
|
||||
|
||||
makeDraggable(el: HTMLElement) {
|
||||
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
|
||||
|
||||
el.onmousedown = dragMouseDown;
|
||||
|
||||
function dragMouseDown(e: MouseEvent) {
|
||||
if ((e.target as HTMLElement).tagName === 'BUTTON') return;
|
||||
e.preventDefault();
|
||||
pos3 = e.clientX;
|
||||
pos4 = e.clientY;
|
||||
document.onmouseup = closeDragElement;
|
||||
document.onmousemove = elementDrag;
|
||||
}
|
||||
|
||||
function elementDrag(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
pos1 = pos3 - e.clientX;
|
||||
pos2 = pos4 - e.clientY;
|
||||
pos3 = e.clientX;
|
||||
pos4 = e.clientY;
|
||||
el.style.top = (el.offsetTop - pos2) + "px";
|
||||
el.style.left = (el.offsetLeft - pos1) + "px";
|
||||
el.style.right = 'auto';
|
||||
el.style.bottom = 'auto';
|
||||
}
|
||||
|
||||
function closeDragElement() {
|
||||
document.onmouseup = null;
|
||||
document.onmousemove = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 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');
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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.data.totalFocusMinutesToday),
|
||||
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();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Show Floating Timer')
|
||||
.setDesc('Display a draggable floating timer widget')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.showFloatingTimer)
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.showFloatingTimer = value;
|
||||
if (value) {
|
||||
this.plugin.createFloatingTimer();
|
||||
} else {
|
||||
this.plugin.removeFloatingTimer();
|
||||
}
|
||||
await this.plugin.saveAllData();
|
||||
}));
|
||||
|
||||
// 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 = `
|
||||
<p><strong>Focus Task</strong> is heavily inspired by <a href="https://www.blitzit.app/">Blitzit</a>,
|
||||
a fantastic productivity app that combines task management with focused time tracking.</p>
|
||||
<p>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.</p>
|
||||
<p>
|
||||
<a href="https://git.cribdev.com/crib/focus-task">Source Code</a>
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user