Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d6a20ff05 | |||
| 271780f48a | |||
| 264441f83b | |||
| c218162edf | |||
| 886a2f7372 | |||
| 68e11c57e1 | |||
| 50ef40d2e0 | |||
| b3aa1f2992 | |||
| e1484b1723 | |||
| 8207b3626e | |||
| f1af574eb9 | |||
| 2fad5d88ab | |||
| 683c4ddafe | |||
| 331a2b41df | |||
| 364935af66 | |||
| 2f2346b4c8 | |||
| c8f5c69102 | |||
| 2800a7507e | |||
| 2f861c2fcb | |||
| 9abdd10ada | |||
| e66d9b4d25 | |||
| 285ac7a7c9 | |||
| 9e35a2603c | |||
| f5e4a16aff | |||
| 9014f086d6 | |||
| d4f2af179f | |||
| aeb1d62895 | |||
| 9de2b00a2f | |||
| 66652afc90 | |||
| ad5b5e81bb | |||
| 951e9c5406 | |||
| 6dc4d2952d | |||
| 8e10724206 | |||
| d661466c81 | |||
| f634993637 |
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,5 +1,8 @@
|
||||
# Build output
|
||||
# Build output (main.js is tracked for easy installation)
|
||||
*.js.map
|
||||
release/
|
||||
*.zip
|
||||
immerse-*.zip
|
||||
|
||||
# npm
|
||||
node_modules/
|
||||
@@ -16,3 +19,11 @@ Thumbs.db
|
||||
|
||||
# Obsidian
|
||||
data.json
|
||||
|
||||
# Development/Documentation (not for distribution)
|
||||
RELEASE-GUIDE.md
|
||||
ROADMAP.md
|
||||
ROADMAP_*.md
|
||||
deploy-test.bat
|
||||
.claude/
|
||||
RELEASE_NOTES_*.md
|
||||
|
||||
253
README.md
253
README.md
@@ -1,78 +1,61 @@
|
||||
# ⚡ Focus Task
|
||||
# ⚡ Immerse
|
||||
|
||||
A powerful task management and focus timer plugin for [Obsidian](https://obsidian.md), heavily inspired by [Blitzit](https://www.blitzit.app/).
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 🎯 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.
|
||||
Task management and focus timer for Obsidian. Plan your day, track time with Pomodoro technique, and manage tasks without leaving your vault.
|
||||
|
||||
### Why Focus Task?
|
||||
## ✨ Key Features
|
||||
|
||||
- **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
|
||||
- **Pomodoro Built-in**: Work in focused sprints with automatic break reminders
|
||||
- **Satisfying Feedback**: Celebratory messages and sounds when you complete tasks
|
||||
- **Visual Progress**: See your daily progress with stats and streaks
|
||||
**📋 Task Management**
|
||||
- Create tasks with time estimates and organize into customizable lists
|
||||
- Filter by list, status, or date
|
||||
- Schedule tasks with reminders (5/10/15/30/60 min before due)
|
||||
- Daily note integration with automatic task logging
|
||||
|
||||
## ✨ Features
|
||||
**⏱️ Dual Timer Modes**
|
||||
- **Pomodoro**: Configurable work sessions with automatic breaks
|
||||
- **Stopwatch**: Free-form time tracking with estimate alerts
|
||||
|
||||
### 📋 Task Management
|
||||
- Create tasks with time estimates
|
||||
- Organize tasks into customizable lists (Work, Personal, Learning, etc.)
|
||||
- Add notes and details to tasks
|
||||
- Filter tasks by list, today's tasks, or completed items
|
||||
- Drag-and-drop task reordering
|
||||
**📊 Analytics & Reports**
|
||||
- View productivity metrics (tasks/day, hours/day, streaks)
|
||||
- Pie charts and bar graphs showing activity breakdown
|
||||
- Time tracking by list category
|
||||
- Insights on most productive hours, days, and months
|
||||
|
||||
### ⏱️ Dual Timer Modes
|
||||
|
||||
#### Pomodoro Timer
|
||||
- Configurable work sessions (default: 25 minutes)
|
||||
- Short breaks (default: 5 minutes)
|
||||
- Long breaks after a set number of pomodoros (default: 15 minutes every 4 pomodoros)
|
||||
- Auto-start break option
|
||||
- Visual countdown with progress bar
|
||||
|
||||
#### Stopwatch Mode
|
||||
- Free-form time tracking
|
||||
- Alerts when you exceed your estimate
|
||||
- Track actual time vs. estimated time
|
||||
|
||||
### 📊 Progress Tracking
|
||||
- **Daily Stats**: See tasks completed, focus time, and more
|
||||
- **Streak Counter**: Build momentum with consecutive productive days
|
||||
- **Time Comparison**: Compare estimated vs. actual time to improve planning
|
||||
- **Pomodoro Count**: Track total pomodoros completed
|
||||
|
||||
### 🎨 User Experience
|
||||
- **Status Bar Timer**: timer that stays visible while you work
|
||||
- **Celebration Messages**: Fun, randomized messages when you complete tasks
|
||||
- **Sound Notifications**: Audio alerts for timer completion and task completion
|
||||
- **Keyboard Shortcuts**: Quick access to common actions
|
||||
- **Responsive Design**: Works great in any panel size
|
||||
**🎨 Polish**
|
||||
- Status bar timer, celebration messages, sound alerts
|
||||
- Keyboard shortcuts, dark mode support
|
||||
- Overdue task detection with visual indicators
|
||||
- Works on desktop and mobile
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### From Obsidian Community Plugins (Coming Soon)
|
||||
1. Open Obsidian Settings
|
||||
2. Go to Community Plugins
|
||||
3. Search for "Focus Task"
|
||||
3. Search for "Immerse"
|
||||
4. Click Install, then Enable
|
||||
|
||||
### Manual Installation
|
||||
1. Download the latest release from the [releases page](https://git.cribdev.com/crib/focus-task/releases)
|
||||
2. Extract the files to your vault's `.obsidian/plugins/focus-task/` folder
|
||||
3. Reload Obsidian
|
||||
4. Enable the plugin in Settings → Community Plugins
|
||||
1. Download the latest release from the [releases page](https://git.cribdev.com/crib/immerse/releases)
|
||||
2. Extract the files to your vault's `.obsidian/plugins/immerse/` folder
|
||||
3. **⚠️ IMPORTANT**: When updating, do NOT replace or delete the existing `data.json` file - this contains all your tasks, settings, and progress!
|
||||
4. Reload Obsidian
|
||||
5. Enable the plugin in Settings → Community Plugins
|
||||
|
||||
> **Note**: Only copy the three plugin files (`main.js`, `manifest.json`, `styles.css`) when updating. Your `data.json` file stores all your tasks and settings and should never be replaced.
|
||||
|
||||
### Building from Source
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://git.cribdev.com/crib/focus-task.git
|
||||
cd focus-task
|
||||
git clone https://git.cribdev.com/crib/immerse.git
|
||||
cd immerse
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
@@ -81,174 +64,48 @@ npm install
|
||||
npm run build
|
||||
|
||||
# Copy to your vault
|
||||
cp main.js manifest.json styles.css /path/to/your/vault/.obsidian/plugins/focus-task/
|
||||
cp main.js manifest.json styles.css /path/to/your/vault/.obsidian/plugins/immerse/
|
||||
```
|
||||
|
||||
## 📖 Usage
|
||||
## 📖 Quick Start
|
||||
|
||||
### Getting Started
|
||||
1. Click ⚡ icon in ribbon or use command palette (`Ctrl/Cmd + P` → "Open Immerse Panel")
|
||||
2. Add a task with "+ Add Task" (description, time estimate, list)
|
||||
3. Start timer: ▶ for Pomodoro or ⏱ for stopwatch
|
||||
4. Complete and enjoy the celebration!
|
||||
|
||||
1. **Open Focus Task**: Click the ⚡ icon in the ribbon or use the command palette (`Ctrl/Cmd + P` → "Open Focus Task Panel")
|
||||
## ⚙️ Configuration
|
||||
|
||||
2. **Add a Task**: Click "+ Add Task" and fill in:
|
||||
- Task description
|
||||
- Time estimate
|
||||
- List category
|
||||
All settings available in Settings → Immerse:
|
||||
- Pomodoro durations (work: 25min, short break: 5min, long break: 15min)
|
||||
- Default time estimates, sounds, celebrations
|
||||
- Daily note integration (auto-log completed tasks)
|
||||
- Custom lists with names, emojis, and colors
|
||||
|
||||
3. **Start Focusing**: Click ▶ on any task to start a Pomodoro session, or ⏱ for stopwatch mode
|
||||
## 🙏 Credits
|
||||
|
||||
4. **Complete Tasks**: Check off tasks when done and enjoy the celebration!
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
| Action | Command |
|
||||
|--------|---------|
|
||||
| Open Panel | `Ctrl/Cmd + P` → "Open Focus Task Panel" |
|
||||
| Quick Add Task | `Ctrl/Cmd + P` → "Quick Add Task" |
|
||||
| Toggle Timer | `Ctrl/Cmd + P` → "Toggle Timer" |
|
||||
| Complete Task | `Ctrl/Cmd + P` → "Complete Current Task" |
|
||||
| Start Focus | `Ctrl/Cmd + P` → "Start Focus Mode on Next Task" |
|
||||
|
||||
### Timer Modes Explained
|
||||
|
||||
#### 🍅 Pomodoro Mode (▶ button)
|
||||
Best for: Maintaining focus on challenging tasks
|
||||
|
||||
1. Timer counts down from your configured work duration
|
||||
2. When time's up, you'll get a notification
|
||||
3. Take a break (short or long, based on your settings)
|
||||
4. Repeat until the task is complete
|
||||
|
||||
#### ⏱️ Stopwatch Mode (⏱ button)
|
||||
Best for: Tracking time on open-ended tasks
|
||||
|
||||
1. Timer counts up from zero
|
||||
2. Get alerted when you exceed your estimate
|
||||
3. Stop whenever the task is complete
|
||||
4. See exactly how long the task took
|
||||
|
||||
## ⚙️ Settings
|
||||
|
||||
### Pomodoro Timer
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| Work Duration | Length of each work session | 25 min |
|
||||
| Short Break | Length of short breaks | 5 min |
|
||||
| Long Break | Length of long breaks | 15 min |
|
||||
| Long Break Interval | Pomodoros before a long break | 4 |
|
||||
| Auto-start Breaks | Automatically start break timer | Off |
|
||||
|
||||
### General
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| Default Time Estimate | Default estimate for new tasks | 30 min |
|
||||
| Enable Sounds | Play completion/alert sounds | On |
|
||||
| Enable Celebrations | Show celebration messages | On |
|
||||
| Show Floating Timer | Display draggable timer widget | On |
|
||||
|
||||
### Lists
|
||||
Customize your task lists with:
|
||||
- Custom names
|
||||
- Emoji icons
|
||||
- Color coding
|
||||
|
||||
Default lists: Work 💼, Personal 🏠, Learning 📚
|
||||
|
||||
## 🎨 Customization
|
||||
|
||||
### Adding Custom Lists
|
||||
1. Go to Settings → Focus Task → Lists
|
||||
2. Click "+ Add List"
|
||||
3. Set the name, emoji, and color
|
||||
4. Click Save
|
||||
|
||||
### Theming
|
||||
Focus Task respects your Obsidian theme and adapts to both light and dark modes automatically.
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
focus-task/
|
||||
├── src/
|
||||
│ ├── main.ts # Main plugin class
|
||||
│ ├── types.ts # TypeScript interfaces
|
||||
│ ├── view.ts # Main UI view
|
||||
│ └── modals.ts # Task modals
|
||||
├── styles.css # Plugin styles
|
||||
├── manifest.json # Obsidian plugin manifest
|
||||
├── package.json # npm configuration
|
||||
├── tsconfig.json # TypeScript config
|
||||
├── esbuild.config.mjs # Build configuration
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## 🙏 Inspiration & Credits
|
||||
|
||||
This plugin is **heavily inspired by [Blitzit](https://www.blitzit.app/)**, a fantastic standalone productivity app that combines task management with focused time tracking.
|
||||
|
||||
Blitzit's approach to productivity resonated with me:
|
||||
- Simple, focused interface
|
||||
- Time estimation and tracking
|
||||
- Pomodoro technique integration
|
||||
- Satisfying task completion experience
|
||||
- Progress insights
|
||||
|
||||
I wanted to bring this experience directly into Obsidian, where I already manage my notes and knowledge. Focus Task is my attempt to capture the essence of what makes Blitzit great while leveraging Obsidian's powerful ecosystem.
|
||||
|
||||
**If you're looking for a dedicated productivity app, I highly recommend checking out [Blitzit](https://www.blitzit.app/)!**
|
||||
Heavily inspired by [Blitzit](https://www.blitzit.app/) - a fantastic productivity app. Immerse brings that experience into Obsidian.
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions are welcome! Feel free to:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
### Development Setup
|
||||
Contributions welcome! Fork, create a feature branch, and open a PR.
|
||||
|
||||
**Dev Setup:**
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://git.cribdev.com/crib/focus-task.git
|
||||
cd focus-task
|
||||
|
||||
# Install dependencies
|
||||
git clone https://git.cribdev.com/crib/immerse.git
|
||||
cd immerse
|
||||
npm install
|
||||
|
||||
# Start development build (watches for changes)
|
||||
npm run dev
|
||||
|
||||
# Create symlink to your test vault
|
||||
ln -s $(pwd) /path/to/vault/.obsidian/plugins/focus-task
|
||||
```
|
||||
|
||||
## 📝 Roadmap
|
||||
|
||||
- [ ] Integration with Obsidian Tasks plugin
|
||||
- [ ] Calendar view for scheduled tasks
|
||||
- [ ] Weekly/Monthly reports
|
||||
- [ ] Task templates
|
||||
- [ ] Sync with external task managers
|
||||
- [ ] Mobile optimizations
|
||||
- [ ] Task dependencies
|
||||
- [ ] Time blocking in daily notes
|
||||
|
||||
## 📜 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- **Repository**: [https://git.cribdev.com/crib/focus-task](https://git.cribdev.com/crib/focus-task)
|
||||
- **Issues**: [https://git.cribdev.com/crib/focus-task/issues](https://git.cribdev.com/crib/focus-task/issues)
|
||||
- **Inspiration**: [Blitzit App](https://www.blitzit.app/)
|
||||
MIT License - see [LICENSE](LICENSE) file.
|
||||
|
||||
---
|
||||
|
||||
**Links:** [Repository](https://git.cribdev.com/crib/immerse) • [Issues](https://git.cribdev.com/crib/immerse/issues) • [Blitzit](https://www.blitzit.app/)
|
||||
|
||||
<p align="center">
|
||||
Made with ❤️ for the Obsidian community
|
||||
<br>
|
||||
Inspired by <a href="https://www.blitzit.app/">Blitzit</a> ⚡
|
||||
<em>Made with ❤️ for the Obsidian community • Inspired by Blitzit ⚡</em>
|
||||
</p>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"id": "focus-task",
|
||||
"name": "Focus Task",
|
||||
"version": "1.0.2",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "A Blitzit-inspired task management and focus timer plugin. Plan your day, track time with Pomodoro technique, and crush your tasks with satisfying checkoffs.",
|
||||
"author": "Crib",
|
||||
"authorUrl": "https://git.cribdev.com/crib",
|
||||
"fundingUrl": "",
|
||||
"isDesktopOnly": false
|
||||
"id": "immerse",
|
||||
"name": "Immerse",
|
||||
"version": "1.1.4",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "A Blitzit-inspired task management and focus timer plugin. Plan your day, track time with Pomodoro technique, and crush your tasks with satisfying checkoffs.",
|
||||
"author": "Crib",
|
||||
"authorUrl": "https://git.cribdev.com/crib",
|
||||
"fundingUrl": "",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
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",
|
||||
"version": "1.0.2",
|
||||
"name": "immerse",
|
||||
"version": "1.1.4",
|
||||
"description": "A Blitzit-inspired task management and focus timer plugin for Obsidian",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"package": "npm run build && node package-release.mjs",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": [
|
||||
@@ -21,7 +22,7 @@
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.cribdev.com/crib/focus-task"
|
||||
"url": "https://git.cribdev.com/crib/immerse"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
|
||||
818
src/main.ts
818
src/main.ts
File diff suppressed because it is too large
Load Diff
776
src/modals.ts
776
src/modals.ts
@@ -5,21 +5,312 @@ import {
|
||||
Setting,
|
||||
} from 'obsidian';
|
||||
|
||||
import { FocusTask } from './types';
|
||||
import FocusTaskPlugin from './main';
|
||||
import { ImmerseTask } from './types';
|
||||
import ImmersePlugin from './main';
|
||||
|
||||
// Emoji search keywords mapping
|
||||
const EMOJI_KEYWORDS: { [key: string]: string } = {
|
||||
'💼': 'briefcase work business office job',
|
||||
'🏠': 'home house',
|
||||
'📚': 'books study read library',
|
||||
'🎯': 'target goal aim dart',
|
||||
'✅': 'check mark done complete checkbox tick',
|
||||
'📝': 'memo note write pencil',
|
||||
'💡': 'light bulb idea',
|
||||
'🔥': 'fire hot flame',
|
||||
'⚡': 'lightning bolt electric zap',
|
||||
'🎨': 'art paint palette',
|
||||
'🏆': 'trophy award win',
|
||||
'💪': 'muscle strong flex',
|
||||
'🚀': 'rocket ship launch',
|
||||
'📊': 'chart graph data',
|
||||
'⏰': 'clock time alarm',
|
||||
'💰': 'money bag cash dollar',
|
||||
'😀': 'smile happy face grin',
|
||||
'😃': 'smile happy grin',
|
||||
'😄': 'smile happy laugh',
|
||||
'😁': 'grin smile happy',
|
||||
'😆': 'laugh smile happy',
|
||||
'😅': 'sweat smile nervous',
|
||||
'🤣': 'laugh rolling floor',
|
||||
'😂': 'tears joy laugh cry',
|
||||
'🙂': 'smile happy slight',
|
||||
'🙃': 'upside down smile',
|
||||
'😉': 'wink smile flirt',
|
||||
'😊': 'blush smile happy',
|
||||
'😇': 'angel halo smile',
|
||||
'🥰': 'love hearts smile',
|
||||
'😍': 'love heart eyes smile',
|
||||
'🤩': 'star eyes excited',
|
||||
'😘': 'kiss love heart',
|
||||
'😗': 'kiss love',
|
||||
'😚': 'kiss love',
|
||||
'😙': 'kiss love smile',
|
||||
'🥲': 'smile tear cry happy',
|
||||
'😋': 'yum delicious smile',
|
||||
'😛': 'tongue playful',
|
||||
'😜': 'wink tongue playful',
|
||||
'🤪': 'crazy wild eyes',
|
||||
'😝': 'tongue eyes squint',
|
||||
'🤑': 'money dollar rich',
|
||||
'🤗': 'hug smile',
|
||||
'🤭': 'hand over mouth giggle',
|
||||
'🤫': 'shush quiet secret',
|
||||
'🤔': 'think hmm wonder',
|
||||
'🤐': 'zipper mouth secret',
|
||||
'🤨': 'eyebrow raised skeptical',
|
||||
'😐': 'neutral meh',
|
||||
'😑': 'expressionless blank',
|
||||
'😶': 'no mouth silent',
|
||||
'😏': 'smirk confident',
|
||||
'😒': 'unamused annoyed',
|
||||
'🙄': 'eye roll annoyed',
|
||||
'😬': 'grimace awkward',
|
||||
'🤥': 'liar lying pinocchio',
|
||||
'😌': 'relieved content',
|
||||
'😔': 'sad pensive',
|
||||
'😪': 'sleepy tired',
|
||||
'🤤': 'drool sleep',
|
||||
'😴': 'sleep zzz',
|
||||
'😷': 'mask sick medical',
|
||||
'🤒': 'sick thermometer',
|
||||
'🤕': 'injured bandage',
|
||||
'🤢': 'nausea sick',
|
||||
'🤮': 'vomit sick',
|
||||
'🤧': 'sneeze sick tissue',
|
||||
'🥵': 'hot sweat',
|
||||
'🥶': 'cold freeze',
|
||||
'😎': 'cool sunglasses',
|
||||
'🤓': 'nerd glasses',
|
||||
'🧐': 'monocle fancy',
|
||||
'😕': 'confused uncertain',
|
||||
'😟': 'worried concerned',
|
||||
'🙁': 'frown sad',
|
||||
'☹️': 'frown sad',
|
||||
'😮': 'wow surprised',
|
||||
'😯': 'surprised shocked',
|
||||
'😲': 'shocked astonished',
|
||||
'😳': 'flushed embarrassed',
|
||||
'🥺': 'pleading puppy eyes',
|
||||
'😦': 'frown worried',
|
||||
'😧': 'anguished worried',
|
||||
'😨': 'fearful scared',
|
||||
'😰': 'anxious sweat',
|
||||
'😥': 'sad sweat',
|
||||
'😢': 'cry tear sad',
|
||||
'😭': 'cry tears sob',
|
||||
'😱': 'scream fear',
|
||||
'😖': 'confounded',
|
||||
'😣': 'persevere struggle',
|
||||
'😞': 'disappointed sad',
|
||||
'😓': 'downcast sweat',
|
||||
'😩': 'weary tired',
|
||||
'😫': 'tired exhausted',
|
||||
'🥱': 'yawn tired',
|
||||
'😤': 'triumph proud',
|
||||
'😡': 'angry mad rage',
|
||||
'😠': 'angry mad',
|
||||
'🤬': 'cursing swearing angry',
|
||||
'😈': 'devil smiling evil',
|
||||
'👿': 'devil angry evil',
|
||||
'💀': 'skull death',
|
||||
'☠️': 'skull crossbones death',
|
||||
'💩': 'poop poo',
|
||||
'🤡': 'clown funny',
|
||||
'👹': 'ogre monster',
|
||||
'👺': 'goblin monster',
|
||||
'👻': 'ghost boo',
|
||||
'👽': 'alien extraterrestrial',
|
||||
'👾': 'alien monster game',
|
||||
'🤖': 'robot bot',
|
||||
'❤️': 'red heart love',
|
||||
'🧡': 'orange heart love',
|
||||
'💛': 'yellow heart love',
|
||||
'💚': 'green heart love',
|
||||
'💙': 'blue heart love',
|
||||
'💜': 'purple heart love',
|
||||
'🤎': 'brown heart love',
|
||||
'🖤': 'black heart love',
|
||||
'🤍': 'white heart love',
|
||||
'💔': 'broken heart sad',
|
||||
'❣️': 'heart exclamation love',
|
||||
'💕': 'two hearts love',
|
||||
'💞': 'revolving hearts love',
|
||||
'💓': 'beating heart love',
|
||||
'💗': 'growing heart love',
|
||||
'💖': 'sparkling heart love',
|
||||
'💘': 'arrow heart love cupid',
|
||||
'💝': 'heart box gift love',
|
||||
'💟': 'heart decoration love',
|
||||
'❤️🔥': 'heart fire love passion',
|
||||
'❤️🩹': 'heart bandage healing',
|
||||
'💌': 'love letter heart',
|
||||
'💋': 'kiss lips',
|
||||
'💑': 'couple love kiss',
|
||||
'💏': 'kiss couple love',
|
||||
'👋': 'wave hand hello goodbye',
|
||||
'🤚': 'raised hand back',
|
||||
'🖐️': 'hand fingers spread',
|
||||
'✋': 'raised hand stop',
|
||||
'🖖': 'vulcan salute spock',
|
||||
'👌': 'ok okay hand',
|
||||
'🤌': 'pinched fingers italian',
|
||||
'🤏': 'pinching hand small',
|
||||
'✌️': 'peace victory hand',
|
||||
'🤞': 'crossed fingers luck',
|
||||
'🤟': 'love you hand',
|
||||
'🤘': 'rock on horns',
|
||||
'🤙': 'call me hang loose',
|
||||
'👈': 'left point finger',
|
||||
'👉': 'right point finger',
|
||||
'👆': 'up point finger',
|
||||
'🖕': 'middle finger rude',
|
||||
'👇': 'down point finger',
|
||||
'☝️': 'up point finger',
|
||||
'👍': 'thumbs up yes good',
|
||||
'👎': 'thumbs down no bad',
|
||||
'✊': 'fist hand',
|
||||
'👊': 'fist bump punch',
|
||||
'🤛': 'left fist bump',
|
||||
'🤜': 'right fist bump',
|
||||
'👏': 'clap applause',
|
||||
'🙌': 'raising hands celebration',
|
||||
'👐': 'open hands',
|
||||
'🤲': 'palms together pray',
|
||||
'🤝': 'handshake deal',
|
||||
'🙏': 'pray please thank',
|
||||
'✍️': 'writing hand',
|
||||
'💅': 'nail polish manicure',
|
||||
'🤳': 'selfie camera phone',
|
||||
'🐶': 'dog puppy pet',
|
||||
'🐱': 'cat kitty pet',
|
||||
'🐭': 'mouse rat',
|
||||
'🐹': 'hamster pet',
|
||||
'🐰': 'rabbit bunny',
|
||||
'🦊': 'fox',
|
||||
'🐻': 'bear',
|
||||
'🐼': 'panda bear',
|
||||
'🐨': 'koala bear',
|
||||
'🐯': 'tiger face',
|
||||
'🦁': 'lion face',
|
||||
'🐮': 'cow face',
|
||||
'🐷': 'pig face',
|
||||
'🐸': 'frog face',
|
||||
'🐵': 'monkey face',
|
||||
'🍎': 'apple red fruit',
|
||||
'🍊': 'orange fruit',
|
||||
'🍋': 'lemon fruit',
|
||||
'🍌': 'banana fruit',
|
||||
'🍉': 'watermelon fruit',
|
||||
'🍇': 'grapes fruit',
|
||||
'🍓': 'strawberry fruit',
|
||||
'🍒': 'cherry fruit',
|
||||
'🍑': 'peach fruit',
|
||||
'🥭': 'mango fruit',
|
||||
'🍍': 'pineapple fruit',
|
||||
'🥥': 'coconut fruit',
|
||||
'🥝': 'kiwi fruit',
|
||||
'🍅': 'tomato vegetable',
|
||||
'🥑': 'avocado fruit',
|
||||
'🍞': 'bread food',
|
||||
'⚽': 'soccer ball football',
|
||||
'🏀': 'basketball ball',
|
||||
'🏈': 'american football',
|
||||
'⚾': 'baseball ball',
|
||||
'🎾': 'tennis ball',
|
||||
'🏐': 'volleyball ball',
|
||||
'🚗': 'car auto vehicle',
|
||||
'🚕': 'taxi car',
|
||||
'🚙': 'suv car vehicle',
|
||||
'🚌': 'bus vehicle',
|
||||
'🚎': 'trolleybus bus',
|
||||
'🏎️': 'racing car fast',
|
||||
'🚓': 'police car cop',
|
||||
'🚑': 'ambulance emergency',
|
||||
'🚒': 'fire truck engine',
|
||||
'🚲': 'bicycle bike',
|
||||
'✈️': 'airplane plane flight',
|
||||
'💻': 'laptop computer',
|
||||
'⌨️': 'keyboard computer',
|
||||
'🖱️': 'mouse computer',
|
||||
'🖥️': 'desktop computer',
|
||||
'🖨️': 'printer',
|
||||
'📱': 'phone mobile iphone',
|
||||
'📞': 'phone telephone',
|
||||
'☎️': 'telephone phone',
|
||||
'📺': 'tv television',
|
||||
'📻': 'radio',
|
||||
'📁': 'folder file',
|
||||
'📂': 'open folder file',
|
||||
'📅': 'calendar date',
|
||||
'📆': 'calendar date',
|
||||
'📈': 'chart up graph',
|
||||
'📉': 'chart down graph',
|
||||
'📌': 'pushpin pin',
|
||||
'📍': 'pin location map',
|
||||
'📎': 'paperclip clip',
|
||||
'🎵': 'music note',
|
||||
'🎶': 'music notes',
|
||||
'🎼': 'musical score',
|
||||
'🎹': 'piano keyboard music',
|
||||
'🎸': 'guitar music',
|
||||
'🎺': 'trumpet music',
|
||||
'🎷': 'saxophone music',
|
||||
'🥁': 'drum music',
|
||||
'🎤': 'microphone mic sing',
|
||||
'🎧': 'headphones music',
|
||||
'🔊': 'speaker loud volume',
|
||||
'❌': 'cross x no cancel',
|
||||
'⚠️': 'warning caution alert',
|
||||
'🔴': 'red circle',
|
||||
'🟢': 'green circle',
|
||||
'🔵': 'blue circle',
|
||||
'🟡': 'yellow circle',
|
||||
'🟣': 'purple circle',
|
||||
'⚫': 'black circle',
|
||||
'⚪': 'white circle',
|
||||
'🟤': 'brown circle',
|
||||
'🔺': 'triangle red up',
|
||||
'🔻': 'triangle red down',
|
||||
'🔸': 'diamond orange small',
|
||||
'🔹': 'diamond blue small',
|
||||
'🔶': 'diamond orange large',
|
||||
'🔷': 'diamond blue large',
|
||||
};
|
||||
|
||||
// Emoji categories for picker
|
||||
const EMOJI_CATEGORIES = {
|
||||
'⭐ Frequently Used': ['💼', '🏠', '📚', '🎯', '✅', '📝', '💡', '🔥', '⚡', '🎨', '🏆', '💪', '🚀', '📊', '⏰', '💰'],
|
||||
'😀 Smileys & Emotion': ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '🥰', '😍', '🤩', '😘', '😗', '😚', '😙', '🥲', '😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔', '🤐', '🤨', '😐', '😑', '😶', '😏', '😒', '🙄', '😬', '🤥', '😌', '😔', '😪', '🤤', '😴', '😷', '🤒', '🤕', '🤢', '🤮', '🤧', '🥵', '🥶', '😎', '🤓', '🧐', '😕', '😟', '🙁', '☹️', '😮', '😯', '😲', '😳', '🥺', '😦', '😧', '😨', '😰', '😥', '😢', '😭', '😱', '😖', '😣', '😞', '😓', '😩', '😫', '🥱', '😤', '😡', '😠', '🤬', '😈', '👿', '💀', '☠️', '💩', '🤡', '👹', '👺', '👻', '👽', '👾', '🤖'],
|
||||
'❤️ Hearts & Love': ['❤️', '🧡', '💛', '💚', '💙', '💜', '🤎', '🖤', '🤍', '💔', '❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝', '💟', '❤️🔥', '❤️🩹', '💌', '💋', '💑', '💏', '👩❤️👨', '👨❤️👨', '👩❤️👩'],
|
||||
'👤 People & Body': ['👋', '🤚', '🖐️', '✋', '🖖', '👌', '🤌', '🤏', '✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉', '👆', '🖕', '👇', '☝️', '👍', '👎', '✊', '👊', '🤛', '🤜', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '✍️', '💅', '🤳', '💪', '🦾', '🦿', '🦵', '🦶', '👂', '🦻', '👃', '🧠', '🫀', '🫁', '🦷', '🦴', '👀', '👁️', '👅', '👄', '👶', '🧒', '👦', '👧', '🧑', '👨', '👩', '🧔', '🧑🦰', '👨🦰', '👩🦰', '🧑🦱', '👨🦱', '👩🦱', '🧑🦳', '👨🦳', '👩🦳', '🧑🦲', '👨🦲', '👩🦲', '👱', '👱♂️', '👱♀️', '🧓', '👴', '👵', '🙍', '🙍♂️', '🙍♀️', '🙎', '🙎♂️', '🙎♀️', '🙅', '🙅♂️', '🙅♀️', '🙆', '🙆♂️', '🙆♀️', '💁', '💁♂️', '💁♀️', '🙋', '🙋♂️', '🙋♀️', '🧏', '🧏♂️', '🧏♀️', '🙇', '🙇♂️', '🙇♀️', '🤦', '🤦♂️', '🤦♀️', '🤷', '🤷♂️', '🤷♀️'],
|
||||
'🐶 Animals & Nature': ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐮', '🐷', '🐽', '🐸', '🐵', '🙈', '🙉', '🙊', '🐒', '🐔', '🐧', '🐦', '🐤', '🐣', '🐥', '🦆', '🦅', '🦉', '🦇', '🐺', '🐗', '🐴', '🦄', '🐝', '🐛', '🦋', '🐌', '🐞', '🐜', '🦟', '🦗', '🕷️', '🕸️', '🦂', '🐢', '🐍', '🦎', '🦖', '🦕', '🐙', '🦑', '🦐', '🦞', '🦀', '🐡', '🐠', '🐟', '🐬', '🐳', '🐋', '🦈', '🐊', '🐅', '🐆', '🦓', '🦍', '🦧', '🐘', '🦛', '🦏', '🐪', '🐫', '🦒', '🦘', '🐃', '🐂', '🐄', '🐎', '🐖', '🐏', '🐑', '🦙', '🐐', '🦌', '🐕', '🐩', '🦮', '🐕🦺', '🐈', '🐈⬛', '🐓', '🦃', '🦚', '🦜', '🦢', '🦩', '🕊️', '🐇', '🦝', '🦨', '🦡', '🦦', '🦥', '🐁', '🐀', '🐿️', '🦔', '🌲', '🌳', '🌴', '🌱', '🌿', '☘️', '🍀', '🎍', '🎋', '🍃', '🍂', '🍁', '🍄', '🌾', '💐', '🌷', '🌹', '🥀', '🌺', '🌸', '🌼', '🌻', '🌞', '🌝', '🌛', '🌜', '🌚', '🌕', '🌖', '🌗', '🌘', '🌑', '🌒', '🌓', '🌔', '🌙', '🌎', '🌍', '🌏', '🪐', '💫', '⭐', '🌟', '✨', '⚡', '☄️', '💥', '🔥', '🌪️', '🌈', '☀️', '🌤️', '⛅', '🌥️', '☁️', '🌦️', '🌧️', '⛈️', '🌩️', '🌨️', '❄️', '☃️', '⛄', '🌬️', '💨', '💧', '💦', '☔', '☂️', '🌊', '🌫️'],
|
||||
'🍎 Food & Drink': ['🍇', '🍈', '🍉', '🍊', '🍋', '🍌', '🍍', '🥭', '🍎', '🍏', '🍐', '🍑', '🍒', '🍓', '🫐', '🥝', '🍅', '🫒', '🥥', '🥑', '🍆', '🥔', '🥕', '🌽', '🌶️', '🫑', '🥒', '🥬', '🥦', '🧄', '🧅', '🍄', '🥜', '🌰', '🍞', '🥐', '🥖', '🫓', '🥨', '🥯', '🥞', '🧇', '🧀', '🍖', '🍗', '🥩', '🥓', '🍔', '🍟', '🍕', '🌭', '🥪', '🌮', '🌯', '🫔', '🥙', '🧆', '🥚', '🍳', '🥘', '🍲', '🫕', '🥣', '🥗', '🍿', '🧈', '🧂', '🥫', '🍱', '🍘', '🍙', '🍚', '🍛', '🍜', '🍝', '🍠', '🍢', '🍣', '🍤', '🍥', '🥮', '🍡', '🥟', '🥠', '🥡', '🦀', '🦞', '🦐', '🦑', '🦪', '🍦', '🍧', '🍨', '🍩', '🍪', '🎂', '🍰', '🧁', '🥧', '🍫', '🍬', '🍭', '🍮', '🍯', '🍼', '🥛', '☕', '🫖', '🍵', '🍶', '🍾', '🍷', '🍸', '🍹', '🍺', '🍻', '🥂', '🥃', '🥤', '🧋', '🧃', '🧉', '🧊'],
|
||||
'⚽ Activities & Sports': ['⚽', '🏀', '🏈', '⚾', '🥎', '🎾', '🏐', '🏉', '🥏', '🎱', '🪀', '🏓', '🏸', '🏒', '🏑', '🥍', '🏏', '🪃', '🥅', '⛳', '🪁', '🏹', '🎣', '🤿', '🥊', '🥋', '🎽', '🛹', '🛼', '🛷', '⛸️', '🥌', '🎿', '⛷️', '🏂', '🪂', '🏋️', '🏋️♂️', '🏋️♀️', '🤼', '🤼♂️', '🤼♀️', '🤸', '🤸♂️', '🤸♀️', '⛹️', '⛹️♂️', '⛹️♀️', '🤺', '🤾', '🤾♂️', '🤾♀️', '🏌️', '🏌️♂️', '🏌️♀️', '🏇', '🧘', '🧘♂️', '🧘♀️', '🏄', '🏄♂️', '🏄♀️', '🏊', '🏊♂️', '🏊♀️', '🤽', '🤽♂️', '🤽♀️', '🚣', '🚣♂️', '🚣♀️', '🧗', '🧗♂️', '🧗♀️', '🚵', '🚵♂️', '🚵♀️', '🚴', '🚴♂️', '🚴♀️', '🏆', '🥇', '🥈', '🥉', '🏅', '🎖️', '🏵️', '🎗️', '🎫', '🎟️', '🎪', '🤹', '🤹♂️', '🤹♀️', '🎭', '🩰', '🎨', '🎬', '🎤', '🎧', '🎼', '🎹', '🥁', '🪘', '🎷', '🎺', '🪗', '🎸', '🪕', '🎻', '🎲', '♟️', '🎯', '🎳', '🎮', '🎰', '🧩'],
|
||||
'🚗 Travel & Places': ['🚗', '🚕', '🚙', '🚌', '🚎', '🏎️', '🚓', '🚑', '🚒', '🚐', '🛻', '🚚', '🚛', '🚜', '🦯', '🦽', '🦼', '🛴', '🚲', '🛵', '🏍️', '🛺', '🚨', '🚔', '🚍', '🚘', '🚖', '🚡', '🚠', '🚟', '🚃', '🚋', '🚞', '🚝', '🚄', '🚅', '🚈', '🚂', '🚆', '🚇', '🚊', '🚉', '✈️', '🛫', '🛬', '🛩️', '💺', '🛰️', '🚀', '🛸', '🚁', '🛶', '⛵', '🚤', '🛥️', '🛳️', '⛴️', '🚢', '⚓', '⛽', '🚧', '🚦', '🚥', '🚏', '🗺️', '🗿', '🗽', '🗼', '🏰', '🏯', '🏟️', '🎡', '🎢', '🎠', '⛲', '⛱️', '🏖️', '🏝️', '🏜️', '🌋', '⛰️', '🏔️', '🗻', '🏕️', '⛺', '🛖', '🏠', '🏡', '🏘️', '🏚️', '🏗️', '🏭', '🏢', '🏬', '🏣', '🏤', '🏥', '🏦', '🏨', '🏪', '🏫', '🏩', '💒', '🏛️', '⛪', '🕌', '🕍', '🛕', '🕋', '⛩️', '🛤️', '🛣️', '🗾', '🎑', '🏞️', '🌅', '🌄', '🌠', '🎇', '🎆', '🌇', '🌆', '🏙️', '🌃', '🌌', '🌉', '🌁'],
|
||||
'💻 Objects & Technology': ['⌚', '📱', '📲', '💻', '⌨️', '🖥️', '🖨️', '🖱️', '🖲️', '🕹️', '🗜️', '💾', '💿', '📀', '📼', '📷', '📸', '📹', '🎥', '📽️', '🎞️', '📞', '☎️', '📟', '📠', '📺', '📻', '🎙️', '🎚️', '🎛️', '🧭', '⏱️', '⏲️', '⏰', '🕰️', '⌛', '⏳', '📡', '🔋', '🔌', '💡', '🔦', '🕯️', '🪔', '🧯', '🛢️', '💸', '💵', '💴', '💶', '💷', '🪙', '💰', '💳', '💎', '⚖️', '🪜', '🧰', '🪛', '🔧', '🔨', '⚒️', '🛠️', '⛏️', '🪚', '🔩', '⚙️', '🪤', '🧱', '⛓️', '🧲', '🔫', '💣', '🧨', '🪓', '🔪', '🗡️', '⚔️', '🛡️', '🚬', '⚰️', '🪦', '⚱️', '🏺', '🔮', '📿', '🧿', '💈', '⚗️', '🔭', '🔬', '🕳️', '🩹', '🩺', '💊', '💉', '🩸', '🧬', '🦷', '🧪', '🌡️', '🧹', '🪠', '🧺', '🧻', '🚽', '🚰', '🚿', '🛁', '🛀', '🧼', '🪥', '🪒', '🧽', '🪣', '🧴', '🛎️', '🔑', '🗝️', '🚪', '🪑', '🛋️', '🛏️', '🛌', '🧸', '🪆', '🖼️', '🪞', '🪟', '🛍️', '🛒', '🎁', '🎈', '🎏', '🎀', '🪄', '🪅', '🎊', '🎉', '🎎', '🏮', '🎐', '🧧'],
|
||||
'📋 Office & Writing': ['✉️', '📧', '📨', '📩', '📤', '📥', '📦', '📫', '📪', '📬', '📭', '📮', '🗳️', '✏️', '✒️', '🖋️', '🖊️', '🖌️', '🖍️', '📝', '💼', '📁', '📂', '🗂️', '📅', '📆', '🗒️', '🗓️', '📇', '📈', '📉', '📊', '📋', '📌', '📍', '📎', '🖇️', '📏', '📐', '✂️', '🗃️', '🗄️', '🗑️', '🔒', '🔓', '🔏', '🔐', '🔑', '🗝️', '🔨', '🪓', '⛏️', '⚒️', '🛠️', '🗡️', '⚔️', '💣', '🪃', '🏹', '🛡️', '🪚', '🔧', '🪛', '🔩', '⚙️', '🗜️', '⚖️'],
|
||||
'🎵 Music & Sound': ['🎵', '🎶', '🎼', '🎹', '🎸', '🎺', '🎷', '🥁', '🪘', '🎤', '🎧', '📻', '🎙️', '🔊', '🔉', '🔈', '🔇', '📢', '📣', '📯', '🔔', '🔕', '🎚️', '🎛️', '🎖️', '🏆', '🥇', '🥈', '🥉', '⚡', '🔥', '💥'],
|
||||
'⚡ Symbols & Signs': ['❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝', '💟', '☮️', '✝️', '☪️', '🕉️', '☸️', '✡️', '🔯', '🕎', '☯️', '☦️', '🛐', '⛎', '♈', '♉', '♊', '♋', '♌', '♍', '♎', '♏', '♐', '♑', '♒', '♓', '🆔', '⚛️', '🉑', '☢️', '☣️', '📴', '📳', '🈶', '🈚', '🈸', '🈺', '🈷️', '✴️', '🆚', '💮', '🉐', '㊙️', '㊗️', '🈴', '🈵', '🈹', '🈲', '🅰️', '🅱️', '🆎', '🆑', '🅾️', '🆘', '❌', '⭕', '🛑', '⛔', '📛', '🚫', '💯', '💢', '♨️', '🚷', '🚯', '🚳', '🚱', '🔞', '📵', '🚭', '❗', '❕', '❓', '❔', '‼️', '⁉️', '🔅', '🔆', '〽️', '⚠️', '🚸', '🔱', '⚜️', '🔰', '♻️', '✅', '🈯', '💹', '❇️', '✳️', '❎', '🌐', '💠', 'Ⓜ️', '🌀', '💤', '🏧', '🚾', '♿', '🅿️', '🛗', '🈳', '🈂️', '🛂', '🛃', '🛄', '🛅', '🚹', '🚺', '🚼', '⚧️', '🚻', '🚮', '🎦', '📶', '🈁', '🔣', 'ℹ️', '🔤', '🔡', '🔠', '🆖', '🆗', '🆙', '🆒', '🆕', '🆓', '0️⃣', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟', '🔢', '#️⃣', '*️⃣', '⏏️', '▶️', '⏸️', '⏯️', '⏹️', '⏺️', '⏭️', '⏮️', '⏩', '⏪', '⏫', '⏬', '◀️', '🔼', '🔽', '➡️', '⬅️', '⬆️', '⬇️', '↗️', '↘️', '↙️', '↖️', '↕️', '↔️', '↪️', '↩️', '⤴️', '⤵️', '🔀', '🔁', '🔂', '🔄', '🔃', '🎵', '🎶', '➕', '➖', '➗', '✖️', '♾️', '💲', '💱', '™️', '©️', '®️', '〰️', '➰', '➿', '🔚', '🔙', '🔛', '🔝', '🔜', '✔️', '☑️', '🔘', '🔴', '🟠', '🟡', '🟢', '🔵', '🟣', '⚫', '⚪', '🟤', '🔺', '🔻', '🔸', '🔹', '🔶', '🔷', '🔳', '🔲', '▪️', '▫️', '◾', '◽', '◼️', '◻️', '🟥', '🟧', '🟨', '🟩', '🟦', '🟪', '⬛', '⬜', '🟫', '🔈', '🔇', '🔉', '🔊', '🔔', '🔕', '📣', '📢', '💬', '💭', '🗯️', '♠️', '♣️', '♥️', '♦️', '🃏', '🎴', '🀄', '🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚', '🕛', '🕜', '🕝', '🕞', '🕟', '🕠', '🕡', '🕢', '🕣', '🕤', '🕥', '🕦', '🕧'],
|
||||
'🏁 Flags': ['🏁', '🚩', '🎌', '🏴', '🏳️', '🏳️🌈', '🏳️⚧️', '🏴☠️', '🇦🇨', '🇦🇩', '🇦🇪', '🇦🇫', '🇦🇬', '🇦🇮', '🇦🇱', '🇦🇲', '🇦🇴', '🇦🇶', '🇦🇷', '🇦🇸', '🇦🇹', '🇦🇺', '🇦🇼', '🇦🇽', '🇦🇿', '🇧🇦', '🇧🇧', '🇧🇩', '🇧🇪', '🇧🇫', '🇧🇬', '🇧🇭', '🇧🇮', '🇧🇯', '🇧🇱', '🇧🇲', '🇧🇳', '🇧🇴', '🇧🇶', '🇧🇷', '🇧🇸', '🇧🇹', '🇧🇻', '🇧🇼', '🇧🇾', '🇧🇿', '🇨🇦', '🇨🇨', '🇨🇩', '🇨🇫', '🇨🇬', '🇨🇭', '🇨🇮', '🇨🇰', '🇨🇱', '🇨🇲', '🇨🇳', '🇨🇴', '🇨🇵', '🇨🇷', '🇨🇺', '🇨🇻', '🇨🇼', '🇨🇽', '🇨🇾', '🇨🇿', '🇩🇪', '🇩🇬', '🇩🇯', '🇩🇰', '🇩🇲', '🇩🇴', '🇩🇿', '🇪🇦', '🇪🇨', '🇪🇪', '🇪🇬', '🇪🇭', '🇪🇷', '🇪🇸', '🇪🇹', '🇪🇺', '🇫🇮', '🇫🇯', '🇫🇰', '🇫🇲', '🇫🇴', '🇫🇷', '🇬🇦', '🇬🇧', '🇬🇩', '🇬🇪', '🇬🇫', '🇬🇬', '🇬🇭', '🇬🇮', '🇬🇱', '🇬🇲', '🇬🇳', '🇬🇵', '🇬🇶', '🇬🇷', '🇬🇸', '🇬🇹', '🇬🇺', '🇬🇼', '🇬🇾', '🇭🇰', '🇭🇲', '🇭🇳', '🇭🇷', '🇭🇹', '🇭🇺', '🇮🇨', '🇮🇩', '🇮🇪', '🇮🇱', '🇮🇲', '🇮🇳', '🇮🇴', '🇮🇶', '🇮🇷', '🇮🇸', '🇮🇹', '🇯🇪', '🇯🇲', '🇯🇴', '🇯🇵', '🇰🇪', '🇰🇬', '🇰🇭', '🇰🇮', '🇰🇲', '🇰🇳', '🇰🇵', '🇰🇷', '🇰🇼', '🇰🇾', '🇰🇿', '🇱🇦', '🇱🇧', '🇱🇨', '🇱🇮', '🇱🇰', '🇱🇷', '🇱🇸', '🇱🇹', '🇱🇺', '🇱🇻', '🇱🇾', '🇲🇦', '🇲🇨', '🇲🇩', '🇲🇪', '🇲🇫', '🇲🇬', '🇲🇭', '🇲🇰', '🇲🇱', '🇲🇲', '🇲🇳', '🇲🇴', '🇲🇵', '🇲🇶', '🇲🇷', '🇲🇸', '🇲🇹', '🇲🇺', '🇲🇻', '🇲🇼', '🇲🇽', '🇲🇾', '🇲🇿', '🇳🇦', '🇳🇨', '🇳🇪', '🇳🇫', '🇳🇬', '🇳🇮', '🇳🇱', '🇳🇴', '🇳🇵', '🇳🇷', '🇳🇺', '🇳🇿', '🇴🇲', '🇵🇦', '🇵🇪', '🇵🇫', '🇵🇬', '🇵🇭', '🇵🇰', '🇵🇱', '🇵🇲', '🇵🇳', '🇵🇷', '🇵🇸', '🇵🇹', '🇵🇼', '🇵🇾', '🇶🇦', '🇷🇪', '🇷🇴', '🇷🇸', '🇷🇺', '🇷🇼', '🇸🇦', '🇸🇧', '🇸🇨', '🇸🇩', '🇸🇪', '🇸🇬', '🇸🇭', '🇸🇮', '🇸🇯', '🇸🇰', '🇸🇱', '🇸🇲', '🇸🇳', '🇸🇴', '🇸🇷', '🇸🇸', '🇸🇹', '🇸🇻', '🇸🇽', '🇸🇾', '🇸🇿', '🇹🇦', '🇹🇨', '🇹🇩', '🇹🇫', '🇹🇬', '🇹🇭', '🇹🇯', '🇹🇰', '🇹🇱', '🇹🇲', '🇹🇳', '🇹🇴', '🇹🇷', '🇹🇹', '🇹🇻', '🇹🇼', '🇹🇿', '🇺🇦', '🇺🇬', '🇺🇲', '🇺🇳', '🇺🇸', '🇺🇾', '🇺🇿', '🇻🇦', '🇻🇨', '🇻🇪', '🇻🇬', '🇻🇮', '🇻🇳', '🇻🇺', '🇼🇫', '🇼🇸', '🇽🇰', '🇾🇪', '🇾🇹', '🇿🇦', '🇿🇲', '🇿🇼', '🏴', '🏴', '🏴'],
|
||||
};
|
||||
|
||||
// ============ Quick Add Task Modal ============
|
||||
|
||||
export class QuickAddTaskModal extends Modal {
|
||||
plugin: FocusTaskPlugin;
|
||||
plugin: ImmersePlugin;
|
||||
taskText: string = '';
|
||||
estimatedMinutes: number;
|
||||
selectedList: string = 'work';
|
||||
scheduledDate: string = '';
|
||||
scheduledTime: string = '';
|
||||
reminderMinutes: number = 0;
|
||||
|
||||
constructor(app: App, plugin: FocusTaskPlugin) {
|
||||
constructor(app: App, plugin: ImmersePlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.estimatedMinutes = plugin.settings.defaultEstimateMinutes;
|
||||
this.reminderMinutes = plugin.settings.defaultReminderMinutes;
|
||||
if (plugin.settings.lists.length > 0) {
|
||||
this.selectedList = plugin.settings.lists[0].id;
|
||||
}
|
||||
@@ -27,7 +318,7 @@ export class QuickAddTaskModal extends Modal {
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.addClass('focus-task-modal');
|
||||
contentEl.addClass('immerse-modal');
|
||||
|
||||
contentEl.createEl('h2', { text: '⚡ Add New Task' });
|
||||
|
||||
@@ -82,19 +373,68 @@ export class QuickAddTaskModal extends Modal {
|
||||
dropdown.onChange(value => this.selectedList = value);
|
||||
});
|
||||
|
||||
// Buttons
|
||||
const buttonContainer = contentEl.createEl('div', { cls: 'focus-task-modal-buttons' });
|
||||
// Scheduled date
|
||||
new Setting(contentEl)
|
||||
.setName('📅 Scheduled Date')
|
||||
.setDesc('Optional: When do you plan to work on this?')
|
||||
.addText(text => {
|
||||
text.setPlaceholder('YYYY-MM-DD')
|
||||
.setValue(this.scheduledDate)
|
||||
.onChange(value => this.scheduledDate = value);
|
||||
text.inputEl.type = 'date';
|
||||
});
|
||||
|
||||
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());
|
||||
|
||||
const addBtn = buttonContainer.createEl('button', { text: 'Add Task', cls: 'focus-task-btn focus-task-btn-primary' });
|
||||
const addBtn = buttonContainer.createEl('button', { text: 'Add Task', cls: 'immerse-btn immerse-btn-primary' });
|
||||
addBtn.addEventListener('click', () => this.submitTask());
|
||||
}
|
||||
|
||||
submitTask() {
|
||||
if (this.taskText.trim()) {
|
||||
const task = this.plugin.createTask(this.taskText, this.estimatedMinutes, this.selectedList);
|
||||
// Add scheduling data if provided
|
||||
if (this.scheduledDate) {
|
||||
task.scheduledDate = this.scheduledDate;
|
||||
}
|
||||
if (this.scheduledTime) {
|
||||
task.scheduledTime = this.scheduledTime;
|
||||
}
|
||||
if (this.reminderMinutes > 0 && this.scheduledDate && this.scheduledTime) {
|
||||
task.reminderMinutes = this.reminderMinutes;
|
||||
}
|
||||
this.plugin.addTask(task);
|
||||
new Notice('✅ Task added!');
|
||||
this.close();
|
||||
@@ -112,10 +452,10 @@ export class QuickAddTaskModal extends Modal {
|
||||
// ============ Edit Task Modal ============
|
||||
|
||||
export class EditTaskModal extends Modal {
|
||||
plugin: FocusTaskPlugin;
|
||||
task: FocusTask;
|
||||
plugin: ImmersePlugin;
|
||||
task: ImmerseTask;
|
||||
|
||||
constructor(app: App, plugin: FocusTaskPlugin, task: FocusTask) {
|
||||
constructor(app: App, plugin: ImmersePlugin, task: ImmerseTask) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.task = { ...task };
|
||||
@@ -123,7 +463,7 @@ export class EditTaskModal extends Modal {
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.addClass('focus-task-modal');
|
||||
contentEl.addClass('immerse-modal');
|
||||
|
||||
contentEl.createEl('h2', { text: '✏️ Edit Task' });
|
||||
|
||||
@@ -177,6 +517,48 @@ export class EditTaskModal extends Modal {
|
||||
textarea.inputEl.rows = 4;
|
||||
});
|
||||
|
||||
// Scheduled date
|
||||
new Setting(contentEl)
|
||||
.setName('📅 Scheduled Date')
|
||||
.setDesc('Optional: When do you plan to work on this?')
|
||||
.addText(text => {
|
||||
text.setPlaceholder('YYYY-MM-DD')
|
||||
.setValue(this.task.scheduledDate || '')
|
||||
.onChange(value => this.task.scheduledDate = value || undefined);
|
||||
text.inputEl.type = 'date';
|
||||
});
|
||||
|
||||
// Scheduled time
|
||||
new Setting(contentEl)
|
||||
.setName('⏰ Scheduled Time')
|
||||
.setDesc('Optional: What time?')
|
||||
.addText(text => {
|
||||
text.setPlaceholder('HH:mm')
|
||||
.setValue(this.task.scheduledTime || '')
|
||||
.onChange(value => this.task.scheduledTime = value || undefined);
|
||||
text.inputEl.type = 'time';
|
||||
});
|
||||
|
||||
// Reminder
|
||||
if (this.plugin.settings.enableReminders) {
|
||||
new Setting(contentEl)
|
||||
.setName('🔔 Reminder')
|
||||
.setDesc('Remind me before the scheduled time')
|
||||
.addDropdown(dropdown => {
|
||||
dropdown.addOption('0', 'No reminder');
|
||||
dropdown.addOption('5', '5 minutes before');
|
||||
dropdown.addOption('10', '10 minutes before');
|
||||
dropdown.addOption('15', '15 minutes before');
|
||||
dropdown.addOption('30', '30 minutes before');
|
||||
dropdown.addOption('60', '1 hour before');
|
||||
dropdown.setValue((this.task.reminderMinutes || 0).toString());
|
||||
dropdown.onChange(value => {
|
||||
const minutes = parseInt(value);
|
||||
this.task.reminderMinutes = minutes > 0 ? minutes : undefined;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Show actual time if task has been worked on
|
||||
if (this.task.actualMinutes > 0) {
|
||||
new Setting(contentEl)
|
||||
@@ -184,12 +566,12 @@ export class EditTaskModal extends Modal {
|
||||
.setDesc(`You've worked on this task for ${this.plugin.formatTimeHuman(this.task.actualMinutes)}`);
|
||||
}
|
||||
|
||||
const buttonContainer = contentEl.createEl('div', { cls: 'focus-task-modal-buttons' });
|
||||
const buttonContainer = contentEl.createEl('div', { cls: 'immerse-modal-buttons' });
|
||||
|
||||
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel', cls: 'focus-task-btn' });
|
||||
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel', cls: 'immerse-btn' });
|
||||
cancelBtn.addEventListener('click', () => this.close());
|
||||
|
||||
const saveBtn = buttonContainer.createEl('button', { text: 'Save', cls: 'focus-task-btn focus-task-btn-primary' });
|
||||
const saveBtn = buttonContainer.createEl('button', { text: 'Save', cls: 'immerse-btn immerse-btn-primary' });
|
||||
saveBtn.addEventListener('click', () => {
|
||||
this.plugin.updateTask(this.task.id, this.task);
|
||||
new Notice('✅ Task updated!');
|
||||
@@ -202,3 +584,365 @@ export class EditTaskModal extends Modal {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Emoji Picker Modal ============
|
||||
|
||||
export class EmojiPickerModal extends Modal {
|
||||
onSelect: (emoji: string) => void;
|
||||
currentEmoji: string;
|
||||
|
||||
constructor(app: App, currentEmoji: string, onSelect: (emoji: string) => void) {
|
||||
super(app);
|
||||
this.currentEmoji = currentEmoji;
|
||||
this.onSelect = onSelect;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.addClass('immerse-emoji-picker');
|
||||
|
||||
contentEl.createEl('h2', { text: 'Select Emoji' });
|
||||
|
||||
// Current selection
|
||||
if (this.currentEmoji) {
|
||||
const currentDiv = contentEl.createDiv({ cls: 'immerse-emoji-current' });
|
||||
currentDiv.createEl('span', { text: 'Current: ' });
|
||||
currentDiv.createEl('span', { text: this.currentEmoji, cls: 'immerse-emoji-current-icon' });
|
||||
}
|
||||
|
||||
// Search box
|
||||
const searchContainer = contentEl.createDiv({ cls: 'immerse-emoji-search' });
|
||||
const searchInput = searchContainer.createEl('input', {
|
||||
type: 'text',
|
||||
placeholder: 'Search emojis...',
|
||||
cls: 'immerse-emoji-search-input'
|
||||
});
|
||||
|
||||
// Emoji grid container
|
||||
const gridContainer = contentEl.createDiv({ cls: 'immerse-emoji-categories' });
|
||||
|
||||
// Render all categories
|
||||
const renderCategories = (filter: string = '') => {
|
||||
gridContainer.empty();
|
||||
|
||||
Object.entries(EMOJI_CATEGORIES).forEach(([category, emojis]) => {
|
||||
const filteredEmojis = filter
|
||||
? emojis.filter(emoji => {
|
||||
const keywords = EMOJI_KEYWORDS[emoji] || '';
|
||||
return keywords.toLowerCase().includes(filter) || emoji.includes(filter);
|
||||
})
|
||||
: emojis;
|
||||
|
||||
if (filteredEmojis.length === 0) return;
|
||||
|
||||
const categoryDiv = gridContainer.createDiv({ cls: 'immerse-emoji-category' });
|
||||
categoryDiv.createEl('h3', { text: category, cls: 'immerse-emoji-category-title' });
|
||||
|
||||
const grid = categoryDiv.createDiv({ cls: 'immerse-emoji-grid' });
|
||||
|
||||
filteredEmojis.forEach(emoji => {
|
||||
const button = grid.createEl('button', {
|
||||
text: emoji,
|
||||
cls: 'immerse-emoji-button'
|
||||
});
|
||||
|
||||
if (emoji === this.currentEmoji) {
|
||||
button.addClass('immerse-emoji-selected');
|
||||
}
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
this.onSelect(emoji);
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Initial render
|
||||
renderCategories();
|
||||
|
||||
// Search functionality
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const filter = (e.target as HTMLInputElement).value.toLowerCase();
|
||||
renderCategories(filter);
|
||||
});
|
||||
|
||||
// Custom emoji input
|
||||
const customDiv = contentEl.createDiv({ cls: 'immerse-emoji-custom' });
|
||||
customDiv.createEl('span', { text: 'Or enter custom emoji: ' });
|
||||
const customInput = customDiv.createEl('input', {
|
||||
type: 'text',
|
||||
placeholder: 'Paste emoji',
|
||||
cls: 'immerse-emoji-custom-input',
|
||||
});
|
||||
|
||||
const customBtn = customDiv.createEl('button', {
|
||||
text: 'Use Custom',
|
||||
cls: 'immerse-btn immerse-btn-primary'
|
||||
});
|
||||
|
||||
customBtn.addEventListener('click', () => {
|
||||
const customEmoji = customInput.value.trim();
|
||||
if (customEmoji) {
|
||||
this.onSelect(customEmoji);
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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' });
|
||||
});
|
||||
}
|
||||
}
|
||||
101
src/types.ts
101
src/types.ts
@@ -1,6 +1,6 @@
|
||||
// ============ Types & Interfaces ============
|
||||
|
||||
export interface FocusTask {
|
||||
export interface ImmerseTask {
|
||||
id: string;
|
||||
text: string;
|
||||
completed: boolean;
|
||||
@@ -10,7 +10,9 @@ export interface FocusTask {
|
||||
completedAt?: number;
|
||||
list: string;
|
||||
notes: string;
|
||||
scheduledDate?: string;
|
||||
scheduledDate?: string; // Date in YYYY-MM-DD format
|
||||
scheduledTime?: string; // Time in HH:mm format (24-hour)
|
||||
reminderMinutes?: number; // Minutes before scheduled time to remind (0 = no reminder)
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
@@ -21,7 +23,7 @@ export interface TaskList {
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface FocusTaskSettings {
|
||||
export interface ImmerseSettings {
|
||||
pomodoroWorkMinutes: number;
|
||||
pomodoroBreakMinutes: number;
|
||||
longBreakMinutes: number;
|
||||
@@ -32,18 +34,49 @@ export interface FocusTaskSettings {
|
||||
lists: TaskList[];
|
||||
autoStartBreak: boolean;
|
||||
tickSoundEnabled: boolean;
|
||||
// Daily note logging
|
||||
logToDaily: boolean;
|
||||
// Task reminders
|
||||
enableReminders: boolean;
|
||||
defaultReminderMinutes: number; // Default minutes before task to remind
|
||||
}
|
||||
|
||||
export interface FocusTaskData {
|
||||
tasks: FocusTask[];
|
||||
// Daily statistics snapshot
|
||||
export interface DailyStats {
|
||||
date: string; // YYYY-MM-DD format
|
||||
tasksCompleted: number;
|
||||
totalMinutes: number;
|
||||
pomodorosCompleted: number;
|
||||
tasksByList: Record<string, number>; // listId -> count
|
||||
minutesByList: Record<string, number>; // listId -> minutes
|
||||
}
|
||||
|
||||
// Archived completed task (for historical reporting)
|
||||
export interface CompletedTaskRecord {
|
||||
id: string;
|
||||
text: string;
|
||||
list: string;
|
||||
estimatedMinutes: number;
|
||||
actualMinutes: number;
|
||||
createdAt: number;
|
||||
completedAt: number;
|
||||
scheduledDate?: string;
|
||||
wasOverdue: boolean; // was it completed after scheduled time?
|
||||
}
|
||||
|
||||
export interface ImmerseData {
|
||||
tasks: ImmerseTask[];
|
||||
completedToday: number;
|
||||
totalFocusMinutesToday: number;
|
||||
streak: number;
|
||||
lastActiveDate: string;
|
||||
pomodorosCompleted: number;
|
||||
// Historical data for reporting
|
||||
dailyStats: DailyStats[]; // Array of daily statistics
|
||||
completedTasksArchive: CompletedTaskRecord[]; // All completed tasks history
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: FocusTaskSettings = {
|
||||
export const DEFAULT_SETTINGS: ImmerseSettings = {
|
||||
pomodoroWorkMinutes: 25,
|
||||
pomodoroBreakMinutes: 5,
|
||||
longBreakMinutes: 15,
|
||||
@@ -56,20 +89,70 @@ export const DEFAULT_SETTINGS: FocusTaskSettings = {
|
||||
{ id: 'personal', name: 'Personal', color: '#22c55e', icon: '🏠' },
|
||||
{ id: 'learning', name: 'Learning', color: '#f59e0b', icon: '📚' },
|
||||
],
|
||||
autoStartBreak: false,
|
||||
autoStartBreak: true,
|
||||
tickSoundEnabled: false,
|
||||
// Daily note logging
|
||||
logToDaily: false,
|
||||
// Task reminders
|
||||
enableReminders: true,
|
||||
defaultReminderMinutes: 15,
|
||||
};
|
||||
|
||||
export const DEFAULT_DATA: FocusTaskData = {
|
||||
export const DEFAULT_DATA: ImmerseData = {
|
||||
tasks: [],
|
||||
completedToday: 0,
|
||||
totalFocusMinutesToday: 0,
|
||||
streak: 0,
|
||||
lastActiveDate: '',
|
||||
pomodorosCompleted: 0,
|
||||
dailyStats: [],
|
||||
completedTasksArchive: [],
|
||||
};
|
||||
|
||||
export const VIEW_TYPE_FOCUS_TASK = 'focus-task-view';
|
||||
export const VIEW_TYPE_IMMERSE = 'immerse-view';
|
||||
|
||||
// ============ Reporting Types ============
|
||||
|
||||
export interface ReportFilters {
|
||||
startDate: string; // YYYY-MM-DD
|
||||
endDate: string; // YYYY-MM-DD
|
||||
listIds?: string[]; // Filter by specific lists (undefined = all)
|
||||
}
|
||||
|
||||
export interface ReportData {
|
||||
// Summary stats
|
||||
totalTasks: number;
|
||||
totalMinutes: number;
|
||||
totalPomodoros: number;
|
||||
tasksPerDay: number;
|
||||
hoursPerDay: number;
|
||||
minsPerTask: number;
|
||||
currentStreak: number;
|
||||
|
||||
// Time breakdown by list
|
||||
timeByList: Array<{
|
||||
listId: string;
|
||||
listName: string;
|
||||
listIcon: string;
|
||||
listColor: string;
|
||||
minutes: number;
|
||||
taskCount: number;
|
||||
percentage: number;
|
||||
}>;
|
||||
|
||||
// Daily breakdown for charts
|
||||
dailyBreakdown: Array<{
|
||||
date: string; // YYYY-MM-DD
|
||||
tasks: number;
|
||||
hours: number;
|
||||
pomodoros: number;
|
||||
}>;
|
||||
|
||||
// Productivity insights
|
||||
mostProductiveHour?: number; // 0-23
|
||||
mostProductiveDay?: string; // day name
|
||||
mostProductiveMonth?: string; // month name
|
||||
}
|
||||
|
||||
// ============ Celebration Messages ============
|
||||
|
||||
|
||||
245
src/view.ts
245
src/view.ts
@@ -3,14 +3,14 @@ import {
|
||||
WorkspaceLeaf,
|
||||
} from 'obsidian';
|
||||
|
||||
import { VIEW_TYPE_FOCUS_TASK, FocusTask } from './types';
|
||||
import { VIEW_TYPE_IMMERSE, ImmerseTask } from './types';
|
||||
import { QuickAddTaskModal, EditTaskModal } from './modals';
|
||||
import FocusTaskPlugin from './main';
|
||||
import ImmersePlugin from './main';
|
||||
|
||||
// ============ Main View ============
|
||||
|
||||
export class FocusTaskView extends ItemView {
|
||||
plugin: FocusTaskPlugin;
|
||||
export class ImmerseView extends ItemView {
|
||||
plugin: ImmersePlugin;
|
||||
currentFilter: string = 'all';
|
||||
|
||||
// References to elements that need frequent updates
|
||||
@@ -19,17 +19,17 @@ export class FocusTaskView extends ItemView {
|
||||
private actualTimeEl: HTMLElement | null = null;
|
||||
private pauseBtnEl: HTMLElement | null = null;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: FocusTaskPlugin) {
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ImmersePlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return VIEW_TYPE_FOCUS_TASK;
|
||||
return VIEW_TYPE_IMMERSE;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return 'Focus Task';
|
||||
return 'Immerse';
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
@@ -74,7 +74,7 @@ export class FocusTaskView extends ItemView {
|
||||
refresh() {
|
||||
const container = this.containerEl.children[1];
|
||||
container.empty();
|
||||
container.addClass('focus-task-container');
|
||||
container.addClass('immerse-container');
|
||||
|
||||
// Reset element references
|
||||
this.timerTimeEl = null;
|
||||
@@ -96,18 +96,24 @@ export class FocusTaskView extends ItemView {
|
||||
}
|
||||
|
||||
renderHeader(container: Element) {
|
||||
const header = container.createEl('div', { cls: 'focus-task-header' });
|
||||
const header = container.createEl('div', { cls: 'immerse-header' });
|
||||
|
||||
const titleSection = header.createEl('div', { cls: 'focus-task-title-section' });
|
||||
titleSection.createEl('h2', { text: '⚡ Focus Task', cls: 'focus-task-title' });
|
||||
const titleSection = header.createEl('div', { cls: 'immerse-title-section' });
|
||||
titleSection.createEl('h2', { text: '⚡ Immerse', cls: 'immerse-title' });
|
||||
|
||||
const today = new Date();
|
||||
const dateStr = today.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' });
|
||||
titleSection.createEl('div', { text: dateStr, cls: 'focus-task-date' });
|
||||
titleSection.createEl('div', { text: dateStr, cls: 'immerse-date' });
|
||||
|
||||
const actions = header.createEl('div', { cls: 'focus-task-header-actions' });
|
||||
const 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.addEventListener('click', () => {
|
||||
new QuickAddTaskModal(this.app, this.plugin).open();
|
||||
@@ -116,52 +122,60 @@ export class FocusTaskView extends ItemView {
|
||||
|
||||
renderStatsBar(container: Element) {
|
||||
const stats = this.plugin.getStats();
|
||||
const statsBar = container.createEl('div', { cls: 'focus-task-stats-bar' });
|
||||
const statsBar = container.createEl('div', { cls: 'immerse-stats-bar' });
|
||||
|
||||
const statItems = [
|
||||
{ label: 'Pending', value: stats.pendingCount.toString(), icon: '📋' },
|
||||
{ label: 'Done Today', value: stats.completedToday.toString(), icon: '✅' },
|
||||
{ label: 'Focus Time', value: this.plugin.formatTimeHuman(stats.totalFocusMinutesToday), icon: '⏱️' },
|
||||
{ label: 'Today\'s Focus', value: this.plugin.formatTimeHuman(stats.totalFocusMinutesToday), icon: '⏱️' },
|
||||
{ label: 'Streak', value: `${stats.streak} days`, icon: '🔥' },
|
||||
];
|
||||
|
||||
statItems.forEach(stat => {
|
||||
const item = statsBar.createEl('div', { cls: 'focus-task-stat-item' });
|
||||
item.createEl('div', { cls: 'focus-task-stat-icon', text: stat.icon });
|
||||
item.createEl('div', { cls: 'focus-task-stat-value', text: stat.value });
|
||||
item.createEl('div', { cls: 'focus-task-stat-label', text: stat.label });
|
||||
const item = statsBar.createEl('div', { cls: 'immerse-stat-item' });
|
||||
item.createEl('div', { cls: 'immerse-stat-icon', text: stat.icon });
|
||||
item.createEl('div', { cls: 'immerse-stat-value', text: stat.value });
|
||||
item.createEl('div', { cls: 'immerse-stat-label', text: stat.label });
|
||||
});
|
||||
}
|
||||
|
||||
renderActiveTask(container: Element) {
|
||||
const activeSection = container.createEl('div', { cls: 'focus-task-active-section' });
|
||||
const activeSection = container.createEl('div', { cls: 'immerse-active-section' });
|
||||
|
||||
if (this.plugin.activeTaskId) {
|
||||
const task = this.plugin.data.tasks.find(t => t.id === this.plugin.activeTaskId);
|
||||
if (task) {
|
||||
activeSection.addClass('focus-task-has-active');
|
||||
activeSection.addClass('immerse-has-active');
|
||||
|
||||
const activeCard = activeSection.createEl('div', { cls: 'focus-task-active-card' });
|
||||
const activeCard = activeSection.createEl('div', { cls: 'immerse-active-card' });
|
||||
|
||||
if (this.plugin.isBreakMode) {
|
||||
activeCard.addClass('focus-task-break-card');
|
||||
activeCard.createEl('div', { cls: 'focus-task-active-label', text: '☕ BREAK TIME' });
|
||||
activeCard.addClass('immerse-break-card');
|
||||
const breakLabel = this.plugin.currentTimerSeconds > 0 ? '☕ BREAK TIME' : '✨ BREAK COMPLETE';
|
||||
activeCard.createEl('div', { cls: 'immerse-active-label', text: breakLabel });
|
||||
} else {
|
||||
activeCard.createEl('div', { cls: 'focus-task-active-label', text: '🎯 FOCUSING ON' });
|
||||
// Determine label based on whether timer is active and mode (stopwatch vs pomodoro)
|
||||
let workLabel: string;
|
||||
if (this.plugin.currentTimerSeconds > 0 || this.plugin.isStopwatchMode) {
|
||||
workLabel = '🎯 FOCUSING ON';
|
||||
} else {
|
||||
workLabel = '🍅 POMODORO COMPLETE';
|
||||
}
|
||||
activeCard.createEl('div', { cls: 'immerse-active-label', text: workLabel });
|
||||
}
|
||||
|
||||
activeCard.createEl('div', { cls: 'focus-task-active-task-name', text: task.text });
|
||||
activeCard.createEl('div', { cls: 'immerse-active-task-name', text: task.text });
|
||||
|
||||
// Timer display - store reference for updates
|
||||
const timerDisplay = activeCard.createEl('div', { cls: 'focus-task-timer-display' });
|
||||
const timerDisplay = activeCard.createEl('div', { cls: 'immerse-timer-display' });
|
||||
this.timerTimeEl = timerDisplay.createEl('span', {
|
||||
cls: 'focus-task-timer-time',
|
||||
cls: 'immerse-timer-time',
|
||||
text: this.plugin.formatTime(this.plugin.currentTimerSeconds)
|
||||
});
|
||||
|
||||
// Progress bar - store reference for updates
|
||||
const progressWrap = activeCard.createEl('div', { cls: 'focus-task-progress-wrap' });
|
||||
this.progressBarEl = progressWrap.createEl('div', { cls: 'focus-task-progress-bar' });
|
||||
const progressWrap = activeCard.createEl('div', { cls: 'immerse-progress-wrap' });
|
||||
this.progressBarEl = progressWrap.createEl('div', { cls: 'immerse-progress-bar' });
|
||||
|
||||
let progressPercent = 0;
|
||||
if (this.plugin.isBreakMode) {
|
||||
@@ -175,56 +189,104 @@ export class FocusTaskView extends ItemView {
|
||||
}
|
||||
|
||||
this.progressBarEl.style.width = `${Math.min(Math.max(progressPercent, 0), 100)}%`;
|
||||
if (progressPercent >= 100) this.progressBarEl.addClass('focus-task-overtime');
|
||||
if (progressPercent >= 100) this.progressBarEl.addClass('immerse-overtime');
|
||||
|
||||
// Time info - store reference for actual time updates
|
||||
if (!this.plugin.isBreakMode) {
|
||||
const timeInfo = activeCard.createEl('div', { cls: 'focus-task-time-info' });
|
||||
const timeInfo = activeCard.createEl('div', { cls: 'immerse-time-info' });
|
||||
timeInfo.createEl('span', { text: `Est: ${this.plugin.formatTimeHuman(task.estimatedMinutes)}` });
|
||||
this.actualTimeEl = timeInfo.createEl('span', { text: `Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}` });
|
||||
}
|
||||
|
||||
// Controls
|
||||
const controls = activeCard.createEl('div', { cls: 'focus-task-active-controls' });
|
||||
|
||||
this.pauseBtnEl = controls.createEl('button', { cls: 'focus-task-btn focus-task-btn-secondary' });
|
||||
this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? '⏸ Pause' : '▶ Resume';
|
||||
this.pauseBtnEl.addEventListener('click', () => this.plugin.toggleTimer());
|
||||
|
||||
if (!this.plugin.isBreakMode) {
|
||||
const completeBtn = controls.createEl('button', { cls: 'focus-task-btn focus-task-btn-success' });
|
||||
completeBtn.innerHTML = '✓ Complete';
|
||||
completeBtn.addEventListener('click', () => this.plugin.completeTask(task.id));
|
||||
}
|
||||
|
||||
const stopBtn = controls.createEl('button', { cls: 'focus-task-btn focus-task-btn-danger' });
|
||||
stopBtn.innerHTML = '✕ Stop';
|
||||
stopBtn.addEventListener('click', () => this.plugin.stopTimer());
|
||||
const controls = activeCard.createEl('div', { cls: 'immerse-active-controls' });
|
||||
|
||||
if (this.plugin.isBreakMode) {
|
||||
const skipBreakBtn = controls.createEl('button', { cls: 'focus-task-btn' });
|
||||
skipBreakBtn.innerHTML = '⏭ Skip Break';
|
||||
skipBreakBtn.addEventListener('click', () => {
|
||||
this.plugin.isBreakMode = false;
|
||||
this.plugin.stopTimer();
|
||||
this.refresh();
|
||||
});
|
||||
// Break mode controls
|
||||
if (this.plugin.currentTimerSeconds > 0) {
|
||||
// Break is still counting down
|
||||
this.pauseBtnEl = controls.createEl('button', { cls: 'immerse-btn immerse-btn-secondary' });
|
||||
this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? '⏸ Pause' : '▶ Resume';
|
||||
this.pauseBtnEl.addEventListener('click', () => this.plugin.toggleTimer());
|
||||
|
||||
const skipBreakBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-primary' });
|
||||
skipBreakBtn.innerHTML = '⏭ Skip Break';
|
||||
skipBreakBtn.addEventListener('click', () => {
|
||||
this.plugin.isBreakMode = false;
|
||||
this.plugin.startPomodoro(task.id);
|
||||
});
|
||||
|
||||
const stopBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-danger' });
|
||||
stopBtn.innerHTML = '✕ Stop';
|
||||
stopBtn.addEventListener('click', () => {
|
||||
this.plugin.isBreakMode = false;
|
||||
this.plugin.stopTimer();
|
||||
});
|
||||
} else {
|
||||
// Break timer finished - show resume button
|
||||
const resumeWorkBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-success' });
|
||||
resumeWorkBtn.innerHTML = '▶ Resume Work';
|
||||
resumeWorkBtn.addEventListener('click', () => {
|
||||
this.plugin.isBreakMode = false;
|
||||
this.plugin.startPomodoro(task.id);
|
||||
});
|
||||
|
||||
const stopBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-danger' });
|
||||
stopBtn.innerHTML = '✕ Stop';
|
||||
stopBtn.addEventListener('click', () => {
|
||||
this.plugin.isBreakMode = false;
|
||||
this.plugin.stopTimer();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Work mode controls
|
||||
if (this.plugin.currentTimerSeconds > 0 || this.plugin.isStopwatchMode) {
|
||||
// Work session still running (or stopwatch mode active)
|
||||
this.pauseBtnEl = controls.createEl('button', { cls: 'immerse-btn immerse-btn-secondary' });
|
||||
this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? '⏸ Pause' : '▶ Resume';
|
||||
this.pauseBtnEl.addEventListener('click', () => this.plugin.toggleTimer());
|
||||
|
||||
const completeBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-success' });
|
||||
completeBtn.innerHTML = '✓ Complete';
|
||||
completeBtn.addEventListener('click', () => this.plugin.completeTask(task.id));
|
||||
|
||||
const stopBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-danger' });
|
||||
stopBtn.innerHTML = '✕ Stop';
|
||||
stopBtn.addEventListener('click', () => this.plugin.stopTimer());
|
||||
} else {
|
||||
// Pomodoro session finished - show break and completion options
|
||||
const startBreakBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-secondary' });
|
||||
startBreakBtn.innerHTML = '☕ Start Break';
|
||||
startBreakBtn.addEventListener('click', () => this.plugin.startBreak());
|
||||
|
||||
const continueBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-primary' });
|
||||
continueBtn.innerHTML = '▶ Continue Working';
|
||||
continueBtn.addEventListener('click', () => this.plugin.startPomodoro(task.id));
|
||||
|
||||
const completeBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-success' });
|
||||
completeBtn.innerHTML = '✓ Complete';
|
||||
completeBtn.addEventListener('click', () => this.plugin.completeTask(task.id));
|
||||
|
||||
const stopBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-danger' });
|
||||
stopBtn.innerHTML = '✕ Stop';
|
||||
stopBtn.addEventListener('click', () => this.plugin.stopTimer());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No active task - show start focus prompt
|
||||
const startPrompt = activeSection.createEl('div', { cls: 'focus-task-start-prompt' });
|
||||
startPrompt.createEl('div', { cls: 'focus-task-prompt-icon', text: '⚡' });
|
||||
startPrompt.createEl('div', { cls: 'focus-task-prompt-text', text: 'Ready to focus?' });
|
||||
startPrompt.createEl('div', { cls: 'focus-task-prompt-hint', text: 'Click ▶ on a task to start a Pomodoro session' });
|
||||
const startPrompt = activeSection.createEl('div', { cls: 'immerse-start-prompt' });
|
||||
startPrompt.createEl('div', { cls: 'immerse-prompt-icon', text: '⚡' });
|
||||
startPrompt.createEl('div', { cls: 'immerse-prompt-text', text: 'Ready to focus?' });
|
||||
startPrompt.createEl('div', { cls: 'immerse-prompt-hint', text: 'Click ▶ on a task to start a Pomodoro session' });
|
||||
}
|
||||
}
|
||||
|
||||
renderTaskList(container: Element) {
|
||||
const listSection = container.createEl('div', { cls: 'focus-task-list-section' });
|
||||
const listSection = container.createEl('div', { cls: 'immerse-list-section' });
|
||||
|
||||
// Filters
|
||||
const filters = listSection.createEl('div', { cls: 'focus-task-filters' });
|
||||
const filters = listSection.createEl('div', { cls: 'immerse-filters' });
|
||||
|
||||
const filterOptions = [
|
||||
{ id: 'all', label: 'All Tasks' },
|
||||
@@ -235,7 +297,7 @@ export class FocusTaskView extends ItemView {
|
||||
|
||||
filterOptions.forEach(opt => {
|
||||
const btn = filters.createEl('button', {
|
||||
cls: `focus-task-filter-btn ${this.currentFilter === opt.id ? 'active' : ''}`,
|
||||
cls: `immerse-filter-btn ${this.currentFilter === opt.id ? 'active' : ''}`,
|
||||
text: opt.label,
|
||||
});
|
||||
btn.addEventListener('click', () => {
|
||||
@@ -245,7 +307,7 @@ export class FocusTaskView extends ItemView {
|
||||
});
|
||||
|
||||
// Task items
|
||||
const taskList = listSection.createEl('div', { cls: 'focus-task-task-list' });
|
||||
const taskList = listSection.createEl('div', { cls: 'immerse-task-list' });
|
||||
|
||||
let tasks = this.plugin.data.tasks;
|
||||
|
||||
@@ -265,24 +327,28 @@ export class FocusTaskView extends ItemView {
|
||||
});
|
||||
|
||||
if (tasks.length === 0) {
|
||||
const emptyState = taskList.createEl('div', { cls: 'focus-task-empty-state' });
|
||||
emptyState.createEl('div', { cls: 'focus-task-empty-icon', text: '📝' });
|
||||
emptyState.createEl('div', { cls: 'focus-task-empty-text', text: 'No tasks yet' });
|
||||
emptyState.createEl('div', { cls: 'focus-task-empty-hint', text: 'Add a task to get started!' });
|
||||
const emptyState = taskList.createEl('div', { cls: 'immerse-empty-state' });
|
||||
emptyState.createEl('div', { cls: 'immerse-empty-icon', text: '📝' });
|
||||
emptyState.createEl('div', { cls: 'immerse-empty-text', text: 'No tasks yet' });
|
||||
emptyState.createEl('div', { cls: 'immerse-empty-hint', text: 'Add a task to get started!' });
|
||||
} else {
|
||||
tasks.forEach(task => this.renderTaskItem(taskList, task));
|
||||
}
|
||||
}
|
||||
|
||||
renderTaskItem(container: Element, task: FocusTask) {
|
||||
renderTaskItem(container: Element, task: ImmerseTask) {
|
||||
const list = this.plugin.settings.lists.find(l => l.id === task.list);
|
||||
|
||||
// Check if task is overdue
|
||||
const isOverdue = !task.completed && task.scheduledDate && task.scheduledTime &&
|
||||
new Date(`${task.scheduledDate}T${task.scheduledTime}`).getTime() < Date.now();
|
||||
|
||||
const taskEl = container.createEl('div', {
|
||||
cls: `focus-task-task-item ${task.completed ? 'completed' : ''} ${task.isActive ? 'active' : ''}`
|
||||
cls: `immerse-task-item ${task.completed ? 'completed' : ''} ${task.isActive ? 'active' : ''} ${isOverdue ? 'overdue' : ''}`
|
||||
});
|
||||
|
||||
// Checkbox
|
||||
const checkbox = taskEl.createEl('div', { cls: 'focus-task-checkbox' });
|
||||
const checkbox = taskEl.createEl('div', { cls: 'immerse-checkbox' });
|
||||
checkbox.innerHTML = task.completed ? '✓' : '';
|
||||
checkbox.style.borderColor = list?.color || '#6366f1';
|
||||
if (task.completed) {
|
||||
@@ -297,37 +363,54 @@ export class FocusTaskView extends ItemView {
|
||||
});
|
||||
|
||||
// Task content
|
||||
const content = taskEl.createEl('div', { cls: 'focus-task-task-content' });
|
||||
const content = taskEl.createEl('div', { cls: 'immerse-task-content' });
|
||||
|
||||
const taskHeader = content.createEl('div', { cls: 'focus-task-task-header' });
|
||||
taskHeader.createEl('span', { cls: 'focus-task-task-text', text: task.text });
|
||||
const taskHeader = content.createEl('div', { cls: 'immerse-task-header' });
|
||||
taskHeader.createEl('span', { cls: 'immerse-task-text', text: task.text });
|
||||
|
||||
if (list) {
|
||||
const listBadge = taskHeader.createEl('span', {
|
||||
cls: 'focus-task-list-badge',
|
||||
cls: 'immerse-list-badge',
|
||||
text: `${list.icon} ${list.name}`,
|
||||
});
|
||||
listBadge.style.backgroundColor = list.color + '20';
|
||||
listBadge.style.color = list.color;
|
||||
}
|
||||
|
||||
const taskMeta = content.createEl('div', { cls: 'focus-task-task-meta' });
|
||||
const taskMeta = content.createEl('div', { cls: 'immerse-task-meta' });
|
||||
taskMeta.createEl('span', { text: `Est: ${this.plugin.formatTimeHuman(task.estimatedMinutes)}` });
|
||||
|
||||
if (task.actualMinutes > 0) {
|
||||
const actualSpan = taskMeta.createEl('span');
|
||||
actualSpan.setText(`Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}`);
|
||||
if (task.actualMinutes > task.estimatedMinutes) {
|
||||
actualSpan.addClass('focus-task-overtime-text');
|
||||
actualSpan.addClass('immerse-overtime-text');
|
||||
}
|
||||
}
|
||||
|
||||
// Show scheduled date/time if set
|
||||
if (task.scheduledDate) {
|
||||
const scheduleSpan = taskMeta.createEl('span', {
|
||||
cls: `immerse-schedule-badge ${isOverdue ? 'overdue' : ''}`
|
||||
});
|
||||
const dateStr = task.scheduledDate;
|
||||
const timeStr = task.scheduledTime || '';
|
||||
if (isOverdue) {
|
||||
scheduleSpan.setText(`⚠️ OVERDUE: ${dateStr}${timeStr ? ' ' + timeStr : ''}`);
|
||||
} else {
|
||||
scheduleSpan.setText(`📅 ${dateStr}${timeStr ? ' ' + timeStr : ''}`);
|
||||
}
|
||||
if (task.reminderMinutes) {
|
||||
scheduleSpan.title = `Reminder set for ${task.reminderMinutes} min before`;
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
const actions = taskEl.createEl('div', { cls: 'focus-task-task-actions' });
|
||||
const actions = taskEl.createEl('div', { cls: 'immerse-task-actions' });
|
||||
|
||||
if (!task.completed) {
|
||||
// Start pomodoro button
|
||||
const startBtn = actions.createEl('button', { cls: 'focus-task-task-btn', attr: { 'aria-label': 'Start Pomodoro' } });
|
||||
const startBtn = actions.createEl('button', { cls: 'immerse-task-btn', attr: { 'aria-label': 'Start Pomodoro' } });
|
||||
startBtn.innerHTML = '▶';
|
||||
startBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -335,7 +418,7 @@ export class FocusTaskView extends ItemView {
|
||||
});
|
||||
|
||||
// Stopwatch mode button
|
||||
const stopwatchBtn = actions.createEl('button', { cls: 'focus-task-task-btn', attr: { 'aria-label': 'Start Stopwatch' } });
|
||||
const stopwatchBtn = actions.createEl('button', { cls: 'immerse-task-btn', attr: { 'aria-label': 'Start Stopwatch' } });
|
||||
stopwatchBtn.innerHTML = '⏱';
|
||||
stopwatchBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -344,7 +427,7 @@ export class FocusTaskView extends ItemView {
|
||||
}
|
||||
|
||||
// Edit button
|
||||
const editBtn = actions.createEl('button', { cls: 'focus-task-task-btn', attr: { 'aria-label': 'Edit' } });
|
||||
const editBtn = actions.createEl('button', { cls: 'immerse-task-btn', attr: { 'aria-label': 'Edit' } });
|
||||
editBtn.innerHTML = '✏️';
|
||||
editBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -352,7 +435,7 @@ export class FocusTaskView extends ItemView {
|
||||
});
|
||||
|
||||
// Delete button
|
||||
const deleteBtn = actions.createEl('button', { cls: 'focus-task-task-btn focus-task-delete-btn', attr: { 'aria-label': 'Delete' } });
|
||||
const deleteBtn = actions.createEl('button', { cls: 'immerse-task-btn immerse-delete-btn', attr: { 'aria-label': 'Delete' } });
|
||||
deleteBtn.innerHTML = '🗑';
|
||||
deleteBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
1143
styles.css
1143
styles.css
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,10 @@
|
||||
{
|
||||
"1.0.2": "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",
|
||||
"1.1.3": "0.15.0",
|
||||
"1.1.4": "0.15.0"
|
||||
}
|
||||
Reference in New Issue
Block a user