34 Commits

Author SHA1 Message Date
886a2f7372 fix: Button text overflow in stopwatch mode controls
- Fixed button text overflowing in active task controls
- Changed flex behavior to allow proper text wrapping
- Increased minimum button width from 80px to 100px
- Added word-wrap and line-height for multi-line text support
- Buttons now properly contain text like 'Continue Working'

Fixes button overflow issue in stopwatch mode where text would
extend outside button boundaries instead of wrapping.
2025-11-25 17:11:57 +01:00
68e11c57e1 v1.1.2 - Bug fixes: Fixed Text on buttons to wrap and adapt properly 2025-11-25 17:07:14 +01:00
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 3851 additions and 486 deletions

15
.gitignore vendored
View File

@@ -1,5 +1,8 @@
# Build output # Build output (main.js is tracked for easy installation)
*.js.map *.js.map
release/
*.zip
immerse-*.zip
# npm # npm
node_modules/ node_modules/
@@ -15,4 +18,12 @@ package-lock.json
Thumbs.db Thumbs.db
# Obsidian # 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/). 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) ![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 ## 🎯 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 - **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 - **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 - **Streak Counter**: Build momentum with consecutive productive days
- **Time Comparison**: Compare estimated vs. actual time to improve planning - **Time Comparison**: Compare estimated vs. actual time to improve planning
- **Pomodoro Count**: Track total pomodoros completed - **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 ### 🎨 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 - **Celebration Messages**: Fun, randomized messages when you complete tasks
- **Sound Notifications**: Audio alerts for timer completion and task completion - **Sound Notifications**: Audio alerts for timer completion and task completion
- **Keyboard Shortcuts**: Quick access to common actions - **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) ### From Obsidian Community Plugins (Coming Soon)
1. Open Obsidian Settings 1. Open Obsidian Settings
2. Go to Community Plugins 2. Go to Community Plugins
3. Search for "Focus Task" 3. Search for "Immerse"
4. Click Install, then Enable 4. Click Install, then Enable
### Manual Installation ### Manual Installation
1. Download the latest release from the [releases page](https://git.cribdev.com/crib/focus-task/releases) 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/focus-task/` folder 2. Extract the files to your vault's `.obsidian/plugins/immerse/` folder
3. Reload Obsidian 3. **⚠️ IMPORTANT**: When updating, do NOT replace or delete the existing `data.json` file - this contains all your tasks, settings, and progress!
4. Enable the plugin in Settings → Community Plugins 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 ### Building from Source
```bash ```bash
# Clone the repository # Clone the repository
git clone https://git.cribdev.com/crib/focus-task.git git clone https://git.cribdev.com/crib/immerse.git
cd focus-task cd immerse
# Install dependencies # Install dependencies
npm install npm install
@@ -81,14 +108,14 @@ npm install
npm run build npm run build
# Copy to your vault # 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 ## 📖 Usage
### Getting Started ### 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: 2. **Add a Task**: Click "+ Add Task" and fill in:
- Task description - Task description
@@ -103,7 +130,7 @@ cp main.js manifest.json styles.css /path/to/your/vault/.obsidian/plugins/focus-
| Action | Command | | 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" | | Quick Add Task | `Ctrl/Cmd + P` → "Quick Add Task" |
| Toggle Timer | `Ctrl/Cmd + P` → "Toggle Timer" | | Toggle Timer | `Ctrl/Cmd + P` → "Toggle Timer" |
| Complete Task | `Ctrl/Cmd + P` → "Complete Current Task" | | 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 | | Short Break | Length of short breaks | 5 min |
| Long Break | Length of long breaks | 15 min | | Long Break | Length of long breaks | 15 min |
| Long Break Interval | Pomodoros before a long break | 4 | | 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 ### General
| Setting | Description | Default | | Setting | Description | Default |
@@ -146,6 +173,27 @@ Best for: Tracking time on open-ended tasks
| Enable Celebrations | Show celebration messages | On | | Enable Celebrations | Show celebration messages | On |
| Show Floating Timer | Display draggable timer widget | 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 ### Lists
Customize your task lists with: Customize your task lists with:
- Custom names - Custom names
@@ -157,18 +205,18 @@ Default lists: Work 💼, Personal 🏠, Learning 📚
## 🎨 Customization ## 🎨 Customization
### Adding Custom Lists ### Adding Custom Lists
1. Go to Settings → Focus Task → Lists 1. Go to Settings → Immerse → Lists
2. Click "+ Add List" 2. Click "+ Add List"
3. Set the name, emoji, and color 3. Set the name, emoji, and color
4. Click Save 4. Click Save
### Theming ### 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 ## 📁 Project Structure
``` ```
focus-task/ immerse/
├── src/ ├── src/
│ ├── main.ts # Main plugin class │ ├── main.ts # Main plugin class
│ ├── types.ts # TypeScript interfaces │ ├── types.ts # TypeScript interfaces
@@ -193,7 +241,7 @@ Blitzit's approach to productivity resonated with me:
- Satisfying task completion experience - Satisfying task completion experience
- Progress insights - 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/)!** **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 ```bash
# Clone the repo # Clone the repo
git clone https://git.cribdev.com/crib/focus-task.git git clone https://git.cribdev.com/crib/immerse.git
cd focus-task cd immerse
# Install dependencies # Install dependencies
npm install npm install
@@ -221,28 +269,17 @@ npm install
npm run dev npm run dev
# Create symlink to your test vault # 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 ## 📜 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 🔗 Links ## 🔗 Links
- **Repository**: [https://git.cribdev.com/crib/focus-task](https://git.cribdev.com/crib/focus-task) - **Repository**: [https://git.cribdev.com/crib/immerse](https://git.cribdev.com/crib/immerse)
- **Issues**: [https://git.cribdev.com/crib/focus-task/issues](https://git.cribdev.com/crib/focus-task/issues) - **Issues**: [https://git.cribdev.com/crib/immerse/issues](https://git.cribdev.com/crib/immerse/issues)
- **Inspiration**: [Blitzit App](https://www.blitzit.app/) - **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 Made with ❤️ for the Obsidian community
<br> <br>
Inspired by <a href="https://www.blitzit.app/">Blitzit</a> ⚡ 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> </p>

1147
main.js

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"id": "focus-task", "id": "immerse",
"name": "Focus Task", "name": "Immerse",
"version": "1.0.1", "version": "1.1.2",
"minAppVersion": "0.15.0", "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.", "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", "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", "name": "immerse",
"version": "1.0.1", "version": "1.1.2",
"description": "A Blitzit-inspired task management and focus timer plugin for Obsidian", "description": "A Blitzit-inspired task management and focus timer plugin for Obsidian",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"dev": "node esbuild.config.mjs", "dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "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" "version": "node version-bump.mjs && git add manifest.json versions.json"
}, },
"keywords": [ "keywords": [
@@ -21,7 +22,7 @@
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.cribdev.com/crib/focus-task" "url": "https://git.cribdev.com/crib/immerse"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^16.11.6", "@types/node": "^16.11.6",

File diff suppressed because it is too large Load Diff

View File

@@ -5,21 +5,25 @@ import {
Setting, Setting,
} from 'obsidian'; } from 'obsidian';
import { FocusTask } from './types'; import { ImmerseTask } from './types';
import FocusTaskPlugin from './main'; import ImmersePlugin from './main';
// ============ Quick Add Task Modal ============ // ============ Quick Add Task Modal ============
export class QuickAddTaskModal extends Modal { export class QuickAddTaskModal extends Modal {
plugin: FocusTaskPlugin; plugin: ImmersePlugin;
taskText: string = ''; taskText: string = '';
estimatedMinutes: number; estimatedMinutes: number;
selectedList: string = 'work'; selectedList: string = 'work';
scheduledDate: string = '';
scheduledTime: string = '';
reminderMinutes: number = 0;
constructor(app: App, plugin: FocusTaskPlugin) { constructor(app: App, plugin: ImmersePlugin) {
super(app); super(app);
this.plugin = plugin; this.plugin = plugin;
this.estimatedMinutes = plugin.settings.defaultEstimateMinutes; this.estimatedMinutes = plugin.settings.defaultEstimateMinutes;
this.reminderMinutes = plugin.settings.defaultReminderMinutes;
if (plugin.settings.lists.length > 0) { if (plugin.settings.lists.length > 0) {
this.selectedList = plugin.settings.lists[0].id; this.selectedList = plugin.settings.lists[0].id;
} }
@@ -27,7 +31,7 @@ export class QuickAddTaskModal extends Modal {
onOpen() { onOpen() {
const { contentEl } = this; const { contentEl } = this;
contentEl.addClass('focus-task-modal'); contentEl.addClass('immerse-modal');
contentEl.createEl('h2', { text: '⚡ Add New Task' }); contentEl.createEl('h2', { text: '⚡ Add New Task' });
@@ -82,19 +86,68 @@ export class QuickAddTaskModal extends Modal {
dropdown.onChange(value => this.selectedList = value); 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 // 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()); 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()); addBtn.addEventListener('click', () => this.submitTask());
} }
submitTask() { submitTask() {
if (this.taskText.trim()) { if (this.taskText.trim()) {
const task = this.plugin.createTask(this.taskText, this.estimatedMinutes, this.selectedList); 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); this.plugin.addTask(task);
new Notice('✅ Task added!'); new Notice('✅ Task added!');
this.close(); this.close();
@@ -112,10 +165,10 @@ export class QuickAddTaskModal extends Modal {
// ============ Edit Task Modal ============ // ============ Edit Task Modal ============
export class EditTaskModal extends Modal { export class EditTaskModal extends Modal {
plugin: FocusTaskPlugin; plugin: ImmersePlugin;
task: FocusTask; task: ImmerseTask;
constructor(app: App, plugin: FocusTaskPlugin, task: FocusTask) { constructor(app: App, plugin: ImmersePlugin, task: ImmerseTask) {
super(app); super(app);
this.plugin = plugin; this.plugin = plugin;
this.task = { ...task }; this.task = { ...task };
@@ -123,7 +176,7 @@ export class EditTaskModal extends Modal {
onOpen() { onOpen() {
const { contentEl } = this; const { contentEl } = this;
contentEl.addClass('focus-task-modal'); contentEl.addClass('immerse-modal');
contentEl.createEl('h2', { text: '✏️ Edit Task' }); contentEl.createEl('h2', { text: '✏️ Edit Task' });
@@ -177,6 +230,48 @@ export class EditTaskModal extends Modal {
textarea.inputEl.rows = 4; 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 // Show actual time if task has been worked on
if (this.task.actualMinutes > 0) { if (this.task.actualMinutes > 0) {
new Setting(contentEl) 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)}`); .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()); 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', () => { saveBtn.addEventListener('click', () => {
this.plugin.updateTask(this.task.id, this.task); this.plugin.updateTask(this.task.id, this.task);
new Notice('✅ Task updated!'); 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() { onClose() {
const { contentEl } = this; const { contentEl } = this;
contentEl.empty(); 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 ============ // ============ Types & Interfaces ============
export interface FocusTask { export interface ImmerseTask {
id: string; id: string;
text: string; text: string;
completed: boolean; completed: boolean;
@@ -10,7 +10,9 @@ export interface FocusTask {
completedAt?: number; completedAt?: number;
list: string; list: string;
notes: 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; isActive: boolean;
} }
@@ -21,7 +23,7 @@ export interface TaskList {
icon: string; icon: string;
} }
export interface FocusTaskSettings { export interface ImmerseSettings {
pomodoroWorkMinutes: number; pomodoroWorkMinutes: number;
pomodoroBreakMinutes: number; pomodoroBreakMinutes: number;
longBreakMinutes: number; longBreakMinutes: number;
@@ -32,18 +34,49 @@ export interface FocusTaskSettings {
lists: TaskList[]; lists: TaskList[];
autoStartBreak: boolean; autoStartBreak: boolean;
tickSoundEnabled: boolean; tickSoundEnabled: boolean;
// Daily note logging
logToDaily: boolean;
// Task reminders
enableReminders: boolean;
defaultReminderMinutes: number; // Default minutes before task to remind
} }
export interface FocusTaskData { // Daily statistics snapshot
tasks: FocusTask[]; 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; completedToday: number;
totalFocusMinutesToday: number; totalFocusMinutesToday: number;
streak: number; streak: number;
lastActiveDate: string; lastActiveDate: string;
pomodorosCompleted: number; 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, pomodoroWorkMinutes: 25,
pomodoroBreakMinutes: 5, pomodoroBreakMinutes: 5,
longBreakMinutes: 15, longBreakMinutes: 15,
@@ -56,20 +89,70 @@ export const DEFAULT_SETTINGS: FocusTaskSettings = {
{ id: 'personal', name: 'Personal', color: '#22c55e', icon: '🏠' }, { id: 'personal', name: 'Personal', color: '#22c55e', icon: '🏠' },
{ id: 'learning', name: 'Learning', color: '#f59e0b', icon: '📚' }, { id: 'learning', name: 'Learning', color: '#f59e0b', icon: '📚' },
], ],
autoStartBreak: false, autoStartBreak: true,
tickSoundEnabled: false, tickSoundEnabled: false,
// Daily note logging
logToDaily: false,
// Task reminders
enableReminders: true,
defaultReminderMinutes: 15,
}; };
export const DEFAULT_DATA: FocusTaskData = { export const DEFAULT_DATA: ImmerseData = {
tasks: [], tasks: [],
completedToday: 0, completedToday: 0,
totalFocusMinutesToday: 0, totalFocusMinutesToday: 0,
streak: 0, streak: 0,
lastActiveDate: '', lastActiveDate: '',
pomodorosCompleted: 0, 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 ============ // ============ Celebration Messages ============
@@ -96,4 +179,4 @@ export const OVERTIME_MESSAGES = [
{ emoji: '💪', message: 'Persistence pays off!' }, { emoji: '💪', message: 'Persistence pays off!' },
{ emoji: '🏃', message: 'Marathon runner!' }, { emoji: '🏃', message: 'Marathon runner!' },
{ emoji: '🔥', message: 'The grind is real!' }, { emoji: '🔥', message: 'The grind is real!' },
]; ];

View File

@@ -3,33 +3,33 @@ import {
WorkspaceLeaf, WorkspaceLeaf,
} from 'obsidian'; } from 'obsidian';
import { VIEW_TYPE_FOCUS_TASK, FocusTask } from './types'; import { VIEW_TYPE_IMMERSE, ImmerseTask } from './types';
import { QuickAddTaskModal, EditTaskModal } from './modals'; import { QuickAddTaskModal, EditTaskModal } from './modals';
import FocusTaskPlugin from './main'; import ImmersePlugin from './main';
// ============ Main View ============ // ============ Main View ============
export class FocusTaskView extends ItemView { export class ImmerseView extends ItemView {
plugin: FocusTaskPlugin; plugin: ImmersePlugin;
currentFilter: string = 'all'; currentFilter: string = 'all';
// References to elements that need frequent updates // References to elements that need frequent updates
private timerTimeEl: HTMLElement | null = null; private timerTimeEl: HTMLElement | null = null;
private progressBarEl: HTMLElement | null = null; private progressBarEl: HTMLElement | null = null;
private actualTimeEl: HTMLElement | null = null; private actualTimeEl: HTMLElement | null = null;
private pauseBtnEl: HTMLElement | null = null; private pauseBtnEl: HTMLElement | null = null;
constructor(leaf: WorkspaceLeaf, plugin: FocusTaskPlugin) { constructor(leaf: WorkspaceLeaf, plugin: ImmersePlugin) {
super(leaf); super(leaf);
this.plugin = plugin; this.plugin = plugin;
} }
getViewType(): string { getViewType(): string {
return VIEW_TYPE_FOCUS_TASK; return VIEW_TYPE_IMMERSE;
} }
getDisplayText(): string { getDisplayText(): string {
return 'Focus Task'; return 'Immerse';
} }
getIcon(): string { getIcon(): string {
@@ -74,7 +74,7 @@ export class FocusTaskView extends ItemView {
refresh() { refresh() {
const container = this.containerEl.children[1]; const container = this.containerEl.children[1];
container.empty(); container.empty();
container.addClass('focus-task-container'); container.addClass('immerse-container');
// Reset element references // Reset element references
this.timerTimeEl = null; this.timerTimeEl = null;
@@ -96,18 +96,24 @@ export class FocusTaskView extends ItemView {
} }
renderHeader(container: Element) { 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' }); const titleSection = header.createEl('div', { cls: 'immerse-title-section' });
titleSection.createEl('h2', { text: '⚡ Focus Task', cls: 'focus-task-title' }); titleSection.createEl('h2', { text: '⚡ Immerse', cls: 'immerse-title' });
const today = new Date(); const today = new Date();
const dateStr = today.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' }); 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 actions = header.createEl('div', { cls: 'immerse-header-actions' });
const addBtn = actions.createEl('button', { cls: 'focus-task-btn focus-task-btn-primary' }); 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.innerHTML = '+ Add Task';
addBtn.addEventListener('click', () => { addBtn.addEventListener('click', () => {
new QuickAddTaskModal(this.app, this.plugin).open(); new QuickAddTaskModal(this.app, this.plugin).open();
@@ -116,52 +122,54 @@ export class FocusTaskView extends ItemView {
renderStatsBar(container: Element) { renderStatsBar(container: Element) {
const stats = this.plugin.getStats(); 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 = [ const statItems = [
{ label: 'Pending', value: stats.pendingCount.toString(), icon: '📋' }, { label: 'Pending', value: stats.pendingCount.toString(), icon: '📋' },
{ label: 'Done Today', value: stats.completedToday.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: '🔥' }, { label: 'Streak', value: `${stats.streak} days`, icon: '🔥' },
]; ];
statItems.forEach(stat => { statItems.forEach(stat => {
const item = statsBar.createEl('div', { cls: 'focus-task-stat-item' }); const item = statsBar.createEl('div', { cls: 'immerse-stat-item' });
item.createEl('div', { cls: 'focus-task-stat-icon', text: stat.icon }); item.createEl('div', { cls: 'immerse-stat-icon', text: stat.icon });
item.createEl('div', { cls: 'focus-task-stat-value', text: stat.value }); item.createEl('div', { cls: 'immerse-stat-value', text: stat.value });
item.createEl('div', { cls: 'focus-task-stat-label', text: stat.label }); item.createEl('div', { cls: 'immerse-stat-label', text: stat.label });
}); });
} }
renderActiveTask(container: Element) { 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) { if (this.plugin.activeTaskId) {
const task = this.plugin.data.tasks.find(t => t.id === this.plugin.activeTaskId); const task = this.plugin.data.tasks.find(t => t.id === this.plugin.activeTaskId);
if (task) { 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) { if (this.plugin.isBreakMode) {
activeCard.addClass('focus-task-break-card'); activeCard.addClass('immerse-break-card');
activeCard.createEl('div', { cls: 'focus-task-active-label', text: '☕ BREAK TIME' }); const breakLabel = this.plugin.currentTimerSeconds > 0 ? '☕ BREAK TIME' : '✨ BREAK COMPLETE';
activeCard.createEl('div', { cls: 'immerse-active-label', text: breakLabel });
} else { } 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 // 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', { this.timerTimeEl = timerDisplay.createEl('span', {
cls: 'focus-task-timer-time', cls: 'immerse-timer-time',
text: this.plugin.formatTime(this.plugin.currentTimerSeconds) text: this.plugin.formatTime(this.plugin.currentTimerSeconds)
}); });
// Progress bar - store reference for updates // Progress bar - store reference for updates
const progressWrap = activeCard.createEl('div', { cls: 'focus-task-progress-wrap' }); const progressWrap = activeCard.createEl('div', { cls: 'immerse-progress-wrap' });
this.progressBarEl = progressWrap.createEl('div', { cls: 'focus-task-progress-bar' }); this.progressBarEl = progressWrap.createEl('div', { cls: 'immerse-progress-bar' });
let progressPercent = 0; let progressPercent = 0;
if (this.plugin.isBreakMode) { if (this.plugin.isBreakMode) {
@@ -175,56 +183,104 @@ export class FocusTaskView extends ItemView {
} }
this.progressBarEl.style.width = `${Math.min(Math.max(progressPercent, 0), 100)}%`; 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 // Time info - store reference for actual time updates
if (!this.plugin.isBreakMode) { 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)}` }); timeInfo.createEl('span', { text: `Est: ${this.plugin.formatTimeHuman(task.estimatedMinutes)}` });
this.actualTimeEl = timeInfo.createEl('span', { text: `Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}` }); this.actualTimeEl = timeInfo.createEl('span', { text: `Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}` });
} }
// Controls // Controls
const controls = activeCard.createEl('div', { cls: 'focus-task-active-controls' }); const controls = activeCard.createEl('div', { cls: 'immerse-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());
if (this.plugin.isBreakMode) { if (this.plugin.isBreakMode) {
const skipBreakBtn = controls.createEl('button', { cls: 'focus-task-btn' }); // Break mode controls
skipBreakBtn.innerHTML = '⏭ Skip Break'; if (this.plugin.currentTimerSeconds > 0) {
skipBreakBtn.addEventListener('click', () => { // Break is still counting down
this.plugin.isBreakMode = false; this.pauseBtnEl = controls.createEl('button', { cls: 'immerse-btn immerse-btn-secondary' });
this.plugin.stopTimer(); this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? '⏸ Pause' : '▶ Resume';
this.refresh(); 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 { } else {
// No active task - show start focus prompt // No active task - show start focus prompt
const startPrompt = activeSection.createEl('div', { cls: 'focus-task-start-prompt' }); const startPrompt = activeSection.createEl('div', { cls: 'immerse-start-prompt' });
startPrompt.createEl('div', { cls: 'focus-task-prompt-icon', text: '⚡' }); startPrompt.createEl('div', { cls: 'immerse-prompt-icon', text: '⚡' });
startPrompt.createEl('div', { cls: 'focus-task-prompt-text', text: 'Ready to focus?' }); startPrompt.createEl('div', { cls: 'immerse-prompt-text', text: 'Ready to focus?' });
startPrompt.createEl('div', { cls: 'focus-task-prompt-hint', text: 'Click ▶ on a task to start a Pomodoro session' }); startPrompt.createEl('div', { cls: 'immerse-prompt-hint', text: 'Click ▶ on a task to start a Pomodoro session' });
} }
} }
renderTaskList(container: Element) { renderTaskList(container: Element) {
const listSection = container.createEl('div', { cls: 'focus-task-list-section' }); const listSection = container.createEl('div', { cls: 'immerse-list-section' });
// Filters // Filters
const filters = listSection.createEl('div', { cls: 'focus-task-filters' }); const filters = listSection.createEl('div', { cls: 'immerse-filters' });
const filterOptions = [ const filterOptions = [
{ id: 'all', label: 'All Tasks' }, { id: 'all', label: 'All Tasks' },
@@ -235,7 +291,7 @@ export class FocusTaskView extends ItemView {
filterOptions.forEach(opt => { filterOptions.forEach(opt => {
const btn = filters.createEl('button', { 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, text: opt.label,
}); });
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
@@ -245,7 +301,7 @@ export class FocusTaskView extends ItemView {
}); });
// Task items // 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; let tasks = this.plugin.data.tasks;
@@ -265,24 +321,28 @@ export class FocusTaskView extends ItemView {
}); });
if (tasks.length === 0) { if (tasks.length === 0) {
const emptyState = taskList.createEl('div', { cls: 'focus-task-empty-state' }); const emptyState = taskList.createEl('div', { cls: 'immerse-empty-state' });
emptyState.createEl('div', { cls: 'focus-task-empty-icon', text: '📝' }); emptyState.createEl('div', { cls: 'immerse-empty-icon', text: '📝' });
emptyState.createEl('div', { cls: 'focus-task-empty-text', text: 'No tasks yet' }); emptyState.createEl('div', { cls: 'immerse-empty-text', text: 'No tasks yet' });
emptyState.createEl('div', { cls: 'focus-task-empty-hint', text: 'Add a task to get started!' }); emptyState.createEl('div', { cls: 'immerse-empty-hint', text: 'Add a task to get started!' });
} else { } else {
tasks.forEach(task => this.renderTaskItem(taskList, task)); 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 list = this.plugin.settings.lists.find(l => l.id === task.list);
const taskEl = container.createEl('div', { // Check if task is overdue
cls: `focus-task-task-item ${task.completed ? 'completed' : ''} ${task.isActive ? 'active' : ''}` 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 // Checkbox
const checkbox = taskEl.createEl('div', { cls: 'focus-task-checkbox' }); const checkbox = taskEl.createEl('div', { cls: 'immerse-checkbox' });
checkbox.innerHTML = task.completed ? '✓' : ''; checkbox.innerHTML = task.completed ? '✓' : '';
checkbox.style.borderColor = list?.color || '#6366f1'; checkbox.style.borderColor = list?.color || '#6366f1';
if (task.completed) { if (task.completed) {
@@ -297,37 +357,54 @@ export class FocusTaskView extends ItemView {
}); });
// Task content // 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' }); const taskHeader = content.createEl('div', { cls: 'immerse-task-header' });
taskHeader.createEl('span', { cls: 'focus-task-task-text', text: task.text }); taskHeader.createEl('span', { cls: 'immerse-task-text', text: task.text });
if (list) { if (list) {
const listBadge = taskHeader.createEl('span', { const listBadge = taskHeader.createEl('span', {
cls: 'focus-task-list-badge', cls: 'immerse-list-badge',
text: `${list.icon} ${list.name}`, text: `${list.icon} ${list.name}`,
}); });
listBadge.style.backgroundColor = list.color + '20'; listBadge.style.backgroundColor = list.color + '20';
listBadge.style.color = list.color; 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)}` }); taskMeta.createEl('span', { text: `Est: ${this.plugin.formatTimeHuman(task.estimatedMinutes)}` });
if (task.actualMinutes > 0) { if (task.actualMinutes > 0) {
const actualSpan = taskMeta.createEl('span'); const actualSpan = taskMeta.createEl('span');
actualSpan.setText(`Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}`); actualSpan.setText(`Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}`);
if (task.actualMinutes > task.estimatedMinutes) { 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 // Actions
const actions = taskEl.createEl('div', { cls: 'focus-task-task-actions' }); const actions = taskEl.createEl('div', { cls: 'immerse-task-actions' });
if (!task.completed) { if (!task.completed) {
// Start pomodoro button // 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.innerHTML = '▶';
startBtn.addEventListener('click', (e) => { startBtn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -335,7 +412,7 @@ export class FocusTaskView extends ItemView {
}); });
// Stopwatch mode button // 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.innerHTML = '⏱';
stopwatchBtn.addEventListener('click', (e) => { stopwatchBtn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -344,7 +421,7 @@ export class FocusTaskView extends ItemView {
} }
// Edit button // 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.innerHTML = '✏️';
editBtn.addEventListener('click', (e) => { editBtn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -352,7 +429,7 @@ export class FocusTaskView extends ItemView {
}); });
// Delete button // 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.innerHTML = '🗑';
deleteBtn.addEventListener('click', (e) => { deleteBtn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();

1018
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"
} }