32 Commits

Author SHA1 Message Date
50ef40d2e0 Release v1.1.1: Critical timer bug fix
Fixed critical bug where actual time spent on tasks was being reset to 0 when resuming work after a break in Pomodoro mode.

## Bug Fix
- Fixed timer actual time resetting to 0 when skipping break or resuming work
- Added preserveActualTime parameter to stopTimer() function
- Actual time now correctly accumulates across multiple Pomodoro sessions
2025-11-25 10:11:05 +01:00
b3aa1f2992 Improve .gitignore comments and patterns 2025-11-24 21:22:48 +01:00
e1484b1723 Update .gitignore to exclude release notes and versioned roadmaps 2025-11-24 21:19:47 +01:00
8207b3626e Update .gitignore 2025-11-24 20:49:44 +01:00
f1af574eb9 Release v1.1.0: Reports, Scheduling, and Mobile Optimization 2025-11-24 20:48:47 +01:00
2fad5d88ab feat: Add task scheduling and reminders system
Add comprehensive scheduling functionality with reminder notifications:

- Add scheduledDate, scheduledTime, and reminderMinutes fields to ImmerseTask
- Add enableReminders and defaultReminderMinutes to plugin settings
- Implement reminder notification system with 30-second background checks
- Add overdue task detection with visual indicators
- Add native HTML5 date/time pickers in Quick Add and Edit modals
- Add reminder dropdown with 5/10/15/30/60 minute options
- Display scheduled date/time with blue badge (📅) in task list
- Show pulsing red "OVERDUE" badge (⚠️) for past-due tasks
- Add red left border highlight for overdue tasks
- Implement startup check for due/overdue tasks when plugin loads
- Show overdue notices when Obsidian opens if tasks are past due
- Play alert sounds for reminders (if sounds enabled)
- Track shown reminders to prevent duplicate notifications
2025-11-23 21:12:21 +01:00
683c4ddafe Complete internal code renaming from FocusTask to Immerse types
- Renamed FocusTask → ImmerseTask
- Renamed FocusTaskSettings → ImmerseSettings
- Renamed FocusTaskData → ImmerseData
- Updated CSS classes: focus-task-status-* → immerse-status-*
- Updated package release script zip filename
- Updated remaining README references
- Rebuilt main.js with new type names
2025-11-23 20:12:28 +01:00
331a2b41df Release v1.0.9: Rename plugin from Focus Task to Immerse 2025-11-23 19:56:06 +01:00
364935af66 Updated README.md 2025-11-23 16:02:14 +01:00
2f2346b4c8 Release v1.0.8: Timer Accuracy Fix 2025-11-23 15:49:08 +01:00
c8f5c69102 General Bugfixes 2025-11-23 15:33:51 +01:00
2800a7507e Release v1.0.7: General bugfixes 2025-11-23 15:06:12 +01:00
2f861c2fcb Release v1.0.6: Background Timer Fix 2025-11-23 13:15:45 +01:00
9abdd10ada Release v1.0.5: Break Timer & Time Tracking Improvements 2025-11-23 11:49:45 +01:00
e66d9b4d25 General Bug fixes on Timers/Breaks 2025-11-23 11:46:13 +01:00
285ac7a7c9 Updated README.md 2025-11-23 11:45:37 +01:00
9e35a2603c Updated README.md 2025-11-22 20:48:35 +01:00
f5e4a16aff Bug Fixes - Updated Version v1.0.4 2025-11-22 20:31:57 +01:00
9014f086d6 Bug Fixes 2025-11-22 20:26:48 +01:00
d4f2af179f Bug Fixes 2025-11-22 20:26:18 +01:00
aeb1d62895 Bug fixes 2025-11-22 18:05:01 +01:00
9de2b00a2f Bug Fixes 2025-11-22 18:03:37 +01:00
66652afc90 Bug Fixes 2025-11-22 17:53:41 +01:00
ad5b5e81bb Updated README.md 2025-11-22 17:10:27 +01:00
951e9c5406 Added Feature - Daily note summary 2025-11-22 17:01:41 +01:00
6dc4d2952d Added Feature - Daily note summary 2025-11-22 17:01:12 +01:00
8e10724206 Updated Version 2025-11-22 16:57:32 +01:00
d661466c81 Added Feature - Daily note summary 2025-11-22 16:56:25 +01:00
f634993637 Updated README.md 2025-11-22 16:54:14 +01:00
1a0c37d642 Updated Version 2025-11-22 16:31:15 +01:00
5a33e641b1 Fixed Focus time not adding time properly 2025-11-22 16:28:44 +01:00
7b4e2e674a Updated README.md 2025-11-22 14:59:53 +01:00
13 changed files with 3844 additions and 484 deletions

15
.gitignore vendored
View File

@@ -1,5 +1,8 @@
# Build output
# Build output (main.js is tracked for easy installation)
*.js.map
release/
*.zip
immerse-*.zip
# npm
node_modules/
@@ -15,4 +18,12 @@ package-lock.json
Thumbs.db
# Obsidian
data.json
data.json
# Development/Documentation (not for distribution)
RELEASE-GUIDE.md
ROADMAP.md
ROADMAP_*.md
deploy-test.bat
.claude/
RELEASE_NOTES_*.md

113
README.md
View File

@@ -1,16 +1,16 @@
# ⚡ Focus Task
# ⚡ Immerse
A powerful task management and focus timer plugin for [Obsidian](https://obsidian.md), heavily inspired by [Blitzit](https://www.blitzit.app/).
![Focus Task Banner](https://img.shields.io/badge/Obsidian-Plugin-7c3aed?style=for-the-badge&logo=obsidian&logoColor=white)
![Immerse Banner](https://img.shields.io/badge/Obsidian-Plugin-7c3aed?style=for-the-badge&logo=obsidian&logoColor=white)
![License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge)
![Version](https://img.shields.io/badge/Version-1.0.0-blue?style=for-the-badge)
![Version](https://img.shields.io/badge/Version-1.1.0-blue?style=for-the-badge)
## 🎯 Overview
Focus Task brings the power of time-boxed task management directly into your Obsidian vault. Plan your day, track time with the Pomodoro technique, and crush your tasks with satisfying checkoffs - all without leaving your notes.
Immerse brings the power of time-boxed task management directly into your Obsidian vault. Plan your day, track time with the Pomodoro technique, and crush your tasks with satisfying checkoffs - all without leaving your notes.
### Why Focus Task?
### Why Immerse?
- **Stay in Flow**: No need to switch between apps - manage tasks where you take notes
- **Time Awareness**: Know exactly how long tasks take vs. your estimates
@@ -46,9 +46,33 @@ Focus Task brings the power of time-boxed task management directly into your Obs
- **Streak Counter**: Build momentum with consecutive productive days
- **Time Comparison**: Compare estimated vs. actual time to improve planning
- **Pomodoro Count**: Track total pomodoros completed
- **Daily Note Logging**: Automatically log completed tasks to your daily notes with timestamps and performance metrics
### 📈 Reporting & Analytics (New in v1.1.0!)
- **Comprehensive Reports**: View detailed productivity reports with date range filtering
- **Key Metrics Dashboard**: Track tasks done, tasks per day, hours per day, minutes per task, and day streaks
- **Visual Analytics**: Pie charts showing task distribution and time allocation
- **Time by List**: See how much time you spend on different task categories
- **Productivity Insights**: Discover your most productive hour, day, and month
- **Daily Breakdown**: Visual bar charts showing last 10 days of activity
- **Quick Filters**: Today, Last 7/30/90 days for easy report generation
### 🗓️ Task Scheduling & Reminders (New in v1.1.0!)
- **Schedule Tasks**: Set specific date and time for tasks
- **Smart Reminders**: Get notifications before task is due (5/10/15/30/60 minute options)
- **Overdue Detection**: Visual indicators (⚠️ red badge) for past-due tasks
- **Startup Checks**: Alerts when opening Obsidian if tasks are overdue
- **Background Monitoring**: Automatic 30-second checks for due tasks
- **Sound Alerts**: Optional audio notifications for reminders
### 📱 Mobile Optimized (New in v1.1.0!)
- **Responsive Design**: Fully optimized for mobile screens (tablets and phones)
- **Touch-Friendly**: Larger buttons and tap targets (44px minimum)
- **Adaptive Layouts**: Charts and visualizations scale appropriately
- **Mobile Testing**: Works great on Obsidian mobile app
### 🎨 User Experience
- **Status Bar Timer**: timer that stays visible while you work
- **Status Bar Timer**: Timer that stays visible while you work
- **Celebration Messages**: Fun, randomized messages when you complete tasks
- **Sound Notifications**: Audio alerts for timer completion and task completion
- **Keyboard Shortcuts**: Quick access to common actions
@@ -59,20 +83,23 @@ Focus Task brings the power of time-boxed task management directly into your Obs
### From Obsidian Community Plugins (Coming Soon)
1. Open Obsidian Settings
2. Go to Community Plugins
3. Search for "Focus Task"
3. Search for "Immerse"
4. Click Install, then Enable
### Manual Installation
1. Download the latest release from the [releases page](https://git.cribdev.com/crib/focus-task/releases)
2. Extract the files to your vault's `.obsidian/plugins/focus-task/` folder
3. Reload Obsidian
4. Enable the plugin in Settings → Community Plugins
1. Download the latest release from the [releases page](https://git.cribdev.com/crib/immerse/releases)
2. Extract the files to your vault's `.obsidian/plugins/immerse/` folder
3. **⚠️ IMPORTANT**: When updating, do NOT replace or delete the existing `data.json` file - this contains all your tasks, settings, and progress!
4. Reload Obsidian
5. Enable the plugin in Settings → Community Plugins
> **Note**: Only copy the three plugin files (`main.js`, `manifest.json`, `styles.css`) when updating. Your `data.json` file stores all your tasks and settings and should never be replaced.
### Building from Source
```bash
# Clone the repository
git clone https://git.cribdev.com/crib/focus-task.git
cd focus-task
git clone https://git.cribdev.com/crib/immerse.git
cd immerse
# Install dependencies
npm install
@@ -81,14 +108,14 @@ npm install
npm run build
# Copy to your vault
cp main.js manifest.json styles.css /path/to/your/vault/.obsidian/plugins/focus-task/
cp main.js manifest.json styles.css /path/to/your/vault/.obsidian/plugins/immerse/
```
## 📖 Usage
### Getting Started
1. **Open Focus Task**: Click the ⚡ icon in the ribbon or use the command palette (`Ctrl/Cmd + P` → "Open Focus Task Panel")
1. **Open Immerse**: Click the ⚡ icon in the ribbon or use the command palette (`Ctrl/Cmd + P` → "Open Immerse Panel")
2. **Add a Task**: Click "+ Add Task" and fill in:
- Task description
@@ -103,7 +130,7 @@ cp main.js manifest.json styles.css /path/to/your/vault/.obsidian/plugins/focus-
| Action | Command |
|--------|---------|
| Open Panel | `Ctrl/Cmd + P` → "Open Focus Task Panel" |
| Open Panel | `Ctrl/Cmd + P` → "Open Immerse Panel" |
| Quick Add Task | `Ctrl/Cmd + P` → "Quick Add Task" |
| Toggle Timer | `Ctrl/Cmd + P` → "Toggle Timer" |
| Complete Task | `Ctrl/Cmd + P` → "Complete Current Task" |
@@ -136,7 +163,7 @@ Best for: Tracking time on open-ended tasks
| Short Break | Length of short breaks | 5 min |
| Long Break | Length of long breaks | 15 min |
| Long Break Interval | Pomodoros before a long break | 4 |
| Auto-start Breaks | Automatically start break timer | Off |
| Auto-start Breaks | Automatically start break timer | On |
### General
| Setting | Description | Default |
@@ -146,6 +173,27 @@ Best for: Tracking time on open-ended tasks
| Enable Celebrations | Show celebration messages | On |
| Show Floating Timer | Display draggable timer widget | On |
### Daily Note Integration
| Setting | Description | Default |
|---------|-------------|---------|
| Log to Daily Note | Automatically log completed tasks to your daily note | Off |
When enabled, completed tasks are automatically appended to your daily note with:
- Task name and list category
- Time spent vs. estimated time
- Completion timestamp
- Performance indicator (under/over/on target)
**Example entry:**
```
- [x] Write project proposal | 💼 Work | ⏱️ 45min / 30min (15min over estimate) | ✅ 14:30
```
**Requirements:** The core "Daily Notes" plugin must be enabled in Obsidian settings. Immerse respects your Daily Notes configuration (folder, date format, and template).
### Lists
Customize your task lists with:
- Custom names
@@ -157,18 +205,18 @@ Default lists: Work 💼, Personal 🏠, Learning 📚
## 🎨 Customization
### Adding Custom Lists
1. Go to Settings → Focus Task → Lists
1. Go to Settings → Immerse → Lists
2. Click "+ Add List"
3. Set the name, emoji, and color
4. Click Save
### Theming
Focus Task respects your Obsidian theme and adapts to both light and dark modes automatically.
Immerse respects your Obsidian theme and adapts to both light and dark modes automatically.
## 📁 Project Structure
```
focus-task/
immerse/
├── src/
│ ├── main.ts # Main plugin class
│ ├── types.ts # TypeScript interfaces
@@ -193,7 +241,7 @@ Blitzit's approach to productivity resonated with me:
- Satisfying task completion experience
- Progress insights
I wanted to bring this experience directly into Obsidian, where I already manage my notes and knowledge. Focus Task is my attempt to capture the essence of what makes Blitzit great while leveraging Obsidian's powerful ecosystem.
I wanted to bring this experience directly into Obsidian, where I already manage my notes and knowledge. Immerse is my attempt to capture the essence of what makes Blitzit great while leveraging Obsidian's powerful ecosystem.
**If you're looking for a dedicated productivity app, I highly recommend checking out [Blitzit](https://www.blitzit.app/)!**
@@ -211,8 +259,8 @@ Contributions are welcome! Feel free to:
```bash
# Clone the repo
git clone https://git.cribdev.com/crib/focus-task.git
cd focus-task
git clone https://git.cribdev.com/crib/immerse.git
cd immerse
# Install dependencies
npm install
@@ -221,28 +269,17 @@ npm install
npm run dev
# Create symlink to your test vault
ln -s $(pwd) /path/to/vault/.obsidian/plugins/focus-task
ln -s $(pwd) /path/to/vault/.obsidian/plugins/immerse
```
## 📝 Roadmap
- [ ] Integration with Obsidian Tasks plugin
- [ ] Calendar view for scheduled tasks
- [ ] Weekly/Monthly reports
- [ ] Task templates
- [ ] Sync with external task managers
- [ ] Mobile optimizations
- [ ] Task dependencies
- [ ] Time blocking in daily notes
## 📜 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 🔗 Links
- **Repository**: [https://git.cribdev.com/crib/focus-task](https://git.cribdev.com/crib/focus-task)
- **Issues**: [https://git.cribdev.com/crib/focus-task/issues](https://git.cribdev.com/crib/focus-task/issues)
- **Repository**: [https://git.cribdev.com/crib/immerse](https://git.cribdev.com/crib/immerse)
- **Issues**: [https://git.cribdev.com/crib/immerse/issues](https://git.cribdev.com/crib/immerse/issues)
- **Inspiration**: [Blitzit App](https://www.blitzit.app/)
---
@@ -251,4 +288,6 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
Made with ❤️ for the Obsidian community
<br>
Inspired by <a href="https://www.blitzit.app/">Blitzit</a> ⚡
<br><br>
<em>✨ Vibe coded with assistance from <a href="https://claude.ai">Claude.ai</a></em>
</p>

1147
main.js

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"id": "focus-task",
"name": "Focus Task",
"version": "1.0.1",
"id": "immerse",
"name": "Immerse",
"version": "1.1.1",
"minAppVersion": "0.15.0",
"description": "A Blitzit-inspired task management and focus timer plugin. Plan your day, track time with Pomodoro technique, and crush your tasks with satisfying checkoffs.",
"author": "Crib",

62
package-release.mjs Normal file
View File

@@ -0,0 +1,62 @@
import { promises as fs } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const RELEASE_DIR = 'release';
const FILES_TO_INCLUDE = [
'main.js',
'manifest.json',
'styles.css'
];
async function packageRelease() {
try {
console.log('📦 Packaging release files...\n');
// Create release directory if it doesn't exist
try {
await fs.access(RELEASE_DIR);
console.log(`✓ Release directory exists: ${RELEASE_DIR}`);
} catch {
await fs.mkdir(RELEASE_DIR);
console.log(`✓ Created release directory: ${RELEASE_DIR}`);
}
// Copy each file
for (const file of FILES_TO_INCLUDE) {
const sourcePath = path.join(__dirname, file);
const destPath = path.join(__dirname, RELEASE_DIR, file);
try {
await fs.copyFile(sourcePath, destPath);
console.log(`✓ Copied ${file}`);
} catch (error) {
console.error(`✗ Failed to copy ${file}:`, error.message);
process.exit(1);
}
}
// Read manifest to get version
const manifestPath = path.join(__dirname, 'manifest.json');
const manifestContent = await fs.readFile(manifestPath, 'utf8');
const manifest = JSON.parse(manifestContent);
console.log(`\n✅ Release package created successfully!`);
console.log(`📁 Location: ./${RELEASE_DIR}/`);
console.log(`📌 Version: ${manifest.version}`);
console.log(`\nFiles included:`);
FILES_TO_INCLUDE.forEach(file => console.log(` - ${file}`));
console.log(`\n💡 Tip: You can now upload these files from the '${RELEASE_DIR}' directory to your Gitea release.`);
console.log(`💡 Tip: To create a zip, run: cd ${RELEASE_DIR} && zip -r ../immerse-${manifest.version}.zip *`);
} catch (error) {
console.error('❌ Error packaging release:', error);
process.exit(1);
}
}
packageRelease();

View File

@@ -1,11 +1,12 @@
{
"name": "focus-task",
"version": "1.0.1",
"name": "immerse",
"version": "1.1.1",
"description": "A Blitzit-inspired task management and focus timer plugin for Obsidian",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"package": "npm run build && node package-release.mjs",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [
@@ -21,7 +22,7 @@
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.cribdev.com/crib/focus-task"
"url": "https://git.cribdev.com/crib/immerse"
},
"devDependencies": {
"@types/node": "^16.11.6",

File diff suppressed because it is too large Load Diff

View File

@@ -5,21 +5,25 @@ import {
Setting,
} from 'obsidian';
import { FocusTask } from './types';
import FocusTaskPlugin from './main';
import { ImmerseTask } from './types';
import ImmersePlugin from './main';
// ============ Quick Add Task Modal ============
export class QuickAddTaskModal extends Modal {
plugin: FocusTaskPlugin;
plugin: ImmersePlugin;
taskText: string = '';
estimatedMinutes: number;
selectedList: string = 'work';
scheduledDate: string = '';
scheduledTime: string = '';
reminderMinutes: number = 0;
constructor(app: App, plugin: FocusTaskPlugin) {
constructor(app: App, plugin: ImmersePlugin) {
super(app);
this.plugin = plugin;
this.estimatedMinutes = plugin.settings.defaultEstimateMinutes;
this.reminderMinutes = plugin.settings.defaultReminderMinutes;
if (plugin.settings.lists.length > 0) {
this.selectedList = plugin.settings.lists[0].id;
}
@@ -27,7 +31,7 @@ export class QuickAddTaskModal extends Modal {
onOpen() {
const { contentEl } = this;
contentEl.addClass('focus-task-modal');
contentEl.addClass('immerse-modal');
contentEl.createEl('h2', { text: '⚡ Add New Task' });
@@ -82,19 +86,68 @@ export class QuickAddTaskModal extends Modal {
dropdown.onChange(value => this.selectedList = value);
});
// Scheduled date
new Setting(contentEl)
.setName('📅 Scheduled Date')
.setDesc('Optional: When do you plan to work on this?')
.addText(text => {
text.setPlaceholder('YYYY-MM-DD')
.setValue(this.scheduledDate)
.onChange(value => this.scheduledDate = value);
text.inputEl.type = 'date';
});
// Scheduled time
new Setting(contentEl)
.setName('⏰ Scheduled Time')
.setDesc('Optional: What time?')
.addText(text => {
text.setPlaceholder('HH:mm')
.setValue(this.scheduledTime)
.onChange(value => this.scheduledTime = value);
text.inputEl.type = 'time';
});
// Reminder
if (this.plugin.settings.enableReminders) {
new Setting(contentEl)
.setName('🔔 Reminder')
.setDesc('Remind me before the scheduled time')
.addDropdown(dropdown => {
dropdown.addOption('0', 'No reminder');
dropdown.addOption('5', '5 minutes before');
dropdown.addOption('10', '10 minutes before');
dropdown.addOption('15', '15 minutes before');
dropdown.addOption('30', '30 minutes before');
dropdown.addOption('60', '1 hour before');
dropdown.setValue(this.reminderMinutes.toString());
dropdown.onChange(value => this.reminderMinutes = parseInt(value));
});
}
// Buttons
const buttonContainer = contentEl.createEl('div', { cls: 'focus-task-modal-buttons' });
const buttonContainer = contentEl.createEl('div', { cls: 'immerse-modal-buttons' });
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel', cls: 'focus-task-btn' });
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel', cls: 'immerse-btn' });
cancelBtn.addEventListener('click', () => this.close());
const addBtn = buttonContainer.createEl('button', { text: 'Add Task', cls: 'focus-task-btn focus-task-btn-primary' });
const addBtn = buttonContainer.createEl('button', { text: 'Add Task', cls: 'immerse-btn immerse-btn-primary' });
addBtn.addEventListener('click', () => this.submitTask());
}
submitTask() {
if (this.taskText.trim()) {
const task = this.plugin.createTask(this.taskText, this.estimatedMinutes, this.selectedList);
// Add scheduling data if provided
if (this.scheduledDate) {
task.scheduledDate = this.scheduledDate;
}
if (this.scheduledTime) {
task.scheduledTime = this.scheduledTime;
}
if (this.reminderMinutes > 0 && this.scheduledDate && this.scheduledTime) {
task.reminderMinutes = this.reminderMinutes;
}
this.plugin.addTask(task);
new Notice('✅ Task added!');
this.close();
@@ -112,10 +165,10 @@ export class QuickAddTaskModal extends Modal {
// ============ Edit Task Modal ============
export class EditTaskModal extends Modal {
plugin: FocusTaskPlugin;
task: FocusTask;
plugin: ImmersePlugin;
task: ImmerseTask;
constructor(app: App, plugin: FocusTaskPlugin, task: FocusTask) {
constructor(app: App, plugin: ImmersePlugin, task: ImmerseTask) {
super(app);
this.plugin = plugin;
this.task = { ...task };
@@ -123,7 +176,7 @@ export class EditTaskModal extends Modal {
onOpen() {
const { contentEl } = this;
contentEl.addClass('focus-task-modal');
contentEl.addClass('immerse-modal');
contentEl.createEl('h2', { text: '✏️ Edit Task' });
@@ -177,6 +230,48 @@ export class EditTaskModal extends Modal {
textarea.inputEl.rows = 4;
});
// Scheduled date
new Setting(contentEl)
.setName('📅 Scheduled Date')
.setDesc('Optional: When do you plan to work on this?')
.addText(text => {
text.setPlaceholder('YYYY-MM-DD')
.setValue(this.task.scheduledDate || '')
.onChange(value => this.task.scheduledDate = value || undefined);
text.inputEl.type = 'date';
});
// Scheduled time
new Setting(contentEl)
.setName('⏰ Scheduled Time')
.setDesc('Optional: What time?')
.addText(text => {
text.setPlaceholder('HH:mm')
.setValue(this.task.scheduledTime || '')
.onChange(value => this.task.scheduledTime = value || undefined);
text.inputEl.type = 'time';
});
// Reminder
if (this.plugin.settings.enableReminders) {
new Setting(contentEl)
.setName('🔔 Reminder')
.setDesc('Remind me before the scheduled time')
.addDropdown(dropdown => {
dropdown.addOption('0', 'No reminder');
dropdown.addOption('5', '5 minutes before');
dropdown.addOption('10', '10 minutes before');
dropdown.addOption('15', '15 minutes before');
dropdown.addOption('30', '30 minutes before');
dropdown.addOption('60', '1 hour before');
dropdown.setValue((this.task.reminderMinutes || 0).toString());
dropdown.onChange(value => {
const minutes = parseInt(value);
this.task.reminderMinutes = minutes > 0 ? minutes : undefined;
});
});
}
// Show actual time if task has been worked on
if (this.task.actualMinutes > 0) {
new Setting(contentEl)
@@ -184,12 +279,12 @@ export class EditTaskModal extends Modal {
.setDesc(`You've worked on this task for ${this.plugin.formatTimeHuman(this.task.actualMinutes)}`);
}
const buttonContainer = contentEl.createEl('div', { cls: 'focus-task-modal-buttons' });
const buttonContainer = contentEl.createEl('div', { cls: 'immerse-modal-buttons' });
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel', cls: 'focus-task-btn' });
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel', cls: 'immerse-btn' });
cancelBtn.addEventListener('click', () => this.close());
const saveBtn = buttonContainer.createEl('button', { text: 'Save', cls: 'focus-task-btn focus-task-btn-primary' });
const saveBtn = buttonContainer.createEl('button', { text: 'Save', cls: 'immerse-btn immerse-btn-primary' });
saveBtn.addEventListener('click', () => {
this.plugin.updateTask(this.task.id, this.task);
new Notice('✅ Task updated!');
@@ -197,6 +292,257 @@ export class EditTaskModal extends Modal {
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
// ============ Report Modal ============
export class ReportModal extends Modal {
plugin: ImmersePlugin;
startDate: string;
endDate: string;
selectedListIds: string[] = [];
constructor(app: App, plugin: ImmersePlugin) {
super(app);
this.plugin = plugin;
// Default to last 7 days
const today = new Date();
this.endDate = today.toISOString().split('T')[0];
const weekAgo = new Date(today);
weekAgo.setDate(weekAgo.getDate() - 7);
this.startDate = weekAgo.toISOString().split('T')[0];
}
onOpen() {
const { contentEl } = this;
contentEl.addClass('immerse-modal', 'immerse-report-modal');
contentEl.empty();
// Header
const header = contentEl.createEl('div', { cls: 'immerse-report-header' });
header.createEl('h2', { text: '📊 Reports', cls: 'immerse-report-title' });
// Filters section
this.renderFilters(contentEl);
// Generate button
const generateBtn = contentEl.createEl('button', {
text: '🔄 Generate Report',
cls: 'immerse-btn immerse-btn-primary immerse-report-generate-btn'
});
generateBtn.addEventListener('click', () => this.renderReport(contentEl));
// Initial report render
this.renderReport(contentEl);
}
renderFilters(container: Element) {
const filtersSection = container.createEl('div', { cls: 'immerse-report-filters' });
// Date range
const dateRow = filtersSection.createEl('div', { cls: 'immerse-report-filter-row' });
new Setting(dateRow)
.setName('Start Date')
.addText(text => {
text.setValue(this.startDate)
.onChange(value => this.startDate = value);
text.inputEl.type = 'date';
});
new Setting(dateRow)
.setName('End Date')
.addText(text => {
text.setValue(this.endDate)
.onChange(value => this.endDate = value);
text.inputEl.type = 'date';
});
// Quick filters
const quickFilters = filtersSection.createEl('div', { cls: 'immerse-report-quick-filters' });
quickFilters.createEl('span', { text: 'Quick select: ', cls: 'immerse-filter-label' });
const filters = [
{ label: 'Today', days: 0 },
{ label: 'Last 7 days', days: 7 },
{ label: 'Last 30 days', days: 30 },
{ label: 'Last 90 days', days: 90 },
];
filters.forEach(filter => {
const btn = quickFilters.createEl('button', {
text: filter.label,
cls: 'immerse-quick-filter-btn'
});
btn.addEventListener('click', () => {
const today = new Date();
this.endDate = today.toISOString().split('T')[0];
const startDate = new Date(today);
startDate.setDate(startDate.getDate() - filter.days);
this.startDate = startDate.toISOString().split('T')[0];
this.renderReport(container);
});
});
}
renderReport(container: Element) {
// Remove old report if exists
const oldReport = container.querySelector('.immerse-report-content');
if (oldReport) oldReport.remove();
// Generate report data
const filters: import('./types').ReportFilters = {
startDate: this.startDate,
endDate: this.endDate,
listIds: this.selectedListIds.length > 0 ? this.selectedListIds : undefined,
};
const reportData = this.plugin.generateReport(filters);
// Create report content
const reportContent = container.createEl('div', { cls: 'immerse-report-content' });
// Check if we have data
if (reportData.totalTasks === 0) {
reportContent.createEl('div', {
text: 'No data available for the selected period. Complete some tasks to see your stats!',
cls: 'immerse-no-data-message'
});
return;
}
// Summary stats
this.renderSummaryStats(reportContent, reportData);
// Time by list (donut chart)
if (reportData.timeByList.length > 0) {
this.renderTimeByList(reportContent, reportData);
}
// Productivity insights
this.renderInsights(reportContent, reportData);
// Daily breakdown (bar chart - simplified text version)
if (reportData.dailyBreakdown.length > 0) {
this.renderDailyBreakdown(reportContent, reportData);
}
}
renderSummaryStats(container: Element, data: import('./types').ReportData) {
const statsGrid = container.createEl('div', { cls: 'immerse-stats-grid' });
const stats = [
{ label: 'TASKS DONE', value: data.totalTasks.toString(), icon: '✓' },
{ label: 'TASKS PER DAY', value: data.tasksPerDay.toFixed(1), icon: '📅' },
{ label: 'HOURS PER DAY', value: data.hoursPerDay.toFixed(1), icon: '⏰' },
{ label: 'MINS PER TASK', value: data.minsPerTask.toString(), icon: '⏱️' },
{ label: 'DAY STREAK', value: data.currentStreak.toString(), icon: '🔥' },
{ label: 'TOTAL HOURS', value: (data.totalMinutes / 60).toFixed(1), icon: '⌚' },
];
stats.forEach(stat => {
const statCard = statsGrid.createEl('div', { cls: 'immerse-stat-card' });
statCard.createEl('div', { text: stat.label, cls: 'immerse-stat-label' });
const valueRow = statCard.createEl('div', { cls: 'immerse-stat-value-row' });
valueRow.createEl('span', { text: stat.icon, cls: 'immerse-stat-icon' });
valueRow.createEl('span', { text: stat.value, cls: 'immerse-stat-value' });
});
}
renderTimeByList(container: Element, data: import('./types').ReportData) {
const section = container.createEl('div', { cls: 'immerse-report-section' });
section.createEl('h3', { text: 'Time by List', cls: 'immerse-report-section-title' });
const listContainer = section.createEl('div', { cls: 'immerse-time-by-list' });
data.timeByList.forEach(item => {
const listItem = listContainer.createEl('div', { cls: 'immerse-list-stat-item' });
// List info
const listInfo = listItem.createEl('div', { cls: 'immerse-list-info' });
listInfo.createEl('span', { text: item.listIcon, cls: 'immerse-list-icon' });
listInfo.createEl('span', { text: item.listName, cls: 'immerse-list-name' });
// Progress bar
const progressBar = listItem.createEl('div', { cls: 'immerse-list-progress' });
const progress = progressBar.createEl('div', { cls: 'immerse-list-progress-fill' });
progress.style.width = `${item.percentage}%`;
progress.style.backgroundColor = item.listColor;
// Stats
const stats = listItem.createEl('div', { cls: 'immerse-list-stats' });
stats.createEl('span', {
text: `${item.taskCount} tasks`,
cls: 'immerse-list-stat-text'
});
stats.createEl('span', {
text: `${this.plugin.formatTimeHuman(item.minutes)}`,
cls: 'immerse-list-stat-text'
});
stats.createEl('span', {
text: `${item.percentage.toFixed(1)}%`,
cls: 'immerse-list-stat-percentage'
});
});
}
renderInsights(container: Element, data: import('./types').ReportData) {
const section = container.createEl('div', { cls: 'immerse-report-section' });
section.createEl('h3', { text: 'Productivity Insights', cls: 'immerse-report-section-title' });
const insightsGrid = section.createEl('div', { cls: 'immerse-insights-grid' });
if (data.mostProductiveHour !== undefined) {
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
card.createEl('div', { text: 'MOST PRODUCTIVE HOUR', cls: 'immerse-insight-label' });
card.createEl('div', {
text: `${data.mostProductiveHour}:00 - ${data.mostProductiveHour + 1}:00`,
cls: 'immerse-insight-value'
});
}
if (data.mostProductiveDay) {
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
card.createEl('div', { text: 'MOST PRODUCTIVE DAY', cls: 'immerse-insight-label' });
card.createEl('div', { text: data.mostProductiveDay, cls: 'immerse-insight-value' });
}
if (data.mostProductiveMonth) {
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
card.createEl('div', { text: 'MOST PRODUCTIVE MONTH', cls: 'immerse-insight-label' });
card.createEl('div', { text: data.mostProductiveMonth, cls: 'immerse-insight-value' });
}
}
renderDailyBreakdown(container: Element, data: import('./types').ReportData) {
const section = container.createEl('div', { cls: 'immerse-report-section' });
section.createEl('h3', { text: 'Daily Breakdown', cls: 'immerse-report-section-title' });
const table = section.createEl('table', { cls: 'immerse-daily-table' });
const thead = table.createEl('thead');
const headerRow = thead.createEl('tr');
headerRow.createEl('th', { text: 'Date' });
headerRow.createEl('th', { text: 'Tasks' });
headerRow.createEl('th', { text: 'Hours' });
headerRow.createEl('th', { text: 'Pomodoros' });
const tbody = table.createEl('tbody');
// Show last 14 days max
const recentData = data.dailyBreakdown.slice(-14);
recentData.forEach(day => {
const row = tbody.createEl('tr');
row.createEl('td', { text: day.date });
row.createEl('td', { text: day.tasks.toString() });
row.createEl('td', { text: day.hours.toFixed(1) });
row.createEl('td', { text: day.pomodoros.toString() });
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();

394
src/reportView.ts Normal file
View File

@@ -0,0 +1,394 @@
import {
ItemView,
WorkspaceLeaf,
Setting,
} from 'obsidian';
import { ReportFilters, ReportData } from './types';
import ImmersePlugin from './main';
export const VIEW_TYPE_REPORT = 'immerse-report-view';
export class ReportView extends ItemView {
plugin: ImmersePlugin;
startDate: string;
endDate: string;
selectedListIds: string[] = [];
constructor(leaf: WorkspaceLeaf, plugin: ImmersePlugin) {
super(leaf);
this.plugin = plugin;
// Default to last 7 days
const today = new Date();
this.endDate = today.toISOString().split('T')[0];
const weekAgo = new Date(today);
weekAgo.setDate(weekAgo.getDate() - 7);
this.startDate = weekAgo.toISOString().split('T')[0];
}
getViewType(): string {
return VIEW_TYPE_REPORT;
}
getDisplayText(): string {
return '📊 Reports';
}
getIcon(): string {
return 'bar-chart-2';
}
async onOpen() {
const container = this.containerEl.children[1];
container.empty();
container.addClass('immerse-report-view');
this.renderContent();
}
async onClose() {
// Nothing to clean up
}
renderContent() {
const container = this.containerEl.children[1];
container.empty();
// Header
const header = container.createEl('div', { cls: 'immerse-report-header' });
header.createEl('h2', { text: '📊 Reports', cls: 'immerse-report-title' });
// Filters section
this.renderFilters(container);
// Generate button
const generateBtn = container.createEl('button', {
text: '🔄 Generate Report',
cls: 'immerse-btn immerse-btn-primary immerse-report-generate-btn'
});
generateBtn.addEventListener('click', () => this.renderReport(container));
// Initial report render
this.renderReport(container);
}
renderFilters(container: Element) {
const filtersSection = container.createEl('div', { cls: 'immerse-report-filters' });
// Date range
const dateRow = filtersSection.createEl('div', { cls: 'immerse-report-filter-row' });
new Setting(dateRow)
.setName('Start Date')
.addText(text => {
text.setValue(this.startDate)
.onChange(value => this.startDate = value);
text.inputEl.type = 'date';
});
new Setting(dateRow)
.setName('End Date')
.addText(text => {
text.setValue(this.endDate)
.onChange(value => this.endDate = value);
text.inputEl.type = 'date';
});
// Quick filters
const quickFilters = filtersSection.createEl('div', { cls: 'immerse-report-quick-filters' });
quickFilters.createEl('span', { text: 'Quick select: ', cls: 'immerse-filter-label' });
const filters = [
{ label: 'Today', days: 0 },
{ label: 'Last 7 days', days: 7 },
{ label: 'Last 30 days', days: 30 },
{ label: 'Last 90 days', days: 90 },
];
filters.forEach(filter => {
const btn = quickFilters.createEl('button', {
text: filter.label,
cls: 'immerse-quick-filter-btn'
});
btn.addEventListener('click', () => {
const today = new Date();
this.endDate = today.toISOString().split('T')[0];
const startDate = new Date(today);
startDate.setDate(startDate.getDate() - filter.days);
this.startDate = startDate.toISOString().split('T')[0];
this.renderReport(container);
});
});
}
renderReport(container: Element) {
// Remove old report if exists
const oldReport = container.querySelector('.immerse-report-content');
if (oldReport) oldReport.remove();
// Generate report data
const filters: ReportFilters = {
startDate: this.startDate,
endDate: this.endDate,
listIds: this.selectedListIds.length > 0 ? this.selectedListIds : undefined,
};
const reportData = this.plugin.generateReport(filters);
// Create report content
const reportContent = container.createEl('div', { cls: 'immerse-report-content' });
// Check if we have data
if (reportData.totalTasks === 0) {
reportContent.createEl('div', {
text: 'No data available for the selected period. Complete some tasks to see your stats!',
cls: 'immerse-no-data-message'
});
return;
}
// Summary stats
this.renderSummaryStats(reportContent, reportData);
// Time by list
if (reportData.timeByList.length > 0) {
this.renderTimeByList(reportContent, reportData);
}
// Productivity insights
this.renderInsights(reportContent, reportData);
// Daily breakdown
if (reportData.dailyBreakdown.length > 0) {
this.renderDailyBreakdown(reportContent, reportData);
}
}
renderSummaryStats(container: Element, data: ReportData) {
const statsGrid = container.createEl('div', { cls: 'immerse-stats-grid' });
const stats = [
{ label: 'TASKS DONE', value: data.totalTasks.toString(), icon: '✓' },
{ label: 'TASKS PER DAY', value: data.tasksPerDay.toFixed(1), icon: '📅' },
{ label: 'HOURS PER DAY', value: data.hoursPerDay.toFixed(1), icon: '⏰' },
{ label: 'MINS PER TASK', value: data.minsPerTask.toString(), icon: '⏱️' },
{ label: 'DAY STREAK', value: data.currentStreak.toString(), icon: '🔥' },
{ label: 'TOTAL HOURS', value: (data.totalMinutes / 60).toFixed(1), icon: '⌚' },
];
stats.forEach(stat => {
const statCard = statsGrid.createEl('div', { cls: 'immerse-stat-card' });
statCard.createEl('div', { text: stat.label, cls: 'immerse-stat-label' });
const valueRow = statCard.createEl('div', { cls: 'immerse-stat-value-row' });
valueRow.createEl('span', { text: stat.icon, cls: 'immerse-stat-icon' });
valueRow.createEl('span', { text: stat.value, cls: 'immerse-stat-value' });
});
}
renderTimeByList(container: Element, data: ReportData) {
const section = container.createEl('div', { cls: 'immerse-report-section' });
section.createEl('h3', { text: 'Time by List', cls: 'immerse-report-section-title' });
const listContainer = section.createEl('div', { cls: 'immerse-time-by-list' });
data.timeByList.forEach(item => {
const listItem = listContainer.createEl('div', { cls: 'immerse-list-stat-item' });
// List info
const listInfo = listItem.createEl('div', { cls: 'immerse-list-info' });
listInfo.createEl('span', { text: item.listIcon, cls: 'immerse-list-icon' });
listInfo.createEl('span', { text: item.listName, cls: 'immerse-list-name' });
// Progress bar
const progressBar = listItem.createEl('div', { cls: 'immerse-list-progress' });
const progress = progressBar.createEl('div', { cls: 'immerse-list-progress-fill' });
progress.style.width = `${item.percentage}%`;
progress.style.background = item.listColor;
// Stats
const stats = listItem.createEl('div', { cls: 'immerse-list-stats' });
stats.createEl('span', {
text: `${item.taskCount} tasks`,
cls: 'immerse-list-stat-text'
});
stats.createEl('span', {
text: `${this.plugin.formatTimeHuman(item.minutes)}`,
cls: 'immerse-list-stat-text'
});
stats.createEl('span', {
text: `${item.percentage.toFixed(1)}%`,
cls: 'immerse-list-stat-percentage'
});
});
}
renderInsights(container: Element, data: ReportData) {
const section = container.createEl('div', { cls: 'immerse-report-section' });
section.createEl('h3', { text: 'Productivity Insights', cls: 'immerse-report-section-title' });
const insightsGrid = section.createEl('div', { cls: 'immerse-insights-grid' });
if (data.mostProductiveHour !== undefined) {
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
card.createEl('span', { text: '🕐', cls: 'immerse-insight-icon' });
card.createEl('span', { text: 'MOST PRODUCTIVE HOUR', cls: 'immerse-insight-label' });
card.createEl('span', {
text: `${data.mostProductiveHour}:00 - ${data.mostProductiveHour + 1}:00`,
cls: 'immerse-insight-value'
});
}
if (data.mostProductiveDay) {
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
card.createEl('span', { text: '📅', cls: 'immerse-insight-icon' });
card.createEl('span', { text: 'MOST PRODUCTIVE DAY', cls: 'immerse-insight-label' });
card.createEl('span', { text: data.mostProductiveDay, cls: 'immerse-insight-value' });
}
if (data.mostProductiveMonth) {
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
card.createEl('span', { text: '🗓️', cls: 'immerse-insight-icon' });
card.createEl('span', { text: 'MOST PRODUCTIVE MONTH', cls: 'immerse-insight-label' });
card.createEl('span', { text: data.mostProductiveMonth, cls: 'immerse-insight-value' });
}
}
renderPieChart(container: Element, data: ReportData) {
const pieContainer = container.createEl('div', { cls: 'immerse-daily-pie-container' });
// Calculate totals - normalize to minutes for fair comparison
const totalTasks = data.totalTasks;
const totalMinutes = data.totalMinutes;
const totalPomodoros = data.totalPomodoros;
// For pie chart, let's use minutes as the base unit
// Assume average task takes data.minsPerTask minutes
const taskMinutes = totalTasks * data.minsPerTask;
const pomodoroMinutes = totalPomodoros * 25; // Standard pomodoro is 25 minutes
const totalTime = taskMinutes + totalMinutes + pomodoroMinutes;
if (totalTime === 0) return;
// Calculate percentages based on time
const tasksPercent = (taskMinutes / totalTime) * 100;
const hoursPercent = (totalMinutes / totalTime) * 100;
const pomodorosPercent = (pomodoroMinutes / totalTime) * 100;
// Calculate degrees for conic gradient
const tasksDeg = (tasksPercent / 100) * 360;
const hoursDeg = tasksDeg + (hoursPercent / 100) * 360;
// Create pie chart with segments
const pieChart = pieContainer.createEl('div', { cls: 'immerse-daily-pie-chart' });
// Build conic-gradient string with explicit color stops
const gradient = `conic-gradient(from 0deg, #6366f1 0deg ${tasksDeg}deg, #22c55e ${tasksDeg}deg ${hoursDeg}deg, #f59e0b ${hoursDeg}deg 360deg)`;
pieChart.style.background = gradient;
// Center circle with total
const center = pieChart.createEl('div', { cls: 'immerse-daily-pie-center' });
center.createEl('div', { text: data.totalTasks.toString(), cls: 'immerse-daily-pie-center-value' });
center.createEl('div', { text: 'TOTAL TASKS', cls: 'immerse-daily-pie-center-label' });
// Legend
const legend = pieContainer.createEl('div', { cls: 'immerse-daily-pie-legend' });
// Tasks legend item
const tasksItem = legend.createEl('div', { cls: 'immerse-daily-pie-legend-item' });
const tasksColor = tasksItem.createEl('div', { cls: 'immerse-daily-pie-legend-color' });
tasksColor.style.background = '#6366f1';
const tasksInfo = tasksItem.createEl('div', { cls: 'immerse-daily-pie-legend-info' });
const tasksLabel = tasksInfo.createEl('div', { cls: 'immerse-daily-pie-legend-label' });
tasksLabel.createEl('span', { text: '✓' });
tasksLabel.appendText('Tasks Completed');
tasksInfo.createEl('div', { text: totalTasks.toString(), cls: 'immerse-daily-pie-legend-value' });
tasksInfo.createEl('div', { text: `${tasksPercent.toFixed(1)}%`, cls: 'immerse-daily-pie-legend-percentage' });
// Hours legend item
const hoursItem = legend.createEl('div', { cls: 'immerse-daily-pie-legend-item' });
const hoursColor = hoursItem.createEl('div', { cls: 'immerse-daily-pie-legend-color' });
hoursColor.style.background = '#22c55e';
const hoursInfo = hoursItem.createEl('div', { cls: 'immerse-daily-pie-legend-info' });
const hoursLabel = hoursInfo.createEl('div', { cls: 'immerse-daily-pie-legend-label' });
hoursLabel.createEl('span', { text: '⏱️' });
hoursLabel.appendText('Total Hours');
hoursInfo.createEl('div', { text: (totalMinutes / 60).toFixed(1), cls: 'immerse-daily-pie-legend-value' });
hoursInfo.createEl('div', { text: `${hoursPercent.toFixed(1)}%`, cls: 'immerse-daily-pie-legend-percentage' });
// Pomodoros legend item
const pomodorosItem = legend.createEl('div', { cls: 'immerse-daily-pie-legend-item' });
const pomodorosColor = pomodorosItem.createEl('div', { cls: 'immerse-daily-pie-legend-color' });
pomodorosColor.style.background = '#f59e0b';
const pomodorosInfo = pomodorosItem.createEl('div', { cls: 'immerse-daily-pie-legend-info' });
const pomodorosLabel = pomodorosInfo.createEl('div', { cls: 'immerse-daily-pie-legend-label' });
pomodorosLabel.createEl('span', { text: '🍅' });
pomodorosLabel.appendText('Pomodoros');
pomodorosInfo.createEl('div', { text: totalPomodoros.toString(), cls: 'immerse-daily-pie-legend-value' });
pomodorosInfo.createEl('div', { text: `${pomodorosPercent.toFixed(1)}%`, cls: 'immerse-daily-pie-legend-percentage' });
}
renderDailyBreakdown(container: Element, data: ReportData) {
const section = container.createEl('div', { cls: 'immerse-report-section' });
section.createEl('h3', { text: 'Daily Breakdown', cls: 'immerse-report-section-title' });
// Add pie chart for overall summary
this.renderPieChart(section, data);
const breakdownContainer = section.createEl('div', { cls: 'immerse-daily-breakdown-container' });
// Show last 10 days max
const recentData = data.dailyBreakdown.slice(-10);
// Find max values for scaling bars
const maxTasks = Math.max(...recentData.map(d => d.tasks), 1);
const maxHours = Math.max(...recentData.map(d => d.hours), 1);
const maxPomodoros = Math.max(...recentData.map(d => d.pomodoros), 1);
recentData.forEach(day => {
const row = breakdownContainer.createEl('div', { cls: 'immerse-daily-row' });
// Date column with formatted date
const dateEl = row.createEl('div', { cls: 'immerse-daily-date' });
const date = new Date(day.date + 'T00:00:00');
const dayName = date.toLocaleDateString('en-US', { weekday: 'short' }).toUpperCase();
const monthDay = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
dateEl.createEl('div', { text: dayName, cls: 'immerse-daily-date-day' });
dateEl.createEl('div', { text: monthDay, cls: 'immerse-daily-date-num' });
// Bars column
const barsContainer = row.createEl('div', { cls: 'immerse-daily-bars' });
// Tasks bar
const tasksRow = barsContainer.createEl('div', { cls: 'immerse-daily-bar-row' });
const tasksLabel = tasksRow.createEl('span', { cls: 'immerse-daily-bar-label' });
tasksLabel.createEl('span', { text: '✓', cls: 'immerse-daily-bar-icon' });
tasksLabel.appendText('Tasks');
const tasksTrack = tasksRow.createEl('div', { cls: 'immerse-daily-bar-track' });
const tasksFill = tasksTrack.createEl('div', { cls: 'immerse-daily-bar-fill tasks' });
tasksFill.style.width = `${(day.tasks / maxTasks) * 100}%`;
tasksRow.createEl('span', { text: day.tasks.toString(), cls: 'immerse-daily-bar-value' });
// Hours bar
const hoursRow = barsContainer.createEl('div', { cls: 'immerse-daily-bar-row' });
const hoursLabel = hoursRow.createEl('span', { cls: 'immerse-daily-bar-label' });
hoursLabel.createEl('span', { text: '⏱️', cls: 'immerse-daily-bar-icon' });
hoursLabel.appendText('Hours');
const hoursTrack = hoursRow.createEl('div', { cls: 'immerse-daily-bar-track' });
const hoursFill = hoursTrack.createEl('div', { cls: 'immerse-daily-bar-fill hours' });
hoursFill.style.width = `${(day.hours / maxHours) * 100}%`;
hoursRow.createEl('span', { text: day.hours.toFixed(1), cls: 'immerse-daily-bar-value' });
// Pomodoros bar
const pomodorosRow = barsContainer.createEl('div', { cls: 'immerse-daily-bar-row' });
const pomodorosLabel = pomodorosRow.createEl('span', { cls: 'immerse-daily-bar-label' });
pomodorosLabel.createEl('span', { text: '🍅', cls: 'immerse-daily-bar-icon' });
pomodorosLabel.appendText('Pomodoros');
const pomodorosTrack = pomodorosRow.createEl('div', { cls: 'immerse-daily-bar-track' });
const pomodorosFill = pomodorosTrack.createEl('div', { cls: 'immerse-daily-bar-fill pomodoros' });
pomodorosFill.style.width = `${(day.pomodoros / maxPomodoros) * 100}%`;
pomodorosRow.createEl('span', { text: day.pomodoros.toString(), cls: 'immerse-daily-bar-value' });
});
}
}

View File

@@ -1,6 +1,6 @@
// ============ Types & Interfaces ============
export interface FocusTask {
export interface ImmerseTask {
id: string;
text: string;
completed: boolean;
@@ -10,7 +10,9 @@ export interface FocusTask {
completedAt?: number;
list: string;
notes: string;
scheduledDate?: string;
scheduledDate?: string; // Date in YYYY-MM-DD format
scheduledTime?: string; // Time in HH:mm format (24-hour)
reminderMinutes?: number; // Minutes before scheduled time to remind (0 = no reminder)
isActive: boolean;
}
@@ -21,7 +23,7 @@ export interface TaskList {
icon: string;
}
export interface FocusTaskSettings {
export interface ImmerseSettings {
pomodoroWorkMinutes: number;
pomodoroBreakMinutes: number;
longBreakMinutes: number;
@@ -32,18 +34,49 @@ export interface FocusTaskSettings {
lists: TaskList[];
autoStartBreak: boolean;
tickSoundEnabled: boolean;
// Daily note logging
logToDaily: boolean;
// Task reminders
enableReminders: boolean;
defaultReminderMinutes: number; // Default minutes before task to remind
}
export interface FocusTaskData {
tasks: FocusTask[];
// Daily statistics snapshot
export interface DailyStats {
date: string; // YYYY-MM-DD format
tasksCompleted: number;
totalMinutes: number;
pomodorosCompleted: number;
tasksByList: Record<string, number>; // listId -> count
minutesByList: Record<string, number>; // listId -> minutes
}
// Archived completed task (for historical reporting)
export interface CompletedTaskRecord {
id: string;
text: string;
list: string;
estimatedMinutes: number;
actualMinutes: number;
createdAt: number;
completedAt: number;
scheduledDate?: string;
wasOverdue: boolean; // was it completed after scheduled time?
}
export interface ImmerseData {
tasks: ImmerseTask[];
completedToday: number;
totalFocusMinutesToday: number;
streak: number;
lastActiveDate: string;
pomodorosCompleted: number;
// Historical data for reporting
dailyStats: DailyStats[]; // Array of daily statistics
completedTasksArchive: CompletedTaskRecord[]; // All completed tasks history
}
export const DEFAULT_SETTINGS: FocusTaskSettings = {
export const DEFAULT_SETTINGS: ImmerseSettings = {
pomodoroWorkMinutes: 25,
pomodoroBreakMinutes: 5,
longBreakMinutes: 15,
@@ -56,20 +89,70 @@ export const DEFAULT_SETTINGS: FocusTaskSettings = {
{ id: 'personal', name: 'Personal', color: '#22c55e', icon: '🏠' },
{ id: 'learning', name: 'Learning', color: '#f59e0b', icon: '📚' },
],
autoStartBreak: false,
autoStartBreak: true,
tickSoundEnabled: false,
// Daily note logging
logToDaily: false,
// Task reminders
enableReminders: true,
defaultReminderMinutes: 15,
};
export const DEFAULT_DATA: FocusTaskData = {
export const DEFAULT_DATA: ImmerseData = {
tasks: [],
completedToday: 0,
totalFocusMinutesToday: 0,
streak: 0,
lastActiveDate: '',
pomodorosCompleted: 0,
dailyStats: [],
completedTasksArchive: [],
};
export const VIEW_TYPE_FOCUS_TASK = 'focus-task-view';
export const VIEW_TYPE_IMMERSE = 'immerse-view';
// ============ Reporting Types ============
export interface ReportFilters {
startDate: string; // YYYY-MM-DD
endDate: string; // YYYY-MM-DD
listIds?: string[]; // Filter by specific lists (undefined = all)
}
export interface ReportData {
// Summary stats
totalTasks: number;
totalMinutes: number;
totalPomodoros: number;
tasksPerDay: number;
hoursPerDay: number;
minsPerTask: number;
currentStreak: number;
// Time breakdown by list
timeByList: Array<{
listId: string;
listName: string;
listIcon: string;
listColor: string;
minutes: number;
taskCount: number;
percentage: number;
}>;
// Daily breakdown for charts
dailyBreakdown: Array<{
date: string; // YYYY-MM-DD
tasks: number;
hours: number;
pomodoros: number;
}>;
// Productivity insights
mostProductiveHour?: number; // 0-23
mostProductiveDay?: string; // day name
mostProductiveMonth?: string; // month name
}
// ============ Celebration Messages ============
@@ -96,4 +179,4 @@ export const OVERTIME_MESSAGES = [
{ emoji: '💪', message: 'Persistence pays off!' },
{ emoji: '🏃', message: 'Marathon runner!' },
{ emoji: '🔥', message: 'The grind is real!' },
];
];

View File

@@ -3,33 +3,33 @@ import {
WorkspaceLeaf,
} from 'obsidian';
import { VIEW_TYPE_FOCUS_TASK, FocusTask } from './types';
import { VIEW_TYPE_IMMERSE, ImmerseTask } from './types';
import { QuickAddTaskModal, EditTaskModal } from './modals';
import FocusTaskPlugin from './main';
import ImmersePlugin from './main';
// ============ Main View ============
export class FocusTaskView extends ItemView {
plugin: FocusTaskPlugin;
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: FocusTaskPlugin) {
constructor(leaf: WorkspaceLeaf, plugin: ImmersePlugin) {
super(leaf);
this.plugin = plugin;
}
getViewType(): string {
return VIEW_TYPE_FOCUS_TASK;
return VIEW_TYPE_IMMERSE;
}
getDisplayText(): string {
return 'Focus Task';
return 'Immerse';
}
getIcon(): string {
@@ -74,7 +74,7 @@ export class FocusTaskView extends ItemView {
refresh() {
const container = this.containerEl.children[1];
container.empty();
container.addClass('focus-task-container');
container.addClass('immerse-container');
// Reset element references
this.timerTimeEl = null;
@@ -96,18 +96,24 @@ export class FocusTaskView extends ItemView {
}
renderHeader(container: Element) {
const header = container.createEl('div', { cls: 'focus-task-header' });
const header = container.createEl('div', { cls: 'immerse-header' });
const titleSection = header.createEl('div', { cls: 'focus-task-title-section' });
titleSection.createEl('h2', { text: '⚡ Focus Task', cls: 'focus-task-title' });
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: 'focus-task-date' });
titleSection.createEl('div', { text: dateStr, cls: 'immerse-date' });
const actions = header.createEl('div', { cls: 'focus-task-header-actions' });
const addBtn = actions.createEl('button', { cls: 'focus-task-btn focus-task-btn-primary' });
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();
@@ -116,52 +122,54 @@ export class FocusTaskView extends ItemView {
renderStatsBar(container: Element) {
const stats = this.plugin.getStats();
const statsBar = container.createEl('div', { cls: 'focus-task-stats-bar' });
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: 'Focus Time', value: this.plugin.formatTimeHuman(stats.totalFocusMinutesToday), 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: 'focus-task-stat-item' });
item.createEl('div', { cls: 'focus-task-stat-icon', text: stat.icon });
item.createEl('div', { cls: 'focus-task-stat-value', text: stat.value });
item.createEl('div', { cls: 'focus-task-stat-label', text: stat.label });
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: 'focus-task-active-section' });
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('focus-task-has-active');
activeSection.addClass('immerse-has-active');
const activeCard = activeSection.createEl('div', { cls: 'focus-task-active-card' });
const activeCard = activeSection.createEl('div', { cls: 'immerse-active-card' });
if (this.plugin.isBreakMode) {
activeCard.addClass('focus-task-break-card');
activeCard.createEl('div', { cls: 'focus-task-active-label', text: '☕ BREAK TIME' });
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 {
activeCard.createEl('div', { cls: 'focus-task-active-label', text: '🎯 FOCUSING ON' });
const workLabel = this.plugin.currentTimerSeconds > 0 ? '🎯 FOCUSING ON' : '🍅 POMODORO COMPLETE';
activeCard.createEl('div', { cls: 'immerse-active-label', text: workLabel });
}
activeCard.createEl('div', { cls: 'focus-task-active-task-name', text: task.text });
activeCard.createEl('div', { cls: 'immerse-active-task-name', text: task.text });
// Timer display - store reference for updates
const timerDisplay = activeCard.createEl('div', { cls: 'focus-task-timer-display' });
const timerDisplay = activeCard.createEl('div', { cls: 'immerse-timer-display' });
this.timerTimeEl = timerDisplay.createEl('span', {
cls: 'focus-task-timer-time',
cls: 'immerse-timer-time',
text: this.plugin.formatTime(this.plugin.currentTimerSeconds)
});
// Progress bar - store reference for updates
const progressWrap = activeCard.createEl('div', { cls: 'focus-task-progress-wrap' });
this.progressBarEl = progressWrap.createEl('div', { cls: 'focus-task-progress-bar' });
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) {
@@ -175,56 +183,104 @@ export class FocusTaskView extends ItemView {
}
this.progressBarEl.style.width = `${Math.min(Math.max(progressPercent, 0), 100)}%`;
if (progressPercent >= 100) this.progressBarEl.addClass('focus-task-overtime');
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: 'focus-task-time-info' });
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: 'focus-task-active-controls' });
this.pauseBtnEl = controls.createEl('button', { cls: 'focus-task-btn focus-task-btn-secondary' });
this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? '⏸ Pause' : '▶ Resume';
this.pauseBtnEl.addEventListener('click', () => this.plugin.toggleTimer());
if (!this.plugin.isBreakMode) {
const completeBtn = controls.createEl('button', { cls: 'focus-task-btn focus-task-btn-success' });
completeBtn.innerHTML = '✓ Complete';
completeBtn.addEventListener('click', () => this.plugin.completeTask(task.id));
}
const stopBtn = controls.createEl('button', { cls: 'focus-task-btn focus-task-btn-danger' });
stopBtn.innerHTML = '✕ Stop';
stopBtn.addEventListener('click', () => this.plugin.stopTimer());
const controls = activeCard.createEl('div', { cls: 'immerse-active-controls' });
if (this.plugin.isBreakMode) {
const skipBreakBtn = controls.createEl('button', { cls: 'focus-task-btn' });
skipBreakBtn.innerHTML = '⏭ Skip Break';
skipBreakBtn.addEventListener('click', () => {
this.plugin.isBreakMode = false;
this.plugin.stopTimer();
this.refresh();
});
// 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) {
// Work session still running
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 {
// Work 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: 'focus-task-start-prompt' });
startPrompt.createEl('div', { cls: 'focus-task-prompt-icon', text: '⚡' });
startPrompt.createEl('div', { cls: 'focus-task-prompt-text', text: 'Ready to focus?' });
startPrompt.createEl('div', { cls: 'focus-task-prompt-hint', text: 'Click ▶ on a task to start a Pomodoro session' });
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: 'focus-task-list-section' });
const listSection = container.createEl('div', { cls: 'immerse-list-section' });
// Filters
const filters = listSection.createEl('div', { cls: 'focus-task-filters' });
const filters = listSection.createEl('div', { cls: 'immerse-filters' });
const filterOptions = [
{ id: 'all', label: 'All Tasks' },
@@ -235,7 +291,7 @@ export class FocusTaskView extends ItemView {
filterOptions.forEach(opt => {
const btn = filters.createEl('button', {
cls: `focus-task-filter-btn ${this.currentFilter === opt.id ? 'active' : ''}`,
cls: `immerse-filter-btn ${this.currentFilter === opt.id ? 'active' : ''}`,
text: opt.label,
});
btn.addEventListener('click', () => {
@@ -245,7 +301,7 @@ export class FocusTaskView extends ItemView {
});
// Task items
const taskList = listSection.createEl('div', { cls: 'focus-task-task-list' });
const taskList = listSection.createEl('div', { cls: 'immerse-task-list' });
let tasks = this.plugin.data.tasks;
@@ -265,24 +321,28 @@ export class FocusTaskView extends ItemView {
});
if (tasks.length === 0) {
const emptyState = taskList.createEl('div', { cls: 'focus-task-empty-state' });
emptyState.createEl('div', { cls: 'focus-task-empty-icon', text: '📝' });
emptyState.createEl('div', { cls: 'focus-task-empty-text', text: 'No tasks yet' });
emptyState.createEl('div', { cls: 'focus-task-empty-hint', text: 'Add a task to get started!' });
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: FocusTask) {
renderTaskItem(container: Element, task: ImmerseTask) {
const list = this.plugin.settings.lists.find(l => l.id === task.list);
const taskEl = container.createEl('div', {
cls: `focus-task-task-item ${task.completed ? 'completed' : ''} ${task.isActive ? 'active' : ''}`
// 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: 'focus-task-checkbox' });
const checkbox = taskEl.createEl('div', { cls: 'immerse-checkbox' });
checkbox.innerHTML = task.completed ? '✓' : '';
checkbox.style.borderColor = list?.color || '#6366f1';
if (task.completed) {
@@ -297,37 +357,54 @@ export class FocusTaskView extends ItemView {
});
// Task content
const content = taskEl.createEl('div', { cls: 'focus-task-task-content' });
const content = taskEl.createEl('div', { cls: 'immerse-task-content' });
const taskHeader = content.createEl('div', { cls: 'focus-task-task-header' });
taskHeader.createEl('span', { cls: 'focus-task-task-text', text: task.text });
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: 'focus-task-list-badge',
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: 'focus-task-task-meta' });
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('focus-task-overtime-text');
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: 'focus-task-task-actions' });
const actions = taskEl.createEl('div', { cls: 'immerse-task-actions' });
if (!task.completed) {
// Start pomodoro button
const startBtn = actions.createEl('button', { cls: 'focus-task-task-btn', attr: { 'aria-label': 'Start Pomodoro' } });
const startBtn = actions.createEl('button', { cls: 'immerse-task-btn', attr: { 'aria-label': 'Start Pomodoro' } });
startBtn.innerHTML = '▶';
startBtn.addEventListener('click', (e) => {
e.stopPropagation();
@@ -335,7 +412,7 @@ export class FocusTaskView extends ItemView {
});
// Stopwatch mode button
const stopwatchBtn = actions.createEl('button', { cls: 'focus-task-task-btn', attr: { 'aria-label': 'Start Stopwatch' } });
const stopwatchBtn = actions.createEl('button', { cls: 'immerse-task-btn', attr: { 'aria-label': 'Start Stopwatch' } });
stopwatchBtn.innerHTML = '⏱';
stopwatchBtn.addEventListener('click', (e) => {
e.stopPropagation();
@@ -344,7 +421,7 @@ export class FocusTaskView extends ItemView {
}
// Edit button
const editBtn = actions.createEl('button', { cls: 'focus-task-task-btn', attr: { 'aria-label': 'Edit' } });
const editBtn = actions.createEl('button', { cls: 'immerse-task-btn', attr: { 'aria-label': 'Edit' } });
editBtn.innerHTML = '✏️';
editBtn.addEventListener('click', (e) => {
e.stopPropagation();
@@ -352,7 +429,7 @@ export class FocusTaskView extends ItemView {
});
// Delete button
const deleteBtn = actions.createEl('button', { cls: 'focus-task-task-btn focus-task-delete-btn', attr: { 'aria-label': 'Delete' } });
const deleteBtn = actions.createEl('button', { cls: 'immerse-task-btn immerse-delete-btn', attr: { 'aria-label': 'Delete' } });
deleteBtn.innerHTML = '🗑';
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();

1009
styles.css

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,8 @@
{
"1.0.1": "0.15.0"
"1.0.4": "0.15.0",
"1.0.5": "0.15.0",
"1.0.6": "0.15.0",
"1.0.7": "0.15.0",
"1.0.8": "0.15.0",
"1.0.9": "0.15.0"
}