Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50ef40d2e0 | |||
| b3aa1f2992 | |||
| e1484b1723 | |||
| 8207b3626e | |||
| f1af574eb9 | |||
| 2fad5d88ab | |||
| 683c4ddafe | |||
| 331a2b41df | |||
| 364935af66 | |||
| 2f2346b4c8 | |||
| c8f5c69102 | |||
| 2800a7507e | |||
| 2f861c2fcb | |||
| 9abdd10ada | |||
| e66d9b4d25 | |||
| 285ac7a7c9 | |||
| 9e35a2603c |
13
.gitignore
vendored
13
.gitignore
vendored
@@ -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/
|
||||||
@@ -16,3 +19,11 @@ 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
|
||||||
|
|||||||
102
README.md
102
README.md
@@ -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/).
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## 🎯 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,7 +269,7 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📜 License
|
## 📜 License
|
||||||
@@ -230,8 +278,8 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|||||||
|
|
||||||
## 🔗 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/)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -240,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>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "focus-task",
|
"id": "immerse",
|
||||||
"name": "Focus Task",
|
"name": "Immerse",
|
||||||
"version": "1.0.4",
|
"version": "1.1.1",
|
||||||
"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
62
package-release.mjs
Normal 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();
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "focus-task",
|
"name": "immerse",
|
||||||
"version": "1.0.4",
|
"version": "1.1.1",
|
||||||
"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",
|
||||||
|
|||||||
598
src/main.ts
598
src/main.ts
@@ -9,25 +9,26 @@ import {
|
|||||||
} from 'obsidian';
|
} from 'obsidian';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FocusTask,
|
ImmerseTask,
|
||||||
FocusTaskSettings,
|
ImmerseSettings,
|
||||||
FocusTaskData,
|
ImmerseData,
|
||||||
DEFAULT_SETTINGS,
|
DEFAULT_SETTINGS,
|
||||||
DEFAULT_DATA,
|
DEFAULT_DATA,
|
||||||
VIEW_TYPE_FOCUS_TASK,
|
VIEW_TYPE_IMMERSE,
|
||||||
CELEBRATION_MESSAGES,
|
CELEBRATION_MESSAGES,
|
||||||
EARLY_FINISH_MESSAGES,
|
EARLY_FINISH_MESSAGES,
|
||||||
OVERTIME_MESSAGES,
|
OVERTIME_MESSAGES,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
import { FocusTaskView } from './view';
|
import { ImmerseView } from './view';
|
||||||
|
import { ReportView, VIEW_TYPE_REPORT } from './reportView';
|
||||||
import { QuickAddTaskModal } from './modals';
|
import { QuickAddTaskModal } from './modals';
|
||||||
|
|
||||||
// ============ Main Plugin Class ============
|
// ============ Main Plugin Class ============
|
||||||
|
|
||||||
export default class FocusTaskPlugin extends Plugin {
|
export default class ImmersePlugin extends Plugin {
|
||||||
settings: FocusTaskSettings;
|
settings: ImmerseSettings;
|
||||||
data: FocusTaskData;
|
data: ImmerseData;
|
||||||
|
|
||||||
// Timer state
|
// Timer state
|
||||||
timerInterval: number | null = null;
|
timerInterval: number | null = null;
|
||||||
@@ -37,33 +38,54 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
activeTaskId: string | null = null;
|
activeTaskId: string | null = null;
|
||||||
pomodoroCount: number = 0;
|
pomodoroCount: number = 0;
|
||||||
|
|
||||||
|
// Timestamp-based tracking for reliable background timing
|
||||||
|
private timerStartTimestamp: number = 0;
|
||||||
|
private pausedTimeRemaining: number = 0;
|
||||||
|
|
||||||
// Focus time tracking (in seconds for accuracy)
|
// Focus time tracking (in seconds for accuracy)
|
||||||
private focusSecondsToday: number = 0;
|
private focusSecondsToday: number = 0;
|
||||||
|
private secondsWorkedOnCurrentTask: number = 0;
|
||||||
|
|
||||||
// Status bar element
|
// Status bar element
|
||||||
statusBarEl: HTMLElement | null = null;
|
statusBarEl: HTMLElement | null = null;
|
||||||
|
|
||||||
|
// Reminder system
|
||||||
|
private reminderCheckInterval: number | null = null;
|
||||||
|
private notifiedReminders: Set<string> = new Set(); // Track which reminders have been shown
|
||||||
|
|
||||||
async onload() {
|
async onload() {
|
||||||
await this.loadAllData();
|
await this.loadAllData();
|
||||||
|
|
||||||
// Check and reset daily stats
|
// Check and reset daily stats
|
||||||
this.checkDailyReset();
|
this.checkDailyReset();
|
||||||
|
|
||||||
// Register the main view
|
// Handle visibility changes to sync timer when app comes back to foreground
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (!document.hidden && this.isTimerRunning) {
|
||||||
|
this.syncTimerFromTimestamp();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register views
|
||||||
this.registerView(
|
this.registerView(
|
||||||
VIEW_TYPE_FOCUS_TASK,
|
VIEW_TYPE_IMMERSE,
|
||||||
(leaf) => new FocusTaskView(leaf, this)
|
(leaf) => new ImmerseView(leaf, this)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.registerView(
|
||||||
|
VIEW_TYPE_REPORT,
|
||||||
|
(leaf) => new ReportView(leaf, this)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add ribbon icon
|
// Add ribbon icon
|
||||||
this.addRibbonIcon('zap', 'Open Focus Task', () => {
|
this.addRibbonIcon('zap', 'Open Immerse', () => {
|
||||||
this.activateView();
|
this.activateView();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add commands
|
// Add commands
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: 'open-focus-task',
|
id: 'open-immerse',
|
||||||
name: 'Open Focus Task Panel',
|
name: 'Open Immerse Panel',
|
||||||
callback: () => this.activateView(),
|
callback: () => this.activateView(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,21 +113,42 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
callback: () => this.completeActiveTask(),
|
callback: () => this.completeActiveTask(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: 'view-reports',
|
||||||
|
name: 'View Reports',
|
||||||
|
callback: () => this.activateReportView(),
|
||||||
|
});
|
||||||
|
|
||||||
// Add settings tab
|
// Add settings tab
|
||||||
this.addSettingTab(new FocusTaskSettingTab(this.app, this));
|
this.addSettingTab(new ImmerseSettingTab(this.app, this));
|
||||||
|
|
||||||
// Create status bar timer
|
// Create status bar timer
|
||||||
this.createStatusBar();
|
this.createStatusBar();
|
||||||
|
|
||||||
|
// Start reminder checking system
|
||||||
|
if (this.settings.enableReminders) {
|
||||||
|
this.startReminderSystem();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
this.stopTimer();
|
this.stopTimer();
|
||||||
|
this.stopReminderSystem();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadAllData() {
|
async loadAllData() {
|
||||||
const loaded = await this.loadData();
|
const loaded = await this.loadData();
|
||||||
|
|
||||||
|
// Merge loaded data with defaults (defaults first, then override with loaded)
|
||||||
|
// This ensures new fields get default values even for existing installs
|
||||||
this.data = Object.assign({}, DEFAULT_DATA, loaded?.data || {});
|
this.data = Object.assign({}, DEFAULT_DATA, loaded?.data || {});
|
||||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, loaded?.settings || {});
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, loaded?.settings || {});
|
||||||
|
|
||||||
|
// Ensure lists array exists and has at least the default lists
|
||||||
|
if (!this.settings.lists || this.settings.lists.length === 0) {
|
||||||
|
this.settings.lists = DEFAULT_SETTINGS.lists;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize seconds from stored minutes
|
// Initialize seconds from stored minutes
|
||||||
this.focusSecondsToday = (this.data.totalFocusMinutesToday || 0) * 60;
|
this.focusSecondsToday = (this.data.totalFocusMinutesToday || 0) * 60;
|
||||||
}
|
}
|
||||||
@@ -144,14 +187,14 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
const { workspace } = this.app;
|
const { workspace } = this.app;
|
||||||
|
|
||||||
let leaf: WorkspaceLeaf | null = null;
|
let leaf: WorkspaceLeaf | null = null;
|
||||||
const leaves = workspace.getLeavesOfType(VIEW_TYPE_FOCUS_TASK);
|
const leaves = workspace.getLeavesOfType(VIEW_TYPE_IMMERSE);
|
||||||
|
|
||||||
if (leaves.length > 0) {
|
if (leaves.length > 0) {
|
||||||
leaf = leaves[0];
|
leaf = leaves[0];
|
||||||
} else {
|
} else {
|
||||||
leaf = workspace.getRightLeaf(false);
|
leaf = workspace.getRightLeaf(false);
|
||||||
if (leaf) {
|
if (leaf) {
|
||||||
await leaf.setViewState({ type: VIEW_TYPE_FOCUS_TASK, active: true });
|
await leaf.setViewState({ type: VIEW_TYPE_IMMERSE, active: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,9 +203,29 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async activateReportView() {
|
||||||
|
const { workspace } = this.app;
|
||||||
|
|
||||||
|
// Check if report view is already open
|
||||||
|
const existingLeaves = workspace.getLeavesOfType(VIEW_TYPE_REPORT);
|
||||||
|
|
||||||
|
if (existingLeaves.length > 0) {
|
||||||
|
// If already open, just reveal it
|
||||||
|
workspace.revealLeaf(existingLeaves[0]);
|
||||||
|
} else {
|
||||||
|
// Open in a new tab in the main area
|
||||||
|
const leaf = workspace.getLeaf('tab');
|
||||||
|
await leaf.setViewState({
|
||||||
|
type: VIEW_TYPE_REPORT,
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
workspace.revealLeaf(leaf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============ Task Management ============
|
// ============ Task Management ============
|
||||||
|
|
||||||
createTask(text: string, estimatedMinutes: number = this.settings.defaultEstimateMinutes, list: string = 'work'): FocusTask {
|
createTask(text: string, estimatedMinutes: number = this.settings.defaultEstimateMinutes, list: string = 'work'): ImmerseTask {
|
||||||
return {
|
return {
|
||||||
id: this.generateId(),
|
id: this.generateId(),
|
||||||
text,
|
text,
|
||||||
@@ -180,13 +243,13 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
addTask(task: FocusTask) {
|
addTask(task: ImmerseTask) {
|
||||||
this.data.tasks.push(task);
|
this.data.tasks.push(task);
|
||||||
this.saveAllData();
|
this.saveAllData();
|
||||||
this.refreshView();
|
this.refreshView();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTask(taskId: string, updates: Partial<FocusTask>) {
|
updateTask(taskId: string, updates: Partial<ImmerseTask>) {
|
||||||
const task = this.data.tasks.find(t => t.id === taskId);
|
const task = this.data.tasks.find(t => t.id === taskId);
|
||||||
if (task) {
|
if (task) {
|
||||||
Object.assign(task, updates);
|
Object.assign(task, updates);
|
||||||
@@ -215,6 +278,12 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
this.data.completedToday++;
|
this.data.completedToday++;
|
||||||
this.data.lastActiveDate = new Date().toDateString();
|
this.data.lastActiveDate = new Date().toDateString();
|
||||||
|
|
||||||
|
// Archive task for historical reporting
|
||||||
|
this.archiveCompletedTask(task);
|
||||||
|
|
||||||
|
// Update daily stats
|
||||||
|
this.updateDailyStats(task);
|
||||||
|
|
||||||
// Show celebration
|
// Show celebration
|
||||||
if (this.settings.enableCelebrations) {
|
if (this.settings.enableCelebrations) {
|
||||||
this.showCelebration(task);
|
this.showCelebration(task);
|
||||||
@@ -248,14 +317,235 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ Data Archiving & Statistics ============
|
||||||
|
|
||||||
|
archiveCompletedTask(task: ImmerseTask) {
|
||||||
|
// Check if task was overdue
|
||||||
|
const wasOverdue = task.scheduledDate && task.scheduledTime &&
|
||||||
|
new Date(`${task.scheduledDate}T${task.scheduledTime}`).getTime() < (task.completedAt || Date.now());
|
||||||
|
|
||||||
|
const record: import('./types').CompletedTaskRecord = {
|
||||||
|
id: task.id,
|
||||||
|
text: task.text,
|
||||||
|
list: task.list,
|
||||||
|
estimatedMinutes: task.estimatedMinutes,
|
||||||
|
actualMinutes: task.actualMinutes,
|
||||||
|
createdAt: task.createdAt,
|
||||||
|
completedAt: task.completedAt || Date.now(),
|
||||||
|
scheduledDate: task.scheduledDate,
|
||||||
|
wasOverdue: wasOverdue || false,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.data.completedTasksArchive.push(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDailyStats(task: ImmerseTask) {
|
||||||
|
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
||||||
|
|
||||||
|
// Find or create today's stats
|
||||||
|
let todayStats = this.data.dailyStats.find(s => s.date === today);
|
||||||
|
|
||||||
|
if (!todayStats) {
|
||||||
|
todayStats = {
|
||||||
|
date: today,
|
||||||
|
tasksCompleted: 0,
|
||||||
|
totalMinutes: 0,
|
||||||
|
pomodorosCompleted: 0,
|
||||||
|
tasksByList: {},
|
||||||
|
minutesByList: {},
|
||||||
|
};
|
||||||
|
this.data.dailyStats.push(todayStats);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update stats
|
||||||
|
todayStats.tasksCompleted++;
|
||||||
|
todayStats.totalMinutes += task.actualMinutes;
|
||||||
|
todayStats.pomodorosCompleted = this.data.pomodorosCompleted;
|
||||||
|
|
||||||
|
// Update list-specific stats
|
||||||
|
todayStats.tasksByList[task.list] = (todayStats.tasksByList[task.list] || 0) + 1;
|
||||||
|
todayStats.minutesByList[task.list] = (todayStats.minutesByList[task.list] || 0) + task.actualMinutes;
|
||||||
|
|
||||||
|
// Keep only last 365 days of stats to prevent data bloat
|
||||||
|
if (this.data.dailyStats.length > 365) {
|
||||||
|
this.data.dailyStats.sort((a, b) => b.date.localeCompare(a.date));
|
||||||
|
this.data.dailyStats = this.data.dailyStats.slice(0, 365);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ Report Generation ============
|
||||||
|
|
||||||
|
generateReport(filters: import('./types').ReportFilters): import('./types').ReportData {
|
||||||
|
const { startDate, endDate, listIds } = filters;
|
||||||
|
|
||||||
|
// Filter daily stats by date range
|
||||||
|
const filteredStats = this.data.dailyStats.filter(stat => {
|
||||||
|
return stat.date >= startDate && stat.date <= endDate;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter completed tasks by date range and lists
|
||||||
|
const filteredTasks = this.data.completedTasksArchive.filter(task => {
|
||||||
|
const taskDate = new Date(task.completedAt).toISOString().split('T')[0];
|
||||||
|
const inDateRange = taskDate >= startDate && taskDate <= endDate;
|
||||||
|
const inList = !listIds || listIds.includes(task.list);
|
||||||
|
return inDateRange && inList;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate summary stats
|
||||||
|
const totalTasks = filteredTasks.length;
|
||||||
|
const totalMinutes = filteredTasks.reduce((sum, task) => sum + task.actualMinutes, 0);
|
||||||
|
const totalPomodoros = filteredStats.reduce((sum, stat) => sum + stat.pomodorosCompleted, 0);
|
||||||
|
|
||||||
|
// Calculate number of days with data
|
||||||
|
const daysWithData = filteredStats.length || 1; // Avoid division by zero
|
||||||
|
|
||||||
|
const tasksPerDay = totalTasks / daysWithData;
|
||||||
|
const hoursPerDay = (totalMinutes / 60) / daysWithData;
|
||||||
|
const minsPerTask = totalTasks > 0 ? totalMinutes / totalTasks : 0;
|
||||||
|
|
||||||
|
// Time breakdown by list
|
||||||
|
const timeByListMap: Record<string, { minutes: number; taskCount: number }> = {};
|
||||||
|
filteredTasks.forEach(task => {
|
||||||
|
if (!timeByListMap[task.list]) {
|
||||||
|
timeByListMap[task.list] = { minutes: 0, taskCount: 0 };
|
||||||
|
}
|
||||||
|
timeByListMap[task.list].minutes += task.actualMinutes;
|
||||||
|
timeByListMap[task.list].taskCount++;
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeByList = this.settings.lists.map(list => {
|
||||||
|
const data = timeByListMap[list.id] || { minutes: 0, taskCount: 0 };
|
||||||
|
const percentage = totalMinutes > 0 ? (data.minutes / totalMinutes) * 100 : 0;
|
||||||
|
return {
|
||||||
|
listId: list.id,
|
||||||
|
listName: list.name,
|
||||||
|
listIcon: list.icon,
|
||||||
|
listColor: list.color,
|
||||||
|
minutes: data.minutes,
|
||||||
|
taskCount: data.taskCount,
|
||||||
|
percentage: Math.round(percentage * 10) / 10, // Round to 1 decimal
|
||||||
|
};
|
||||||
|
}).filter(item => item.minutes > 0); // Only show lists with data
|
||||||
|
|
||||||
|
// Daily breakdown
|
||||||
|
const dailyBreakdown = filteredStats.map(stat => ({
|
||||||
|
date: stat.date,
|
||||||
|
tasks: stat.tasksCompleted,
|
||||||
|
hours: Math.round((stat.totalMinutes / 60) * 10) / 10,
|
||||||
|
pomodoros: stat.pomodorosCompleted,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Productivity insights
|
||||||
|
const mostProductiveHour = this.calculateMostProductiveHour(filteredTasks);
|
||||||
|
const mostProductiveDay = this.calculateMostProductiveDay(filteredStats);
|
||||||
|
const mostProductiveMonth = this.calculateMostProductiveMonth(filteredStats);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalTasks,
|
||||||
|
totalMinutes,
|
||||||
|
totalPomodoros,
|
||||||
|
tasksPerDay: Math.round(tasksPerDay * 10) / 10,
|
||||||
|
hoursPerDay: Math.round(hoursPerDay * 10) / 10,
|
||||||
|
minsPerTask: Math.round(minsPerTask),
|
||||||
|
currentStreak: this.data.streak,
|
||||||
|
timeByList,
|
||||||
|
dailyBreakdown,
|
||||||
|
mostProductiveHour,
|
||||||
|
mostProductiveDay,
|
||||||
|
mostProductiveMonth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateMostProductiveHour(tasks: import('./types').CompletedTaskRecord[]): number | undefined {
|
||||||
|
if (tasks.length === 0) return undefined;
|
||||||
|
|
||||||
|
const hourCounts: Record<number, number> = {};
|
||||||
|
tasks.forEach(task => {
|
||||||
|
const hour = new Date(task.completedAt).getHours();
|
||||||
|
hourCounts[hour] = (hourCounts[hour] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
let maxHour = 0;
|
||||||
|
let maxCount = 0;
|
||||||
|
for (const [hour, count] of Object.entries(hourCounts)) {
|
||||||
|
if (count > maxCount) {
|
||||||
|
maxCount = count;
|
||||||
|
maxHour = parseInt(hour);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxCount > 0 ? maxHour : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateMostProductiveDay(stats: import('./types').DailyStats[]): string | undefined {
|
||||||
|
if (stats.length === 0) return undefined;
|
||||||
|
|
||||||
|
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||||
|
const dayCounts: Record<string, number> = {};
|
||||||
|
|
||||||
|
stats.forEach(stat => {
|
||||||
|
const dayOfWeek = new Date(stat.date).getDay();
|
||||||
|
const dayName = dayNames[dayOfWeek];
|
||||||
|
dayCounts[dayName] = (dayCounts[dayName] || 0) + stat.tasksCompleted;
|
||||||
|
});
|
||||||
|
|
||||||
|
let maxDay = '';
|
||||||
|
let maxCount = 0;
|
||||||
|
for (const [day, count] of Object.entries(dayCounts)) {
|
||||||
|
if (count > maxCount) {
|
||||||
|
maxCount = count;
|
||||||
|
maxDay = day;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxCount > 0 ? maxDay : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateMostProductiveMonth(stats: import('./types').DailyStats[]): string | undefined {
|
||||||
|
if (stats.length === 0) return undefined;
|
||||||
|
|
||||||
|
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
|
||||||
|
'July', 'August', 'September', 'October', 'November', 'December'];
|
||||||
|
const monthCounts: Record<string, number> = {};
|
||||||
|
|
||||||
|
stats.forEach(stat => {
|
||||||
|
const month = new Date(stat.date).getMonth();
|
||||||
|
const monthName = monthNames[month];
|
||||||
|
monthCounts[monthName] = (monthCounts[monthName] || 0) + stat.tasksCompleted;
|
||||||
|
});
|
||||||
|
|
||||||
|
let maxMonth = '';
|
||||||
|
let maxCount = 0;
|
||||||
|
for (const [month, count] of Object.entries(monthCounts)) {
|
||||||
|
if (count > maxCount) {
|
||||||
|
maxCount = count;
|
||||||
|
maxMonth = month;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxCount > 0 ? maxMonth : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// ============ Timer Management ============
|
// ============ Timer Management ============
|
||||||
|
|
||||||
|
// Sync timer based on timestamp when app returns from background
|
||||||
|
syncTimerFromTimestamp() {
|
||||||
|
// Since all intervals now calculate from timestamps directly,
|
||||||
|
// we just need to trigger an update when coming back to foreground
|
||||||
|
if (!this.isTimerRunning) return;
|
||||||
|
|
||||||
|
// The interval will automatically calculate correct values on next tick
|
||||||
|
// Just update the display immediately to show current state
|
||||||
|
this.updateStatusBar();
|
||||||
|
this.updateTimerDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
startTimer(taskId: string) {
|
startTimer(taskId: string) {
|
||||||
const task = this.data.tasks.find(t => t.id === taskId);
|
const task = this.data.tasks.find(t => t.id === taskId);
|
||||||
if (!task) return;
|
if (!task) return;
|
||||||
|
|
||||||
// Stop any existing timer
|
// Stop any existing timer, preserving actual time
|
||||||
this.stopTimer();
|
this.stopTimer(true);
|
||||||
|
|
||||||
// Set active task
|
// Set active task
|
||||||
this.activeTaskId = taskId;
|
this.activeTaskId = taskId;
|
||||||
@@ -263,6 +553,15 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
this.isBreakMode = false;
|
this.isBreakMode = false;
|
||||||
this.currentTimerSeconds = 0;
|
this.currentTimerSeconds = 0;
|
||||||
this.isTimerRunning = true;
|
this.isTimerRunning = true;
|
||||||
|
this.secondsWorkedOnCurrentTask = task.actualMinutes * 60;
|
||||||
|
|
||||||
|
// Set timestamp for background tracking (stopwatch mode)
|
||||||
|
this.timerStartTimestamp = Date.now();
|
||||||
|
this.pausedTimeRemaining = 0; // 0 indicates stopwatch mode
|
||||||
|
|
||||||
|
// Store initial values
|
||||||
|
const initialSecondsWorked = this.secondsWorkedOnCurrentTask;
|
||||||
|
let alertShown = false;
|
||||||
|
|
||||||
// Full refresh to show the active task card
|
// Full refresh to show the active task card
|
||||||
this.refreshView();
|
this.refreshView();
|
||||||
@@ -270,18 +569,29 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
|
|
||||||
// Start interval (count up mode - stopwatch)
|
// Start interval (count up mode - stopwatch)
|
||||||
this.timerInterval = window.setInterval(() => {
|
this.timerInterval = window.setInterval(() => {
|
||||||
this.currentTimerSeconds++;
|
// Calculate elapsed time from timestamp
|
||||||
task.actualMinutes = Math.floor(this.currentTimerSeconds / 60);
|
const now = Date.now();
|
||||||
|
const elapsedMs = now - this.timerStartTimestamp;
|
||||||
|
const elapsedSeconds = Math.floor(elapsedMs / 1000);
|
||||||
|
|
||||||
// Track focus time
|
// Update timer (count up)
|
||||||
this.focusSecondsToday++;
|
this.currentTimerSeconds = elapsedSeconds;
|
||||||
|
|
||||||
|
// Update actual time worked
|
||||||
|
this.secondsWorkedOnCurrentTask = initialSecondsWorked + elapsedSeconds;
|
||||||
|
task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60);
|
||||||
|
|
||||||
|
// Update focus time
|
||||||
|
const newFocusSeconds = Math.floor((this.data.totalFocusMinutesToday || 0) * 60) + elapsedSeconds;
|
||||||
|
this.focusSecondsToday = newFocusSeconds;
|
||||||
|
|
||||||
// Light update - only timer display, no full refresh
|
// Light update - only timer display, no full refresh
|
||||||
this.updateStatusBar();
|
this.updateStatusBar();
|
||||||
this.updateTimerDisplay();
|
this.updateTimerDisplay();
|
||||||
|
|
||||||
// Check if over estimate
|
// Check if over estimate (only alert once)
|
||||||
if (this.currentTimerSeconds === task.estimatedMinutes * 60) {
|
if (!alertShown && this.currentTimerSeconds >= task.estimatedMinutes * 60) {
|
||||||
|
alertShown = true;
|
||||||
if (this.settings.enableSounds) {
|
if (this.settings.enableSounds) {
|
||||||
this.playAlertSound();
|
this.playAlertSound();
|
||||||
}
|
}
|
||||||
@@ -296,28 +606,49 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
const task = this.data.tasks.find(t => t.id === taskId);
|
const task = this.data.tasks.find(t => t.id === taskId);
|
||||||
if (!task) return;
|
if (!task) return;
|
||||||
|
|
||||||
this.stopTimer();
|
this.stopTimer(true);
|
||||||
this.activeTaskId = taskId;
|
this.activeTaskId = taskId;
|
||||||
task.isActive = true;
|
task.isActive = true;
|
||||||
this.isBreakMode = false;
|
this.isBreakMode = false;
|
||||||
this.currentTimerSeconds = this.settings.pomodoroWorkMinutes * 60;
|
this.currentTimerSeconds = this.settings.pomodoroWorkMinutes * 60;
|
||||||
this.isTimerRunning = true;
|
this.isTimerRunning = true;
|
||||||
|
|
||||||
|
// Initialize from existing actual time to preserve progress across breaks
|
||||||
|
// Store as seconds for precision
|
||||||
|
this.secondsWorkedOnCurrentTask = Math.floor(task.actualMinutes * 60);
|
||||||
|
|
||||||
|
// Set timestamp for background tracking (pomodoro countdown mode)
|
||||||
|
this.timerStartTimestamp = Date.now();
|
||||||
|
this.pausedTimeRemaining = this.currentTimerSeconds;
|
||||||
|
|
||||||
|
// Store the initial seconds worked to calculate delta
|
||||||
|
const initialSecondsWorked = this.secondsWorkedOnCurrentTask;
|
||||||
|
|
||||||
// Full refresh to show the active task card
|
// Full refresh to show the active task card
|
||||||
this.refreshView();
|
this.refreshView();
|
||||||
this.updateStatusBar();
|
this.updateStatusBar();
|
||||||
|
|
||||||
// Track seconds worked for accurate focus time
|
|
||||||
let secondsWorked = 0;
|
|
||||||
|
|
||||||
this.timerInterval = window.setInterval(() => {
|
this.timerInterval = window.setInterval(() => {
|
||||||
this.currentTimerSeconds--;
|
// Calculate elapsed time from timestamp (more accurate than counting ticks)
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsedMs = now - this.timerStartTimestamp;
|
||||||
|
const elapsedSeconds = Math.floor(elapsedMs / 1000);
|
||||||
|
|
||||||
|
// Update countdown timer
|
||||||
|
this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds);
|
||||||
|
|
||||||
if (!this.isBreakMode) {
|
if (!this.isBreakMode) {
|
||||||
secondsWorked++;
|
// Update actual time worked based on real elapsed time
|
||||||
task.actualMinutes = Math.floor(secondsWorked / 60);
|
this.secondsWorkedOnCurrentTask = initialSecondsWorked + elapsedSeconds;
|
||||||
// Increment focus time by 1 second
|
const actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60);
|
||||||
this.focusSecondsToday++;
|
|
||||||
|
if (task.actualMinutes !== actualMinutes) {
|
||||||
|
task.actualMinutes = actualMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update focus time based on elapsed seconds
|
||||||
|
const newFocusSeconds = Math.floor((this.data.totalFocusMinutesToday || 0) * 60) + elapsedSeconds;
|
||||||
|
this.focusSecondsToday = newFocusSeconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Light update - only timer display, no full refresh
|
// Light update - only timer display, no full refresh
|
||||||
@@ -331,6 +662,20 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlePomodoroEnd() {
|
handlePomodoroEnd() {
|
||||||
|
// Stop the timer interval to prevent going into negative
|
||||||
|
if (this.timerInterval) {
|
||||||
|
window.clearInterval(this.timerInterval);
|
||||||
|
this.timerInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set timer to 0 to ensure it doesn't show negative
|
||||||
|
this.currentTimerSeconds = 0;
|
||||||
|
this.isTimerRunning = false;
|
||||||
|
|
||||||
|
// Update displays immediately
|
||||||
|
this.updateStatusBar();
|
||||||
|
this.updateTimerDisplay();
|
||||||
|
|
||||||
if (!this.isBreakMode) {
|
if (!this.isBreakMode) {
|
||||||
// Work session ended
|
// Work session ended
|
||||||
this.pomodoroCount++;
|
this.pomodoroCount++;
|
||||||
@@ -345,16 +690,17 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
if (this.settings.autoStartBreak) {
|
if (this.settings.autoStartBreak) {
|
||||||
this.startBreak();
|
this.startBreak();
|
||||||
} else {
|
} else {
|
||||||
this.stopTimer();
|
this.refreshView();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Break ended
|
// Break ended - keep timer at 0 until user resumes
|
||||||
if (this.settings.enableSounds) {
|
if (this.settings.enableSounds) {
|
||||||
this.playAlertSound();
|
this.playAlertSound();
|
||||||
}
|
}
|
||||||
new Notice('⚡ Break over! Ready to focus?');
|
new Notice('⚡ Break over! Ready to focus?');
|
||||||
this.isBreakMode = false;
|
|
||||||
this.stopTimer();
|
// Keep the break card visible with timer at 0:00
|
||||||
|
this.refreshView();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.saveAllData();
|
this.saveAllData();
|
||||||
@@ -362,9 +708,14 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
|
|
||||||
startBreak() {
|
startBreak() {
|
||||||
this.isBreakMode = true;
|
this.isBreakMode = true;
|
||||||
|
this.isTimerRunning = true;
|
||||||
const isLongBreak = this.pomodoroCount % this.settings.longBreakInterval === 0;
|
const isLongBreak = this.pomodoroCount % this.settings.longBreakInterval === 0;
|
||||||
this.currentTimerSeconds = (isLongBreak ? this.settings.longBreakMinutes : this.settings.pomodoroBreakMinutes) * 60;
|
this.currentTimerSeconds = (isLongBreak ? this.settings.longBreakMinutes : this.settings.pomodoroBreakMinutes) * 60;
|
||||||
|
|
||||||
|
// Set timestamp for background tracking (break countdown mode)
|
||||||
|
this.timerStartTimestamp = Date.now();
|
||||||
|
this.pausedTimeRemaining = this.currentTimerSeconds;
|
||||||
|
|
||||||
new Notice(isLongBreak ? '☕ Long break time!' : '☕ Short break time!');
|
new Notice(isLongBreak ? '☕ Long break time!' : '☕ Short break time!');
|
||||||
|
|
||||||
// Full refresh to show break state
|
// Full refresh to show break state
|
||||||
@@ -372,7 +723,14 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
|
|
||||||
if (!this.timerInterval) {
|
if (!this.timerInterval) {
|
||||||
this.timerInterval = window.setInterval(() => {
|
this.timerInterval = window.setInterval(() => {
|
||||||
this.currentTimerSeconds--;
|
// Calculate elapsed time from timestamp
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsedMs = now - this.timerStartTimestamp;
|
||||||
|
const elapsedSeconds = Math.floor(elapsedMs / 1000);
|
||||||
|
|
||||||
|
// Update countdown timer
|
||||||
|
this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds);
|
||||||
|
|
||||||
// Light update - only timer display
|
// Light update - only timer display
|
||||||
this.updateStatusBar();
|
this.updateStatusBar();
|
||||||
this.updateTimerDisplay();
|
this.updateTimerDisplay();
|
||||||
@@ -388,22 +746,39 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
|
|
||||||
toggleTimer() {
|
toggleTimer() {
|
||||||
if (this.isTimerRunning && this.timerInterval) {
|
if (this.isTimerRunning && this.timerInterval) {
|
||||||
// Pause
|
// Pause - save current state
|
||||||
window.clearInterval(this.timerInterval);
|
window.clearInterval(this.timerInterval);
|
||||||
this.timerInterval = null;
|
this.timerInterval = null;
|
||||||
this.isTimerRunning = false;
|
this.isTimerRunning = false;
|
||||||
|
this.pausedTimeRemaining = this.currentTimerSeconds;
|
||||||
} else if (this.activeTaskId) {
|
} else if (this.activeTaskId) {
|
||||||
// Resume
|
// Resume - restart with new timestamp
|
||||||
this.isTimerRunning = true;
|
this.isTimerRunning = true;
|
||||||
|
this.timerStartTimestamp = Date.now();
|
||||||
const task = this.data.tasks.find(t => t.id === this.activeTaskId);
|
const task = this.data.tasks.find(t => t.id === this.activeTaskId);
|
||||||
|
|
||||||
|
// Store initial values for resume
|
||||||
|
const initialSecondsWorked = this.secondsWorkedOnCurrentTask;
|
||||||
|
|
||||||
this.timerInterval = window.setInterval(() => {
|
this.timerInterval = window.setInterval(() => {
|
||||||
this.currentTimerSeconds--;
|
// Calculate elapsed time from timestamp
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsedMs = now - this.timerStartTimestamp;
|
||||||
|
const elapsedSeconds = Math.floor(elapsedMs / 1000);
|
||||||
|
|
||||||
|
// Update timer (countdown from paused position)
|
||||||
|
this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds);
|
||||||
|
|
||||||
if (task && !this.isBreakMode) {
|
if (task && !this.isBreakMode) {
|
||||||
task.actualMinutes = Math.floor((this.settings.pomodoroWorkMinutes * 60 - this.currentTimerSeconds) / 60);
|
// Update actual time worked
|
||||||
// Track focus time
|
this.secondsWorkedOnCurrentTask = initialSecondsWorked + elapsedSeconds;
|
||||||
this.focusSecondsToday++;
|
task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60);
|
||||||
|
|
||||||
|
// Update focus time
|
||||||
|
const newFocusSeconds = Math.floor((this.data.totalFocusMinutesToday || 0) * 60) + elapsedSeconds;
|
||||||
|
this.focusSecondsToday = newFocusSeconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Light update - only timer display
|
// Light update - only timer display
|
||||||
this.updateStatusBar();
|
this.updateStatusBar();
|
||||||
this.updateTimerDisplay();
|
this.updateTimerDisplay();
|
||||||
@@ -421,7 +796,7 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
this.refreshView();
|
this.refreshView();
|
||||||
}
|
}
|
||||||
|
|
||||||
stopTimer() {
|
stopTimer(preserveActualTime: boolean = false) {
|
||||||
if (this.timerInterval) {
|
if (this.timerInterval) {
|
||||||
window.clearInterval(this.timerInterval);
|
window.clearInterval(this.timerInterval);
|
||||||
this.timerInterval = null;
|
this.timerInterval = null;
|
||||||
@@ -431,11 +806,18 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
const task = this.data.tasks.find(t => t.id === this.activeTaskId);
|
const task = this.data.tasks.find(t => t.id === this.activeTaskId);
|
||||||
if (task) {
|
if (task) {
|
||||||
task.isActive = false;
|
task.isActive = false;
|
||||||
|
// Only reset actual time when manually stopping (not when resuming after break)
|
||||||
|
if (!preserveActualTime) {
|
||||||
|
task.actualMinutes = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isTimerRunning = false;
|
this.isTimerRunning = false;
|
||||||
this.activeTaskId = null;
|
this.activeTaskId = null;
|
||||||
|
this.secondsWorkedOnCurrentTask = 0;
|
||||||
|
this.timerStartTimestamp = 0;
|
||||||
|
this.pausedTimeRemaining = 0;
|
||||||
this.updateStatusBar();
|
this.updateStatusBar();
|
||||||
this.saveAllData();
|
this.saveAllData();
|
||||||
this.refreshView();
|
this.refreshView();
|
||||||
@@ -454,7 +836,7 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
|
|
||||||
createStatusBar() {
|
createStatusBar() {
|
||||||
this.statusBarEl = this.addStatusBarItem();
|
this.statusBarEl = this.addStatusBarItem();
|
||||||
this.statusBarEl.addClass('focus-task-status-bar');
|
this.statusBarEl.addClass('immerse-status-bar');
|
||||||
this.updateStatusBar();
|
this.updateStatusBar();
|
||||||
|
|
||||||
// Click to open panel
|
// Click to open panel
|
||||||
@@ -473,16 +855,94 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
const icon = this.isTimerRunning ? '▶' : '⏸';
|
const icon = this.isTimerRunning ? '▶' : '⏸';
|
||||||
|
|
||||||
this.statusBarEl.setText(`⚡ ${icon} ${timeStr} - ${taskName}${task && task.text.length > 20 ? '...' : ''}`);
|
this.statusBarEl.setText(`⚡ ${icon} ${timeStr} - ${taskName}${task && task.text.length > 20 ? '...' : ''}`);
|
||||||
this.statusBarEl.addClass('focus-task-status-active');
|
this.statusBarEl.addClass('immerse-status-active');
|
||||||
} else {
|
} else {
|
||||||
this.statusBarEl.setText('⚡ Focus Task');
|
this.statusBarEl.setText('⚡ Immerse');
|
||||||
this.statusBarEl.removeClass('focus-task-status-active');
|
this.statusBarEl.removeClass('immerse-status-active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ Reminder System ============
|
||||||
|
|
||||||
|
startReminderSystem() {
|
||||||
|
// Check for reminders every 30 seconds
|
||||||
|
this.reminderCheckInterval = window.setInterval(() => {
|
||||||
|
this.checkReminders();
|
||||||
|
}, 30000);
|
||||||
|
// Also check immediately
|
||||||
|
this.checkReminders();
|
||||||
|
}
|
||||||
|
|
||||||
|
stopReminderSystem() {
|
||||||
|
if (this.reminderCheckInterval) {
|
||||||
|
window.clearInterval(this.reminderCheckInterval);
|
||||||
|
this.reminderCheckInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkReminders() {
|
||||||
|
if (!this.settings.enableReminders) return;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const currentTime = now.getTime();
|
||||||
|
|
||||||
|
// Check each incomplete task with scheduling
|
||||||
|
this.data.tasks
|
||||||
|
.filter(task => !task.completed && task.scheduledDate && task.scheduledTime)
|
||||||
|
.forEach(task => {
|
||||||
|
const reminderKey = `${task.id}-${task.scheduledDate}-${task.scheduledTime}`;
|
||||||
|
|
||||||
|
// Skip if we already notified for this reminder
|
||||||
|
if (this.notifiedReminders.has(reminderKey)) return;
|
||||||
|
|
||||||
|
// Parse the scheduled date and time
|
||||||
|
const scheduledDateTime = new Date(`${task.scheduledDate}T${task.scheduledTime}`);
|
||||||
|
const scheduledTime = scheduledDateTime.getTime();
|
||||||
|
|
||||||
|
// Check for overdue tasks (scheduled time has passed)
|
||||||
|
if (currentTime > scheduledTime) {
|
||||||
|
this.showOverdueNotice(task);
|
||||||
|
this.notifiedReminders.add(reminderKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for tasks with reminders set
|
||||||
|
if (task.reminderMinutes) {
|
||||||
|
const reminderTime = scheduledTime - (task.reminderMinutes * 60 * 1000);
|
||||||
|
|
||||||
|
// Show reminder if it's past the reminder time but before the scheduled time
|
||||||
|
if (currentTime >= reminderTime) {
|
||||||
|
this.showReminder(task);
|
||||||
|
this.notifiedReminders.add(reminderKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showReminder(task: ImmerseTask) {
|
||||||
|
const timeStr = task.scheduledTime;
|
||||||
|
new Notice(`🔔 Reminder: "${task.text}" is scheduled for ${timeStr}`, 8000);
|
||||||
|
|
||||||
|
// Play alert sound if enabled
|
||||||
|
if (this.settings.enableSounds) {
|
||||||
|
this.playAlertSound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showOverdueNotice(task: ImmerseTask) {
|
||||||
|
const dateStr = task.scheduledDate;
|
||||||
|
const timeStr = task.scheduledTime;
|
||||||
|
new Notice(`⚠️ Overdue: "${task.text}" was scheduled for ${dateStr} ${timeStr}`, 10000);
|
||||||
|
|
||||||
|
// Play alert sound if enabled
|
||||||
|
if (this.settings.enableSounds) {
|
||||||
|
this.playAlertSound();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ Sounds & Celebrations ============
|
// ============ Sounds & Celebrations ============
|
||||||
|
|
||||||
showCelebration(task: FocusTask) {
|
showCelebration(task: ImmerseTask) {
|
||||||
let messages = CELEBRATION_MESSAGES;
|
let messages = CELEBRATION_MESSAGES;
|
||||||
let extraMessage = '';
|
let extraMessage = '';
|
||||||
|
|
||||||
@@ -549,7 +1009,7 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
|
|
||||||
// ============ Daily Note Logging ============
|
// ============ Daily Note Logging ============
|
||||||
|
|
||||||
async logTaskToDailyNote(task: FocusTask) {
|
async logTaskToDailyNote(task: ImmerseTask) {
|
||||||
try {
|
try {
|
||||||
const list = this.settings.lists.find(l => l.id === task.list);
|
const list = this.settings.lists.find(l => l.id === task.list);
|
||||||
|
|
||||||
@@ -720,9 +1180,9 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refreshView() {
|
refreshView() {
|
||||||
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_FOCUS_TASK);
|
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_IMMERSE);
|
||||||
leaves.forEach(leaf => {
|
leaves.forEach(leaf => {
|
||||||
if (leaf.view instanceof FocusTaskView) {
|
if (leaf.view instanceof ImmerseView) {
|
||||||
leaf.view.refresh();
|
leaf.view.refresh();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -730,23 +1190,23 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
|
|
||||||
// Light refresh - only updates timer display without rebuilding DOM
|
// Light refresh - only updates timer display without rebuilding DOM
|
||||||
updateTimerDisplay() {
|
updateTimerDisplay() {
|
||||||
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_FOCUS_TASK);
|
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_IMMERSE);
|
||||||
leaves.forEach(leaf => {
|
leaves.forEach(leaf => {
|
||||||
if (leaf.view instanceof FocusTaskView) {
|
if (leaf.view instanceof ImmerseView) {
|
||||||
leaf.view.updateTimerDisplay();
|
leaf.view.updateTimerDisplay();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getTasksByList(listId: string): FocusTask[] {
|
getTasksByList(listId: string): ImmerseTask[] {
|
||||||
return this.data.tasks.filter(t => t.list === listId);
|
return this.data.tasks.filter(t => t.list === listId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPendingTasks(): FocusTask[] {
|
getPendingTasks(): ImmerseTask[] {
|
||||||
return this.data.tasks.filter(t => !t.completed);
|
return this.data.tasks.filter(t => !t.completed);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTodaysTasks(): FocusTask[] {
|
getTodaysTasks(): ImmerseTask[] {
|
||||||
const today = new Date().toDateString();
|
const today = new Date().toDateString();
|
||||||
return this.data.tasks.filter(t => {
|
return this.data.tasks.filter(t => {
|
||||||
if (t.scheduledDate === today) return true;
|
if (t.scheduledDate === today) return true;
|
||||||
@@ -777,10 +1237,10 @@ export default class FocusTaskPlugin extends Plugin {
|
|||||||
|
|
||||||
// ============ Settings Tab ============
|
// ============ Settings Tab ============
|
||||||
|
|
||||||
class FocusTaskSettingTab extends PluginSettingTab {
|
class ImmerseSettingTab extends PluginSettingTab {
|
||||||
plugin: FocusTaskPlugin;
|
plugin: ImmersePlugin;
|
||||||
|
|
||||||
constructor(app: App, plugin: FocusTaskPlugin) {
|
constructor(app: App, plugin: ImmersePlugin) {
|
||||||
super(app, plugin);
|
super(app, plugin);
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
||||||
@@ -789,7 +1249,7 @@ class FocusTaskSettingTab extends PluginSettingTab {
|
|||||||
const { containerEl } = this;
|
const { containerEl } = this;
|
||||||
containerEl.empty();
|
containerEl.empty();
|
||||||
|
|
||||||
containerEl.createEl('h1', { text: '⚡ Focus Task Settings' });
|
containerEl.createEl('h1', { text: '⚡ Immerse Settings' });
|
||||||
|
|
||||||
// Pomodoro Settings
|
// Pomodoro Settings
|
||||||
containerEl.createEl('h2', { text: '🍅 Pomodoro Timer' });
|
containerEl.createEl('h2', { text: '🍅 Pomodoro Timer' });
|
||||||
@@ -965,14 +1425,14 @@ class FocusTaskSettingTab extends PluginSettingTab {
|
|||||||
// About section
|
// About section
|
||||||
containerEl.createEl('h2', { text: '📖 About' });
|
containerEl.createEl('h2', { text: '📖 About' });
|
||||||
|
|
||||||
const aboutDiv = containerEl.createDiv({ cls: 'focus-task-about' });
|
const aboutDiv = containerEl.createDiv({ cls: 'immerse-about' });
|
||||||
aboutDiv.innerHTML = `
|
aboutDiv.innerHTML = `
|
||||||
<p><strong>Focus Task</strong> is heavily inspired by <a href="https://www.blitzit.app/">Blitzit</a>,
|
<p><strong>Immerse</strong> is heavily inspired by <a href="https://www.blitzit.app/">Blitzit</a>,
|
||||||
a fantastic productivity app that combines task management with focused time tracking.</p>
|
a fantastic productivity app that combines task management with focused time tracking.</p>
|
||||||
<p>This plugin brings similar functionality directly into Obsidian, allowing you to manage tasks,
|
<p>This plugin brings similar functionality directly into Obsidian, allowing you to manage tasks,
|
||||||
use the Pomodoro technique, and track your productivity without leaving your notes.</p>
|
use the Pomodoro technique, and track your productivity without leaving your notes.</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://git.cribdev.com/crib/focus-task">Source Code</a>
|
<a href="https://git.cribdev.com/crib/immerse">Source Code</a>
|
||||||
</p>
|
</p>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
378
src/modals.ts
378
src/modals.ts
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Buttons
|
// Scheduled date
|
||||||
const buttonContainer = contentEl.createEl('div', { cls: 'focus-task-modal-buttons' });
|
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';
|
||||||
|
});
|
||||||
|
|
||||||
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel', cls: 'focus-task-btn' });
|
// 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: 'immerse-modal-buttons' });
|
||||||
|
|
||||||
|
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!');
|
||||||
@@ -202,3 +297,254 @@ export class EditTaskModal extends Modal {
|
|||||||
contentEl.empty();
|
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
394
src/reportView.ts
Normal 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' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
97
src/types.ts
97
src/types.ts
@@ -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;
|
||||||
@@ -34,18 +36,47 @@ export interface FocusTaskSettings {
|
|||||||
tickSoundEnabled: boolean;
|
tickSoundEnabled: boolean;
|
||||||
// Daily note logging
|
// Daily note logging
|
||||||
logToDaily: boolean;
|
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,
|
||||||
@@ -58,22 +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
|
// Daily note logging
|
||||||
logToDaily: false,
|
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 ============
|
||||||
|
|
||||||
|
|||||||
239
src/view.ts
239
src/view.ts
@@ -3,14 +3,14 @@ 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
|
||||||
@@ -19,17 +19,17 @@ export class FocusTaskView extends ItemView {
|
|||||||
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);
|
||||||
|
|
||||||
|
// 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', {
|
const taskEl = container.createEl('div', {
|
||||||
cls: `focus-task-task-item ${task.completed ? 'completed' : ''} ${task.isActive ? 'active' : ''}`
|
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();
|
||||||
|
|||||||
1009
styles.css
1009
styles.css
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,8 @@
|
|||||||
{
|
{
|
||||||
"1.0.4": "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"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user