Bug Fixes: - Removed progress bar from stopwatch mode (only shows in pomodoro/break) - Fixed daily stats not resetting at midnight when app stays open - Fixed focus time tracking to only count completed tasks, not stopped sessions Improvements: - Added daily reset check interval (checks every 60 seconds) - Added daily reset check on app visibility change - Focus time now accurately reflects completed work only - Session time tracking separated from daily focus time Technical: - Added dailyResetCheckInterval for periodic day change detection - Added sessionStartSeconds to track focus time per session - Removed real-time focus time updates from timer intervals - Focus time only added in completeTask() method - Progress bar conditionally rendered based on isStopwatchMode flag
447 lines
17 KiB
TypeScript
447 lines
17 KiB
TypeScript
import {
|
|
ItemView,
|
|
WorkspaceLeaf,
|
|
} from 'obsidian';
|
|
|
|
import { VIEW_TYPE_IMMERSE, ImmerseTask } from './types';
|
|
import { QuickAddTaskModal, EditTaskModal } from './modals';
|
|
import ImmersePlugin from './main';
|
|
|
|
// ============ Main View ============
|
|
|
|
export class ImmerseView extends ItemView {
|
|
plugin: ImmersePlugin;
|
|
currentFilter: string = 'all';
|
|
|
|
// References to elements that need frequent updates
|
|
private timerTimeEl: HTMLElement | null = null;
|
|
private progressBarEl: HTMLElement | null = null;
|
|
private actualTimeEl: HTMLElement | null = null;
|
|
private pauseBtnEl: HTMLElement | null = null;
|
|
|
|
constructor(leaf: WorkspaceLeaf, plugin: ImmersePlugin) {
|
|
super(leaf);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
getViewType(): string {
|
|
return VIEW_TYPE_IMMERSE;
|
|
}
|
|
|
|
getDisplayText(): string {
|
|
return 'Immerse';
|
|
}
|
|
|
|
getIcon(): string {
|
|
return 'zap';
|
|
}
|
|
|
|
async onOpen() {
|
|
this.refresh();
|
|
}
|
|
|
|
// Light update - only updates timer display without rebuilding DOM
|
|
updateTimerDisplay() {
|
|
if (!this.timerTimeEl) return;
|
|
|
|
// Update timer text
|
|
this.timerTimeEl.textContent = this.plugin.formatTime(this.plugin.currentTimerSeconds);
|
|
|
|
// Update progress bar
|
|
if (this.progressBarEl) {
|
|
let progressPercent = 0;
|
|
if (this.plugin.isBreakMode) {
|
|
const breakDuration = this.plugin.pomodoroCount % this.plugin.settings.longBreakInterval === 0
|
|
? this.plugin.settings.longBreakMinutes * 60
|
|
: this.plugin.settings.pomodoroBreakMinutes * 60;
|
|
progressPercent = ((breakDuration - this.plugin.currentTimerSeconds) / breakDuration) * 100;
|
|
} else {
|
|
const workDuration = this.plugin.settings.pomodoroWorkMinutes * 60;
|
|
progressPercent = ((workDuration - this.plugin.currentTimerSeconds) / workDuration) * 100;
|
|
}
|
|
this.progressBarEl.style.width = `${Math.min(Math.max(progressPercent, 0), 100)}%`;
|
|
}
|
|
|
|
// Update actual time display
|
|
if (this.actualTimeEl && this.plugin.activeTaskId) {
|
|
const task = this.plugin.data.tasks.find(t => t.id === this.plugin.activeTaskId);
|
|
if (task) {
|
|
this.actualTimeEl.textContent = `Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
refresh() {
|
|
const container = this.containerEl.children[1];
|
|
container.empty();
|
|
container.addClass('immerse-container');
|
|
|
|
// Reset element references
|
|
this.timerTimeEl = null;
|
|
this.progressBarEl = null;
|
|
this.actualTimeEl = null;
|
|
this.pauseBtnEl = null;
|
|
|
|
// Header
|
|
this.renderHeader(container);
|
|
|
|
// Stats bar
|
|
this.renderStatsBar(container);
|
|
|
|
// Active task / Timer
|
|
this.renderActiveTask(container);
|
|
|
|
// Task list
|
|
this.renderTaskList(container);
|
|
}
|
|
|
|
renderHeader(container: Element) {
|
|
const header = container.createEl('div', { cls: 'immerse-header' });
|
|
|
|
const titleSection = header.createEl('div', { cls: 'immerse-title-section' });
|
|
titleSection.createEl('h2', { text: '⚡ Immerse', cls: 'immerse-title' });
|
|
|
|
const today = new Date();
|
|
const dateStr = today.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' });
|
|
titleSection.createEl('div', { text: dateStr, cls: 'immerse-date' });
|
|
|
|
const actions = header.createEl('div', { cls: 'immerse-header-actions' });
|
|
|
|
const reportsBtn = actions.createEl('button', { cls: 'immerse-btn' });
|
|
reportsBtn.innerHTML = '📊 Reports';
|
|
reportsBtn.addEventListener('click', () => {
|
|
this.plugin.activateReportView();
|
|
});
|
|
|
|
const addBtn = actions.createEl('button', { cls: 'immerse-btn immerse-btn-primary' });
|
|
addBtn.innerHTML = '+ Add Task';
|
|
addBtn.addEventListener('click', () => {
|
|
new QuickAddTaskModal(this.app, this.plugin).open();
|
|
});
|
|
}
|
|
|
|
renderStatsBar(container: Element) {
|
|
const stats = this.plugin.getStats();
|
|
const statsBar = container.createEl('div', { cls: 'immerse-stats-bar' });
|
|
|
|
const statItems = [
|
|
{ label: 'Pending', value: stats.pendingCount.toString(), icon: '📋' },
|
|
{ label: 'Done Today', value: stats.completedToday.toString(), icon: '✅' },
|
|
{ label: 'Today\'s Focus', value: this.plugin.formatTimeHuman(stats.totalFocusMinutesToday), icon: '⏱️' },
|
|
{ label: 'Streak', value: `${stats.streak} days`, icon: '🔥' },
|
|
];
|
|
|
|
statItems.forEach(stat => {
|
|
const item = statsBar.createEl('div', { cls: 'immerse-stat-item' });
|
|
item.createEl('div', { cls: 'immerse-stat-icon', text: stat.icon });
|
|
item.createEl('div', { cls: 'immerse-stat-value', text: stat.value });
|
|
item.createEl('div', { cls: 'immerse-stat-label', text: stat.label });
|
|
});
|
|
}
|
|
|
|
renderActiveTask(container: Element) {
|
|
const activeSection = container.createEl('div', { cls: 'immerse-active-section' });
|
|
|
|
if (this.plugin.activeTaskId) {
|
|
const task = this.plugin.data.tasks.find(t => t.id === this.plugin.activeTaskId);
|
|
if (task) {
|
|
activeSection.addClass('immerse-has-active');
|
|
|
|
const activeCard = activeSection.createEl('div', { cls: 'immerse-active-card' });
|
|
|
|
if (this.plugin.isBreakMode) {
|
|
activeCard.addClass('immerse-break-card');
|
|
const breakLabel = this.plugin.currentTimerSeconds > 0 ? '☕ BREAK TIME' : '✨ BREAK COMPLETE';
|
|
activeCard.createEl('div', { cls: 'immerse-active-label', text: breakLabel });
|
|
} else {
|
|
// Determine label based on whether timer is active and mode (stopwatch vs pomodoro)
|
|
let workLabel: string;
|
|
if (this.plugin.currentTimerSeconds > 0 || this.plugin.isStopwatchMode) {
|
|
workLabel = '🎯 FOCUSING ON';
|
|
} else {
|
|
workLabel = '🍅 POMODORO COMPLETE';
|
|
}
|
|
activeCard.createEl('div', { cls: 'immerse-active-label', text: workLabel });
|
|
}
|
|
|
|
activeCard.createEl('div', { cls: 'immerse-active-task-name', text: task.text });
|
|
|
|
// Timer display - store reference for updates
|
|
const timerDisplay = activeCard.createEl('div', { cls: 'immerse-timer-display' });
|
|
this.timerTimeEl = timerDisplay.createEl('span', {
|
|
cls: 'immerse-timer-time',
|
|
text: this.plugin.formatTime(this.plugin.currentTimerSeconds)
|
|
});
|
|
|
|
// Progress bar - only show in pomodoro/break mode, not stopwatch
|
|
if (!this.plugin.isStopwatchMode) {
|
|
const progressWrap = activeCard.createEl('div', { cls: 'immerse-progress-wrap' });
|
|
this.progressBarEl = progressWrap.createEl('div', { cls: 'immerse-progress-bar' });
|
|
|
|
let progressPercent = 0;
|
|
if (this.plugin.isBreakMode) {
|
|
const breakDuration = this.plugin.pomodoroCount % this.plugin.settings.longBreakInterval === 0
|
|
? this.plugin.settings.longBreakMinutes * 60
|
|
: this.plugin.settings.pomodoroBreakMinutes * 60;
|
|
progressPercent = ((breakDuration - this.plugin.currentTimerSeconds) / breakDuration) * 100;
|
|
} else {
|
|
const workDuration = this.plugin.settings.pomodoroWorkMinutes * 60;
|
|
progressPercent = ((workDuration - this.plugin.currentTimerSeconds) / workDuration) * 100;
|
|
}
|
|
|
|
this.progressBarEl.style.width = `${Math.min(Math.max(progressPercent, 0), 100)}%`;
|
|
if (progressPercent >= 100) this.progressBarEl.addClass('immerse-overtime');
|
|
}
|
|
|
|
// Time info - store reference for actual time updates
|
|
if (!this.plugin.isBreakMode) {
|
|
const timeInfo = activeCard.createEl('div', { cls: 'immerse-time-info' });
|
|
timeInfo.createEl('span', { text: `Est: ${this.plugin.formatTimeHuman(task.estimatedMinutes)}` });
|
|
this.actualTimeEl = timeInfo.createEl('span', { text: `Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}` });
|
|
}
|
|
|
|
// Controls
|
|
const controls = activeCard.createEl('div', { cls: 'immerse-active-controls' });
|
|
|
|
if (this.plugin.isBreakMode) {
|
|
// Break mode controls
|
|
if (this.plugin.currentTimerSeconds > 0) {
|
|
// Break is still counting down
|
|
this.pauseBtnEl = controls.createEl('button', { cls: 'immerse-btn immerse-btn-secondary' });
|
|
this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? '⏸ Pause' : '▶ Resume';
|
|
this.pauseBtnEl.addEventListener('click', () => this.plugin.toggleTimer());
|
|
|
|
const skipBreakBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-primary' });
|
|
skipBreakBtn.innerHTML = '⏭ Skip Break';
|
|
skipBreakBtn.addEventListener('click', () => {
|
|
this.plugin.isBreakMode = false;
|
|
this.plugin.startPomodoro(task.id);
|
|
});
|
|
|
|
const stopBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-danger' });
|
|
stopBtn.innerHTML = '✕ Stop';
|
|
stopBtn.addEventListener('click', () => {
|
|
this.plugin.isBreakMode = false;
|
|
this.plugin.stopTimer();
|
|
});
|
|
} else {
|
|
// Break timer finished - show resume button
|
|
const resumeWorkBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-success' });
|
|
resumeWorkBtn.innerHTML = '▶ Resume Work';
|
|
resumeWorkBtn.addEventListener('click', () => {
|
|
this.plugin.isBreakMode = false;
|
|
this.plugin.startPomodoro(task.id);
|
|
});
|
|
|
|
const stopBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-danger' });
|
|
stopBtn.innerHTML = '✕ Stop';
|
|
stopBtn.addEventListener('click', () => {
|
|
this.plugin.isBreakMode = false;
|
|
this.plugin.stopTimer();
|
|
});
|
|
}
|
|
} else {
|
|
// Work mode controls
|
|
if (this.plugin.currentTimerSeconds > 0 || this.plugin.isStopwatchMode) {
|
|
// Work session still running (or stopwatch mode active)
|
|
this.pauseBtnEl = controls.createEl('button', { cls: 'immerse-btn immerse-btn-secondary' });
|
|
this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? '⏸ Pause' : '▶ Resume';
|
|
this.pauseBtnEl.addEventListener('click', () => this.plugin.toggleTimer());
|
|
|
|
const completeBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-success' });
|
|
completeBtn.innerHTML = '✓ Complete';
|
|
completeBtn.addEventListener('click', () => this.plugin.completeTask(task.id));
|
|
|
|
const stopBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-danger' });
|
|
stopBtn.innerHTML = '✕ Stop';
|
|
stopBtn.addEventListener('click', () => this.plugin.stopTimer());
|
|
} else {
|
|
// Pomodoro session finished - show break and completion options
|
|
const startBreakBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-secondary' });
|
|
startBreakBtn.innerHTML = '☕ Start Break';
|
|
startBreakBtn.addEventListener('click', () => this.plugin.startBreak());
|
|
|
|
const continueBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-primary' });
|
|
continueBtn.innerHTML = '▶ Continue Working';
|
|
continueBtn.addEventListener('click', () => this.plugin.startPomodoro(task.id));
|
|
|
|
const completeBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-success' });
|
|
completeBtn.innerHTML = '✓ Complete';
|
|
completeBtn.addEventListener('click', () => this.plugin.completeTask(task.id));
|
|
|
|
const stopBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-danger' });
|
|
stopBtn.innerHTML = '✕ Stop';
|
|
stopBtn.addEventListener('click', () => this.plugin.stopTimer());
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// No active task - show start focus prompt
|
|
const startPrompt = activeSection.createEl('div', { cls: 'immerse-start-prompt' });
|
|
startPrompt.createEl('div', { cls: 'immerse-prompt-icon', text: '⚡' });
|
|
startPrompt.createEl('div', { cls: 'immerse-prompt-text', text: 'Ready to focus?' });
|
|
startPrompt.createEl('div', { cls: 'immerse-prompt-hint', text: 'Click ▶ on a task to start a Pomodoro session' });
|
|
}
|
|
}
|
|
|
|
renderTaskList(container: Element) {
|
|
const listSection = container.createEl('div', { cls: 'immerse-list-section' });
|
|
|
|
// Filters
|
|
const filters = listSection.createEl('div', { cls: 'immerse-filters' });
|
|
|
|
const filterOptions = [
|
|
{ id: 'all', label: 'All Tasks' },
|
|
{ id: 'today', label: 'Today' },
|
|
{ id: 'completed', label: 'Completed' },
|
|
...this.plugin.settings.lists.map(l => ({ id: l.id, label: `${l.icon} ${l.name}` })),
|
|
];
|
|
|
|
filterOptions.forEach(opt => {
|
|
const btn = filters.createEl('button', {
|
|
cls: `immerse-filter-btn ${this.currentFilter === opt.id ? 'active' : ''}`,
|
|
text: opt.label,
|
|
});
|
|
btn.addEventListener('click', () => {
|
|
this.currentFilter = opt.id;
|
|
this.refresh();
|
|
});
|
|
});
|
|
|
|
// Task items
|
|
const taskList = listSection.createEl('div', { cls: 'immerse-task-list' });
|
|
|
|
let tasks = this.plugin.data.tasks;
|
|
|
|
// Filter tasks
|
|
if (this.currentFilter === 'today') {
|
|
tasks = this.plugin.getTodaysTasks();
|
|
} else if (this.currentFilter === 'completed') {
|
|
tasks = this.plugin.data.tasks.filter(t => t.completed);
|
|
} else if (this.currentFilter !== 'all') {
|
|
tasks = this.plugin.getTasksByList(this.currentFilter);
|
|
}
|
|
|
|
// Sort: incomplete first, then by creation date
|
|
tasks = [...tasks].sort((a, b) => {
|
|
if (a.completed !== b.completed) return a.completed ? 1 : -1;
|
|
return b.createdAt - a.createdAt;
|
|
});
|
|
|
|
if (tasks.length === 0) {
|
|
const emptyState = taskList.createEl('div', { cls: 'immerse-empty-state' });
|
|
emptyState.createEl('div', { cls: 'immerse-empty-icon', text: '📝' });
|
|
emptyState.createEl('div', { cls: 'immerse-empty-text', text: 'No tasks yet' });
|
|
emptyState.createEl('div', { cls: 'immerse-empty-hint', text: 'Add a task to get started!' });
|
|
} else {
|
|
tasks.forEach(task => this.renderTaskItem(taskList, task));
|
|
}
|
|
}
|
|
|
|
renderTaskItem(container: Element, task: ImmerseTask) {
|
|
const list = this.plugin.settings.lists.find(l => l.id === task.list);
|
|
|
|
// Check if task is overdue
|
|
const isOverdue = !task.completed && task.scheduledDate && task.scheduledTime &&
|
|
new Date(`${task.scheduledDate}T${task.scheduledTime}`).getTime() < Date.now();
|
|
|
|
const taskEl = container.createEl('div', {
|
|
cls: `immerse-task-item ${task.completed ? 'completed' : ''} ${task.isActive ? 'active' : ''} ${isOverdue ? 'overdue' : ''}`
|
|
});
|
|
|
|
// Checkbox
|
|
const checkbox = taskEl.createEl('div', { cls: 'immerse-checkbox' });
|
|
checkbox.innerHTML = task.completed ? '✓' : '';
|
|
checkbox.style.borderColor = list?.color || '#6366f1';
|
|
if (task.completed) {
|
|
checkbox.style.backgroundColor = list?.color || '#6366f1';
|
|
checkbox.style.color = 'white';
|
|
}
|
|
checkbox.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
if (!task.completed) {
|
|
this.plugin.completeTask(task.id);
|
|
}
|
|
});
|
|
|
|
// Task content
|
|
const content = taskEl.createEl('div', { cls: 'immerse-task-content' });
|
|
|
|
const taskHeader = content.createEl('div', { cls: 'immerse-task-header' });
|
|
taskHeader.createEl('span', { cls: 'immerse-task-text', text: task.text });
|
|
|
|
if (list) {
|
|
const listBadge = taskHeader.createEl('span', {
|
|
cls: 'immerse-list-badge',
|
|
text: `${list.icon} ${list.name}`,
|
|
});
|
|
listBadge.style.backgroundColor = list.color + '20';
|
|
listBadge.style.color = list.color;
|
|
}
|
|
|
|
const taskMeta = content.createEl('div', { cls: 'immerse-task-meta' });
|
|
taskMeta.createEl('span', { text: `Est: ${this.plugin.formatTimeHuman(task.estimatedMinutes)}` });
|
|
|
|
if (task.actualMinutes > 0) {
|
|
const actualSpan = taskMeta.createEl('span');
|
|
actualSpan.setText(`Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}`);
|
|
if (task.actualMinutes > task.estimatedMinutes) {
|
|
actualSpan.addClass('immerse-overtime-text');
|
|
}
|
|
}
|
|
|
|
// Show scheduled date/time if set
|
|
if (task.scheduledDate) {
|
|
const scheduleSpan = taskMeta.createEl('span', {
|
|
cls: `immerse-schedule-badge ${isOverdue ? 'overdue' : ''}`
|
|
});
|
|
const dateStr = task.scheduledDate;
|
|
const timeStr = task.scheduledTime || '';
|
|
if (isOverdue) {
|
|
scheduleSpan.setText(`⚠️ OVERDUE: ${dateStr}${timeStr ? ' ' + timeStr : ''}`);
|
|
} else {
|
|
scheduleSpan.setText(`📅 ${dateStr}${timeStr ? ' ' + timeStr : ''}`);
|
|
}
|
|
if (task.reminderMinutes) {
|
|
scheduleSpan.title = `Reminder set for ${task.reminderMinutes} min before`;
|
|
}
|
|
}
|
|
|
|
// Actions
|
|
const actions = taskEl.createEl('div', { cls: 'immerse-task-actions' });
|
|
|
|
if (!task.completed) {
|
|
// Start pomodoro button
|
|
const startBtn = actions.createEl('button', { cls: 'immerse-task-btn', attr: { 'aria-label': 'Start Pomodoro' } });
|
|
startBtn.innerHTML = '▶';
|
|
startBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
this.plugin.startPomodoro(task.id);
|
|
});
|
|
|
|
// Stopwatch mode button
|
|
const stopwatchBtn = actions.createEl('button', { cls: 'immerse-task-btn', attr: { 'aria-label': 'Start Stopwatch' } });
|
|
stopwatchBtn.innerHTML = '⏱';
|
|
stopwatchBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
this.plugin.startTimer(task.id);
|
|
});
|
|
}
|
|
|
|
// Edit button
|
|
const editBtn = actions.createEl('button', { cls: 'immerse-task-btn', attr: { 'aria-label': 'Edit' } });
|
|
editBtn.innerHTML = '✏️';
|
|
editBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
new EditTaskModal(this.app, this.plugin, task).open();
|
|
});
|
|
|
|
// Delete button
|
|
const deleteBtn = actions.createEl('button', { cls: 'immerse-task-btn immerse-delete-btn', attr: { 'aria-label': 'Delete' } });
|
|
deleteBtn.innerHTML = '🗑';
|
|
deleteBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
this.plugin.deleteTask(task.id);
|
|
});
|
|
}
|
|
} |