13 Commits

Author SHA1 Message Date
8d6a20ff05 Release v1.1.4: Pomodoro & Stopwatch Mode Streamlining
Streamlined timer modes with bug fixes and code cleanup:

Bug Fixes:
- Fixed "POMODORO COMPLETE" showing in stopwatch mode
- Fixed break timer showing pomodoro notices in stopwatch mode
- Fixed pause/resume resetting timer to 0 in stopwatch mode
- Fixed "Continue Working" button appearing in stopwatch mode
- Fixed "Skip Break" button changing to "Continue" incorrectly

Improvements:
- Stopwatch mode now completely independent from pomodoro workflow
- Removed break functionality from stopwatch mode (only Pause, Complete, Stop buttons)
- Stopwatch mode maintains "FOCUSING ON" label throughout session
- Cleaned up dead code (wasStopwatchBeforeBreak flag and related logic)
- Simplified break resume logic (always returns to pomodoro mode)

Technical:
- Added isStopwatchMode flag for proper mode tracking
- Fixed toggleTimer to handle stopwatch count-up vs pomodoro countdown
- Removed unnecessary break integration code from stopwatch workflow
2025-11-25 21:41:12 +01:00
271780f48a Release v1.1.3: Emoji Picker for List Customization
Added visual emoji picker for customizing list icons:
- 1,800+ emojis across 13 categories (smileys, hearts, animals, food, activities, travel, tech, office, music, symbols, flags)
- Searchable with 130+ keyword mappings (e.g., "smile", "heart", "work", "fire")
- Responsive modal design that fits viewport (85vw max 400px, 80vh max height)
- Visual selection with highlight for current emoji
- Category organization with scrollable display
- Custom emoji input option
- Mobile-friendly touch targets (32px buttons)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 19:36:57 +01:00
264441f83b Updated README.md 2025-11-25 17:27:27 +01:00
c218162edf updated for v1.1.2 2025-11-25 17:16:13 +01:00
886a2f7372 fix: Button text overflow in stopwatch mode controls
- Fixed button text overflowing in active task controls
- Changed flex behavior to allow proper text wrapping
- Increased minimum button width from 80px to 100px
- Added word-wrap and line-height for multi-line text support
- Buttons now properly contain text like 'Continue Working'

Fixes button overflow issue in stopwatch mode where text would
extend outside button boundaries instead of wrapping.
2025-11-25 17:11:57 +01:00
68e11c57e1 v1.1.2 - Bug fixes: Fixed Text on buttons to wrap and adapt properly 2025-11-25 17:07:14 +01:00
50ef40d2e0 Release v1.1.1: Critical timer bug fix
Fixed critical bug where actual time spent on tasks was being reset to 0 when resuming work after a break in Pomodoro mode.

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

- Add scheduledDate, scheduledTime, and reminderMinutes fields to ImmerseTask
- Add enableReminders and defaultReminderMinutes to plugin settings
- Implement reminder notification system with 30-second background checks
- Add overdue task detection with visual indicators
- Add native HTML5 date/time pickers in Quick Add and Edit modals
- Add reminder dropdown with 5/10/15/30/60 minute options
- Display scheduled date/time with blue badge (📅) in task list
- Show pulsing red "OVERDUE" badge (⚠️) for past-due tasks
- Add red left border highlight for overdue tasks
- Implement startup check for due/overdue tasks when plugin loads
- Show overdue notices when Obsidian opens if tasks are past due
- Play alert sounds for reminders (if sounds enabled)
- Track shown reminders to prevent duplicate notifications
2025-11-23 21:12:21 +01:00
683c4ddafe Complete internal code renaming from FocusTask to Immerse types
- Renamed FocusTask → ImmerseTask
- Renamed FocusTaskSettings → ImmerseSettings
- Renamed FocusTaskData → ImmerseData
- Updated CSS classes: focus-task-status-* → immerse-status-*
- Updated package release script zip filename
- Updated remaining README references
- Rebuilt main.js with new type names
2025-11-23 20:12:28 +01:00
13 changed files with 3740 additions and 336 deletions

7
.gitignore vendored
View File

@@ -1,7 +1,8 @@
# Build output
# Build output (main.js is tracked for easy installation)
*.js.map
release/
*.zip
immerse-*.zip
# npm
node_modules/
@@ -21,4 +22,8 @@ data.json
# Development/Documentation (not for distribution)
RELEASE-GUIDE.md
ROADMAP.md
ROADMAP_*.md
deploy-test.bat
.claude/
RELEASE_NOTES_*.md

239
README.md
View File

@@ -4,56 +4,35 @@ A powerful task management and focus timer plugin for [Obsidian](https://obsidia
![Immerse Banner](https://img.shields.io/badge/Obsidian-Plugin-7c3aed?style=for-the-badge&logo=obsidian&logoColor=white)
![License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge)
![Version](https://img.shields.io/badge/Version-1.0.9-blue?style=for-the-badge)
![Version](https://img.shields.io/badge/Version-1.1.4-blue?style=for-the-badge)
## 🎯 Overview
Immerse brings the power of time-boxed task management directly into your Obsidian vault. Plan your day, track time with the Pomodoro technique, and crush your tasks with satisfying checkoffs - all without leaving your notes.
Task management and focus timer for Obsidian. Plan your day, track time with Pomodoro technique, and manage tasks without leaving your vault.
### Why Immerse?
## ✨ 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
- **Daily Note Logging**: Automatically log completed tasks to your daily notes with timestamps and performance metrics
### 🎨 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
@@ -88,183 +67,45 @@ npm run build
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 Immerse**: Click the ⚡ icon in the ribbon or use the command palette (`Ctrl/Cmd + P` → "Open Immerse 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 Immerse Panel" |
| Quick Add Task | `Ctrl/Cmd + P` → "Quick Add Task" |
| Toggle Timer | `Ctrl/Cmd + P` → "Toggle Timer" |
| Complete Task | `Ctrl/Cmd + P` → "Complete Current Task" |
| 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 | On |
### 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 |
### Daily Note Integration
| Setting | Description | Default |
|---------|-------------|---------|
| Log to Daily Note | Automatically log completed tasks to your daily note | Off |
When enabled, completed tasks are automatically appended to your daily note with:
- Task name and list category
- Time spent vs. estimated time
- Completion timestamp
- Performance indicator (under/over/on target)
**Example entry:**
```
- [x] Write project proposal | 💼 Work | ⏱️ 45min / 30min (15min over estimate) | ✅ 14:30
```
**Requirements:** The core "Daily Notes" plugin must be enabled in Obsidian settings. Focus Task respects your Daily Notes configuration (folder, date format, and template).
### 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
Immerse respects your Obsidian theme and adapts to both light and dark modes automatically.
## 📁 Project Structure
```
immerse/
├── 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. Immerse is my attempt to capture the essence of what makes Blitzit great while leveraging Obsidian's powerful ecosystem.
**If you're looking for a dedicated productivity app, I highly recommend checking out [Blitzit](https://www.blitzit.app/)!**
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/immerse.git
cd immerse
# Install dependencies
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/immerse
```
## 📜 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 🔗 Links
- **Repository**: [https://git.cribdev.com/crib/immerse](https://git.cribdev.com/crib/immerse)
- **Issues**: [https://git.cribdev.com/crib/immerse/issues](https://git.cribdev.com/crib/immerse/issues)
- **Inspiration**: [Blitzit App](https://www.blitzit.app/)
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> ⚡
<br><br>
<em>✨ Vibe coded with assistance from <a href="https://claude.ai">Claude.ai</a></em>
<em>Made with ❤️ for the Obsidian community • Inspired by Blitzit ⚡</em>
</p>

1089
main.js

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
{
"id": "immerse",
"name": "Immerse",
"version": "1.0.9",
"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",

View File

@@ -51,7 +51,7 @@ async function packageRelease() {
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 ../focus-task-${manifest.version}.zip *`);
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);

View File

@@ -1,6 +1,6 @@
{
"name": "immerse",
"version": "1.0.9",
"version": "1.1.4",
"description": "A Blitzit-inspired task management and focus timer plugin for Obsidian",
"main": "main.js",
"scripts": {

View File

@@ -9,9 +9,9 @@ import {
} from 'obsidian';
import {
FocusTask,
FocusTaskSettings,
FocusTaskData,
ImmerseTask,
ImmerseSettings,
ImmerseData,
DEFAULT_SETTINGS,
DEFAULT_DATA,
VIEW_TYPE_IMMERSE,
@@ -21,19 +21,21 @@ import {
} from './types';
import { ImmerseView } from './view';
import { QuickAddTaskModal } from './modals';
import { ReportView, VIEW_TYPE_REPORT } from './reportView';
import { QuickAddTaskModal, EmojiPickerModal } from './modals';
// ============ Main Plugin Class ============
export default class ImmersePlugin extends Plugin {
settings: FocusTaskSettings;
data: FocusTaskData;
settings: ImmerseSettings;
data: ImmerseData;
// Timer state
timerInterval: number | null = null;
currentTimerSeconds: number = 0;
isTimerRunning: boolean = false;
isBreakMode: boolean = false;
isStopwatchMode: boolean = false;
activeTaskId: string | null = null;
pomodoroCount: number = 0;
@@ -48,6 +50,10 @@ export default class ImmersePlugin extends Plugin {
// Status bar element
statusBarEl: HTMLElement | null = null;
// Reminder system
private reminderCheckInterval: number | null = null;
private notifiedReminders: Set<string> = new Set(); // Track which reminders have been shown
async onload() {
await this.loadAllData();
@@ -61,12 +67,17 @@ export default class ImmersePlugin extends Plugin {
}
});
// Register the main view
// Register views
this.registerView(
VIEW_TYPE_IMMERSE,
(leaf) => new ImmerseView(leaf, this)
);
this.registerView(
VIEW_TYPE_REPORT,
(leaf) => new ReportView(leaf, this)
);
// Add ribbon icon
this.addRibbonIcon('zap', 'Open Immerse', () => {
this.activateView();
@@ -103,15 +114,27 @@ export default class ImmersePlugin extends Plugin {
callback: () => this.completeActiveTask(),
});
this.addCommand({
id: 'view-reports',
name: 'View Reports',
callback: () => this.activateReportView(),
});
// Add settings tab
this.addSettingTab(new ImmerseSettingTab(this.app, this));
// Create status bar timer
this.createStatusBar();
// Start reminder checking system
if (this.settings.enableReminders) {
this.startReminderSystem();
}
}
onunload() {
this.stopTimer();
this.stopReminderSystem();
}
async loadAllData() {
@@ -181,9 +204,29 @@ export default class ImmersePlugin extends Plugin {
}
}
async activateReportView() {
const { workspace } = this.app;
// Check if report view is already open
const existingLeaves = workspace.getLeavesOfType(VIEW_TYPE_REPORT);
if (existingLeaves.length > 0) {
// If already open, just reveal it
workspace.revealLeaf(existingLeaves[0]);
} else {
// Open in a new tab in the main area
const leaf = workspace.getLeaf('tab');
await leaf.setViewState({
type: VIEW_TYPE_REPORT,
active: true,
});
workspace.revealLeaf(leaf);
}
}
// ============ Task Management ============
createTask(text: string, estimatedMinutes: number = this.settings.defaultEstimateMinutes, list: string = 'work'): FocusTask {
createTask(text: string, estimatedMinutes: number = this.settings.defaultEstimateMinutes, list: string = 'work'): ImmerseTask {
return {
id: this.generateId(),
text,
@@ -201,13 +244,13 @@ export default class ImmersePlugin extends Plugin {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
addTask(task: FocusTask) {
addTask(task: ImmerseTask) {
this.data.tasks.push(task);
this.saveAllData();
this.refreshView();
}
updateTask(taskId: string, updates: Partial<FocusTask>) {
updateTask(taskId: string, updates: Partial<ImmerseTask>) {
const task = this.data.tasks.find(t => t.id === taskId);
if (task) {
Object.assign(task, updates);
@@ -236,6 +279,12 @@ export default class ImmersePlugin extends Plugin {
this.data.completedToday++;
this.data.lastActiveDate = new Date().toDateString();
// Archive task for historical reporting
this.archiveCompletedTask(task);
// Update daily stats
this.updateDailyStats(task);
// Show celebration
if (this.settings.enableCelebrations) {
this.showCelebration(task);
@@ -269,6 +318,215 @@ export default class ImmersePlugin extends Plugin {
}
}
// ============ Data Archiving & Statistics ============
archiveCompletedTask(task: ImmerseTask) {
// Check if task was overdue
const wasOverdue = task.scheduledDate && task.scheduledTime &&
new Date(`${task.scheduledDate}T${task.scheduledTime}`).getTime() < (task.completedAt || Date.now());
const record: import('./types').CompletedTaskRecord = {
id: task.id,
text: task.text,
list: task.list,
estimatedMinutes: task.estimatedMinutes,
actualMinutes: task.actualMinutes,
createdAt: task.createdAt,
completedAt: task.completedAt || Date.now(),
scheduledDate: task.scheduledDate,
wasOverdue: wasOverdue || false,
};
this.data.completedTasksArchive.push(record);
}
updateDailyStats(task: ImmerseTask) {
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
// Find or create today's stats
let todayStats = this.data.dailyStats.find(s => s.date === today);
if (!todayStats) {
todayStats = {
date: today,
tasksCompleted: 0,
totalMinutes: 0,
pomodorosCompleted: 0,
tasksByList: {},
minutesByList: {},
};
this.data.dailyStats.push(todayStats);
}
// Update stats
todayStats.tasksCompleted++;
todayStats.totalMinutes += task.actualMinutes;
todayStats.pomodorosCompleted = this.data.pomodorosCompleted;
// Update list-specific stats
todayStats.tasksByList[task.list] = (todayStats.tasksByList[task.list] || 0) + 1;
todayStats.minutesByList[task.list] = (todayStats.minutesByList[task.list] || 0) + task.actualMinutes;
// Keep only last 365 days of stats to prevent data bloat
if (this.data.dailyStats.length > 365) {
this.data.dailyStats.sort((a, b) => b.date.localeCompare(a.date));
this.data.dailyStats = this.data.dailyStats.slice(0, 365);
}
}
// ============ Report Generation ============
generateReport(filters: import('./types').ReportFilters): import('./types').ReportData {
const { startDate, endDate, listIds } = filters;
// Filter daily stats by date range
const filteredStats = this.data.dailyStats.filter(stat => {
return stat.date >= startDate && stat.date <= endDate;
});
// Filter completed tasks by date range and lists
const filteredTasks = this.data.completedTasksArchive.filter(task => {
const taskDate = new Date(task.completedAt).toISOString().split('T')[0];
const inDateRange = taskDate >= startDate && taskDate <= endDate;
const inList = !listIds || listIds.includes(task.list);
return inDateRange && inList;
});
// Calculate summary stats
const totalTasks = filteredTasks.length;
const totalMinutes = filteredTasks.reduce((sum, task) => sum + task.actualMinutes, 0);
const totalPomodoros = filteredStats.reduce((sum, stat) => sum + stat.pomodorosCompleted, 0);
// Calculate number of days with data
const daysWithData = filteredStats.length || 1; // Avoid division by zero
const tasksPerDay = totalTasks / daysWithData;
const hoursPerDay = (totalMinutes / 60) / daysWithData;
const minsPerTask = totalTasks > 0 ? totalMinutes / totalTasks : 0;
// Time breakdown by list
const timeByListMap: Record<string, { minutes: number; taskCount: number }> = {};
filteredTasks.forEach(task => {
if (!timeByListMap[task.list]) {
timeByListMap[task.list] = { minutes: 0, taskCount: 0 };
}
timeByListMap[task.list].minutes += task.actualMinutes;
timeByListMap[task.list].taskCount++;
});
const timeByList = this.settings.lists.map(list => {
const data = timeByListMap[list.id] || { minutes: 0, taskCount: 0 };
const percentage = totalMinutes > 0 ? (data.minutes / totalMinutes) * 100 : 0;
return {
listId: list.id,
listName: list.name,
listIcon: list.icon,
listColor: list.color,
minutes: data.minutes,
taskCount: data.taskCount,
percentage: Math.round(percentage * 10) / 10, // Round to 1 decimal
};
}).filter(item => item.minutes > 0); // Only show lists with data
// Daily breakdown
const dailyBreakdown = filteredStats.map(stat => ({
date: stat.date,
tasks: stat.tasksCompleted,
hours: Math.round((stat.totalMinutes / 60) * 10) / 10,
pomodoros: stat.pomodorosCompleted,
}));
// Productivity insights
const mostProductiveHour = this.calculateMostProductiveHour(filteredTasks);
const mostProductiveDay = this.calculateMostProductiveDay(filteredStats);
const mostProductiveMonth = this.calculateMostProductiveMonth(filteredStats);
return {
totalTasks,
totalMinutes,
totalPomodoros,
tasksPerDay: Math.round(tasksPerDay * 10) / 10,
hoursPerDay: Math.round(hoursPerDay * 10) / 10,
minsPerTask: Math.round(minsPerTask),
currentStreak: this.data.streak,
timeByList,
dailyBreakdown,
mostProductiveHour,
mostProductiveDay,
mostProductiveMonth,
};
}
calculateMostProductiveHour(tasks: import('./types').CompletedTaskRecord[]): number | undefined {
if (tasks.length === 0) return undefined;
const hourCounts: Record<number, number> = {};
tasks.forEach(task => {
const hour = new Date(task.completedAt).getHours();
hourCounts[hour] = (hourCounts[hour] || 0) + 1;
});
let maxHour = 0;
let maxCount = 0;
for (const [hour, count] of Object.entries(hourCounts)) {
if (count > maxCount) {
maxCount = count;
maxHour = parseInt(hour);
}
}
return maxCount > 0 ? maxHour : undefined;
}
calculateMostProductiveDay(stats: import('./types').DailyStats[]): string | undefined {
if (stats.length === 0) return undefined;
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const dayCounts: Record<string, number> = {};
stats.forEach(stat => {
const dayOfWeek = new Date(stat.date).getDay();
const dayName = dayNames[dayOfWeek];
dayCounts[dayName] = (dayCounts[dayName] || 0) + stat.tasksCompleted;
});
let maxDay = '';
let maxCount = 0;
for (const [day, count] of Object.entries(dayCounts)) {
if (count > maxCount) {
maxCount = count;
maxDay = day;
}
}
return maxCount > 0 ? maxDay : undefined;
}
calculateMostProductiveMonth(stats: import('./types').DailyStats[]): string | undefined {
if (stats.length === 0) return undefined;
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
const monthCounts: Record<string, number> = {};
stats.forEach(stat => {
const month = new Date(stat.date).getMonth();
const monthName = monthNames[month];
monthCounts[monthName] = (monthCounts[monthName] || 0) + stat.tasksCompleted;
});
let maxMonth = '';
let maxCount = 0;
for (const [month, count] of Object.entries(monthCounts)) {
if (count > maxCount) {
maxCount = count;
maxMonth = month;
}
}
return maxCount > 0 ? maxMonth : undefined;
}
// ============ Timer Management ============
// Sync timer based on timestamp when app returns from background
@@ -287,13 +545,14 @@ export default class ImmersePlugin extends Plugin {
const task = this.data.tasks.find(t => t.id === taskId);
if (!task) return;
// Stop any existing timer
this.stopTimer();
// Stop any existing timer, preserving actual time
this.stopTimer(true);
// Set active task
this.activeTaskId = taskId;
task.isActive = true;
this.isBreakMode = false;
this.isStopwatchMode = true;
this.currentTimerSeconds = 0;
this.isTimerRunning = true;
this.secondsWorkedOnCurrentTask = task.actualMinutes * 60;
@@ -349,10 +608,11 @@ export default class ImmersePlugin extends Plugin {
const task = this.data.tasks.find(t => t.id === taskId);
if (!task) return;
this.stopTimer();
this.stopTimer(true);
this.activeTaskId = taskId;
task.isActive = true;
this.isBreakMode = false;
this.isStopwatchMode = false;
this.currentTimerSeconds = this.settings.pomodoroWorkMinutes * 60;
this.isTimerRunning = true;
@@ -509,8 +769,14 @@ export default class ImmersePlugin extends Plugin {
const elapsedMs = now - this.timerStartTimestamp;
const elapsedSeconds = Math.floor(elapsedMs / 1000);
// Update timer (countdown from paused position)
// Update timer based on mode
if (this.pausedTimeRemaining === 0 || this.isStopwatchMode) {
// Stopwatch mode - count up from paused position
this.currentTimerSeconds = this.pausedTimeRemaining + elapsedSeconds;
} else {
// Countdown mode (pomodoro/break) - count down from paused position
this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds);
}
if (task && !this.isBreakMode) {
// Update actual time worked
@@ -526,7 +792,7 @@ export default class ImmersePlugin extends Plugin {
this.updateStatusBar();
this.updateTimerDisplay();
if (this.currentTimerSeconds <= 0) {
if (this.currentTimerSeconds <= 0 && !this.isStopwatchMode) {
this.handlePomodoroEnd();
}
}, 1000);
@@ -539,7 +805,7 @@ export default class ImmersePlugin extends Plugin {
this.refreshView();
}
stopTimer() {
stopTimer(preserveActualTime: boolean = false) {
if (this.timerInterval) {
window.clearInterval(this.timerInterval);
this.timerInterval = null;
@@ -549,13 +815,15 @@ export default class ImmersePlugin extends Plugin {
const task = this.data.tasks.find(t => t.id === this.activeTaskId);
if (task) {
task.isActive = false;
// Reset actual time when manually stopping (not after a break)
// This allows starting fresh next time
// Only reset actual time when manually stopping (not when resuming after break)
if (!preserveActualTime) {
task.actualMinutes = 0;
}
}
}
this.isTimerRunning = false;
this.isStopwatchMode = false;
this.activeTaskId = null;
this.secondsWorkedOnCurrentTask = 0;
this.timerStartTimestamp = 0;
@@ -578,7 +846,7 @@ export default class ImmersePlugin extends Plugin {
createStatusBar() {
this.statusBarEl = this.addStatusBarItem();
this.statusBarEl.addClass('focus-task-status-bar');
this.statusBarEl.addClass('immerse-status-bar');
this.updateStatusBar();
// Click to open panel
@@ -597,16 +865,94 @@ export default class ImmersePlugin extends Plugin {
const icon = this.isTimerRunning ? '▶' : '⏸';
this.statusBarEl.setText(`${icon} ${timeStr} - ${taskName}${task && task.text.length > 20 ? '...' : ''}`);
this.statusBarEl.addClass('focus-task-status-active');
this.statusBarEl.addClass('immerse-status-active');
} else {
this.statusBarEl.setText('⚡ Immerse');
this.statusBarEl.removeClass('focus-task-status-active');
this.statusBarEl.removeClass('immerse-status-active');
}
}
// ============ Reminder System ============
startReminderSystem() {
// Check for reminders every 30 seconds
this.reminderCheckInterval = window.setInterval(() => {
this.checkReminders();
}, 30000);
// Also check immediately
this.checkReminders();
}
stopReminderSystem() {
if (this.reminderCheckInterval) {
window.clearInterval(this.reminderCheckInterval);
this.reminderCheckInterval = null;
}
}
checkReminders() {
if (!this.settings.enableReminders) return;
const now = new Date();
const currentTime = now.getTime();
// Check each incomplete task with scheduling
this.data.tasks
.filter(task => !task.completed && task.scheduledDate && task.scheduledTime)
.forEach(task => {
const reminderKey = `${task.id}-${task.scheduledDate}-${task.scheduledTime}`;
// Skip if we already notified for this reminder
if (this.notifiedReminders.has(reminderKey)) return;
// Parse the scheduled date and time
const scheduledDateTime = new Date(`${task.scheduledDate}T${task.scheduledTime}`);
const scheduledTime = scheduledDateTime.getTime();
// Check for overdue tasks (scheduled time has passed)
if (currentTime > scheduledTime) {
this.showOverdueNotice(task);
this.notifiedReminders.add(reminderKey);
return;
}
// Check for tasks with reminders set
if (task.reminderMinutes) {
const reminderTime = scheduledTime - (task.reminderMinutes * 60 * 1000);
// Show reminder if it's past the reminder time but before the scheduled time
if (currentTime >= reminderTime) {
this.showReminder(task);
this.notifiedReminders.add(reminderKey);
}
}
});
}
showReminder(task: ImmerseTask) {
const timeStr = task.scheduledTime;
new Notice(`🔔 Reminder: "${task.text}" is scheduled for ${timeStr}`, 8000);
// Play alert sound if enabled
if (this.settings.enableSounds) {
this.playAlertSound();
}
}
showOverdueNotice(task: ImmerseTask) {
const dateStr = task.scheduledDate;
const timeStr = task.scheduledTime;
new Notice(`⚠️ Overdue: "${task.text}" was scheduled for ${dateStr} ${timeStr}`, 10000);
// Play alert sound if enabled
if (this.settings.enableSounds) {
this.playAlertSound();
}
}
// ============ Sounds & Celebrations ============
showCelebration(task: FocusTask) {
showCelebration(task: ImmerseTask) {
let messages = CELEBRATION_MESSAGES;
let extraMessage = '';
@@ -673,7 +1019,7 @@ export default class ImmersePlugin extends Plugin {
// ============ Daily Note Logging ============
async logTaskToDailyNote(task: FocusTask) {
async logTaskToDailyNote(task: ImmerseTask) {
try {
const list = this.settings.lists.find(l => l.id === task.list);
@@ -862,15 +1208,15 @@ export default class ImmersePlugin extends Plugin {
});
}
getTasksByList(listId: string): FocusTask[] {
getTasksByList(listId: string): ImmerseTask[] {
return this.data.tasks.filter(t => t.list === listId);
}
getPendingTasks(): FocusTask[] {
getPendingTasks(): ImmerseTask[] {
return this.data.tasks.filter(t => !t.completed);
}
getTodaysTasks(): FocusTask[] {
getTodaysTasks(): ImmerseTask[] {
const today = new Date().toDateString();
return this.data.tasks.filter(t => {
if (t.scheduledDate === today) return true;
@@ -1049,12 +1395,20 @@ class ImmerseSettingTab extends PluginSettingTab {
this.plugin.settings.lists[index].name = value;
await this.plugin.saveAllData();
}))
.addText(text => text
.setValue(list.icon)
.setPlaceholder('Emoji')
.onChange(async value => {
this.plugin.settings.lists[index].icon = value;
.addButton(btn => btn
.setButtonText(list.icon || '📁')
.setTooltip('Choose emoji')
.onClick(() => {
const modal = new EmojiPickerModal(
this.app,
list.icon,
async (emoji) => {
this.plugin.settings.lists[index].icon = emoji;
await this.plugin.saveAllData();
this.display();
}
);
modal.open();
}))
.addColorPicker(picker => picker
.setValue(list.color)

View File

@@ -5,9 +5,296 @@ import {
Setting,
} from 'obsidian';
import { FocusTask } from './types';
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 {
@@ -15,11 +302,15 @@ export class QuickAddTaskModal extends Modal {
taskText: string = '';
estimatedMinutes: number;
selectedList: string = 'work';
scheduledDate: string = '';
scheduledTime: string = '';
reminderMinutes: number = 0;
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;
}
@@ -82,6 +373,45 @@ export class QuickAddTaskModal extends Modal {
dropdown.onChange(value => this.selectedList = value);
});
// Scheduled date
new Setting(contentEl)
.setName('📅 Scheduled Date')
.setDesc('Optional: When do you plan to work on this?')
.addText(text => {
text.setPlaceholder('YYYY-MM-DD')
.setValue(this.scheduledDate)
.onChange(value => this.scheduledDate = value);
text.inputEl.type = 'date';
});
// Scheduled time
new Setting(contentEl)
.setName('⏰ Scheduled Time')
.setDesc('Optional: What time?')
.addText(text => {
text.setPlaceholder('HH:mm')
.setValue(this.scheduledTime)
.onChange(value => this.scheduledTime = value);
text.inputEl.type = 'time';
});
// Reminder
if (this.plugin.settings.enableReminders) {
new Setting(contentEl)
.setName('🔔 Reminder')
.setDesc('Remind me before the scheduled time')
.addDropdown(dropdown => {
dropdown.addOption('0', 'No reminder');
dropdown.addOption('5', '5 minutes before');
dropdown.addOption('10', '10 minutes before');
dropdown.addOption('15', '15 minutes before');
dropdown.addOption('30', '30 minutes before');
dropdown.addOption('60', '1 hour before');
dropdown.setValue(this.reminderMinutes.toString());
dropdown.onChange(value => this.reminderMinutes = parseInt(value));
});
}
// Buttons
const buttonContainer = contentEl.createEl('div', { cls: 'immerse-modal-buttons' });
@@ -95,6 +425,16 @@ export class QuickAddTaskModal extends Modal {
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();
@@ -113,9 +453,9 @@ export class QuickAddTaskModal extends Modal {
export class EditTaskModal extends Modal {
plugin: ImmersePlugin;
task: FocusTask;
task: ImmerseTask;
constructor(app: App, plugin: ImmersePlugin, task: FocusTask) {
constructor(app: App, plugin: ImmersePlugin, task: ImmerseTask) {
super(app);
this.plugin = plugin;
this.task = { ...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)
@@ -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
View File

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

View File

@@ -1,6 +1,6 @@
// ============ Types & Interfaces ============
export interface FocusTask {
export interface ImmerseTask {
id: string;
text: string;
completed: boolean;
@@ -10,7 +10,9 @@ export interface FocusTask {
completedAt?: number;
list: string;
notes: string;
scheduledDate?: string;
scheduledDate?: string; // Date in YYYY-MM-DD format
scheduledTime?: string; // Time in HH:mm format (24-hour)
reminderMinutes?: number; // Minutes before scheduled time to remind (0 = no reminder)
isActive: boolean;
}
@@ -21,7 +23,7 @@ export interface TaskList {
icon: string;
}
export interface FocusTaskSettings {
export interface ImmerseSettings {
pomodoroWorkMinutes: number;
pomodoroBreakMinutes: number;
longBreakMinutes: number;
@@ -34,18 +36,47 @@ export interface FocusTaskSettings {
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,
@@ -62,19 +93,67 @@ export const DEFAULT_SETTINGS: FocusTaskSettings = {
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_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 ============
export const CELEBRATION_MESSAGES = [

View File

@@ -3,7 +3,7 @@ import {
WorkspaceLeaf,
} from 'obsidian';
import { VIEW_TYPE_IMMERSE, FocusTask } from './types';
import { VIEW_TYPE_IMMERSE, ImmerseTask } from './types';
import { QuickAddTaskModal, EditTaskModal } from './modals';
import ImmersePlugin from './main';
@@ -107,6 +107,12 @@ export class ImmerseView extends ItemView {
const actions = header.createEl('div', { cls: 'immerse-header-actions' });
const reportsBtn = actions.createEl('button', { cls: 'immerse-btn' });
reportsBtn.innerHTML = '📊 Reports';
reportsBtn.addEventListener('click', () => {
this.plugin.activateReportView();
});
const addBtn = actions.createEl('button', { cls: 'immerse-btn immerse-btn-primary' });
addBtn.innerHTML = '+ Add Task';
addBtn.addEventListener('click', () => {
@@ -121,7 +127,7 @@ export class ImmerseView extends ItemView {
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: '🔥' },
];
@@ -148,7 +154,13 @@ export class ImmerseView extends ItemView {
const breakLabel = this.plugin.currentTimerSeconds > 0 ? '☕ BREAK TIME' : '✨ BREAK COMPLETE';
activeCard.createEl('div', { cls: 'immerse-active-label', text: breakLabel });
} else {
const workLabel = this.plugin.currentTimerSeconds > 0 ? '🎯 FOCUSING ON' : '🍅 POMODORO COMPLETE';
// 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 });
}
@@ -228,8 +240,8 @@ export class ImmerseView extends ItemView {
}
} else {
// Work mode controls
if (this.plugin.currentTimerSeconds > 0) {
// Work session still running
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());
@@ -242,7 +254,7 @@ export class ImmerseView extends ItemView {
stopBtn.innerHTML = '✕ Stop';
stopBtn.addEventListener('click', () => this.plugin.stopTimer());
} else {
// Work session finished - show break and completion options
// 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());
@@ -324,11 +336,15 @@ export class ImmerseView extends ItemView {
}
}
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: `immerse-task-item ${task.completed ? 'completed' : ''} ${task.isActive ? 'active' : ''}`
cls: `immerse-task-item ${task.completed ? 'completed' : ''} ${task.isActive ? 'active' : ''} ${isOverdue ? 'overdue' : ''}`
});
// Checkbox
@@ -372,6 +388,23 @@ export class ImmerseView extends ItemView {
}
}
// Show scheduled date/time if set
if (task.scheduledDate) {
const scheduleSpan = taskMeta.createEl('span', {
cls: `immerse-schedule-badge ${isOverdue ? 'overdue' : ''}`
});
const dateStr = task.scheduledDate;
const timeStr = task.scheduledTime || '';
if (isOverdue) {
scheduleSpan.setText(`⚠️ OVERDUE: ${dateStr}${timeStr ? ' ' + timeStr : ''}`);
} else {
scheduleSpan.setText(`📅 ${dateStr}${timeStr ? ' ' + timeStr : ''}`);
}
if (task.reminderMinutes) {
scheduleSpan.title = `Reminder set for ${task.reminderMinutes} min before`;
}
}
// Actions
const actions = taskEl.createEl('div', { cls: 'immerse-task-actions' });

View File

@@ -288,11 +288,16 @@
}
.immerse-active-controls .immerse-btn {
flex: 1;
min-width: 80px;
flex: 1 1 auto;
min-width: 100px;
max-width: 100%;
background: rgba(255, 255, 255, 0.2);
color: white;
border: none;
white-space: normal;
word-wrap: break-word;
line-height: 1.3;
padding: 8px 12px;
}
.immerse-active-controls .immerse-btn:hover {
@@ -464,6 +469,33 @@
color: var(--ft-warning);
}
.immerse-schedule-badge {
padding: 2px 8px;
background: var(--ft-primary);
color: white;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.immerse-schedule-badge.overdue {
background: var(--ft-danger);
animation: pulse-overdue 2s ease-in-out infinite;
}
@keyframes pulse-overdue {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
.immerse-task-item.overdue {
border-left: 3px solid var(--ft-danger);
}
/* Task Actions */
.immerse-task-actions {
display: flex;
@@ -622,3 +654,922 @@
margin-top: 8px;
}
}
/* ============ Report View ============ */
.immerse-report-view {
padding: 20px;
overflow-y: auto;
height: 100%;
}
.immerse-report-header {
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid var(--ft-border);
}
.immerse-report-title {
font-size: 28px;
font-weight: 700;
margin: 0 0 10px 0;
color: var(--ft-text);
}
.immerse-report-filters {
background: var(--ft-bg-secondary);
padding: 12px;
border-radius: var(--ft-radius);
margin-bottom: 16px;
}
.immerse-report-filter-row {
display: flex;
gap: 12px;
margin-bottom: 12px;
flex-wrap: wrap;
align-items: center;
}
.immerse-report-filter-row .setting-item {
flex: 1;
min-width: 200px;
margin: 0 !important;
padding: 0 !important;
border: none !important;
display: flex !important;
flex-direction: row !important;
align-items: center !important;
gap: 12px;
}
.immerse-report-filter-row .setting-item > * {
margin: 0 !important;
padding: 0 !important;
}
.immerse-report-filter-row .setting-item-info {
flex-shrink: 0 !important;
width: auto !important;
min-width: 80px !important;
max-width: 100px !important;
padding: 0 !important;
margin: 0 !important;
}
.immerse-report-filter-row .setting-item-control {
flex-grow: 1 !important;
padding: 0 !important;
margin: 0 !important;
}
.immerse-report-filter-row .setting-item-name {
margin: 0 !important;
padding: 0 !important;
line-height: 1.4 !important;
}
.immerse-report-filter-row .setting-item-description {
display: none !important;
}
.immerse-report-filter-row input[type="date"] {
width: 100% !important;
margin: 0 !important;
}
.immerse-report-quick-filters {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.immerse-filter-label {
font-weight: 500;
color: var(--ft-text);
font-size: 14px;
}
.immerse-quick-filter-btn {
padding: 6px 12px;
border-radius: 6px;
background: var(--ft-bg);
border: 1px solid var(--ft-border);
color: var(--ft-text);
cursor: pointer;
transition: all 0.2s ease;
font-size: 13px;
white-space: nowrap;
}
.immerse-quick-filter-btn:hover {
background: var(--ft-primary);
color: white;
border-color: var(--ft-primary);
}
.immerse-report-generate-btn {
width: 100%;
margin-bottom: 20px;
}
.immerse-report-content {
margin-top: 20px;
}
.immerse-no-data-message {
text-align: center;
padding: 60px 20px;
color: var(--ft-text-muted);
font-size: 16px;
}
/* Stats Grid */
.immerse-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
margin-bottom: 30px;
}
.immerse-stat-card {
background: var(--ft-bg-secondary);
padding: 16px;
border-radius: var(--ft-radius);
text-align: center;
border: 1px solid var(--ft-border);
}
.immerse-stat-label {
font-size: 11px;
font-weight: 600;
color: var(--ft-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 10px;
}
.immerse-stat-value-row {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.immerse-stat-icon {
font-size: 24px;
}
.immerse-stat-value {
font-size: 28px;
font-weight: 700;
color: var(--ft-text);
}
/* Report Sections */
.immerse-report-section {
margin-bottom: 40px;
}
.immerse-report-section-title {
font-size: 20px;
font-weight: 700;
margin-bottom: 20px;
color: var(--ft-text);
padding-bottom: 10px;
border-bottom: 2px solid var(--ft-border);
position: relative;
}
.immerse-report-section-title::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 60px;
height: 2px;
background: linear-gradient(90deg, var(--ft-primary), var(--ft-success));
}
/* Time by List */
.immerse-time-by-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.immerse-list-stat-item {
background: var(--ft-bg-secondary);
padding: 16px;
border-radius: 10px;
border: 1px solid var(--ft-border);
transition: all 0.2s ease;
}
.immerse-list-stat-item:hover {
transform: translateX(4px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.immerse-list-info {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.immerse-list-icon {
font-size: 22px;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
}
.immerse-list-name {
font-weight: 600;
color: var(--ft-text);
font-size: 16px;
}
.immerse-list-progress {
width: 100%;
height: 10px;
background: var(--ft-bg);
border-radius: 5px;
overflow: hidden;
margin-bottom: 10px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
}
.immerse-list-progress-fill {
height: 100%;
transition: width 0.6s ease;
border-radius: 5px;
}
.immerse-list-stats {
display: flex;
justify-content: space-between;
font-size: 13px;
gap: 8px;
flex-wrap: wrap;
}
.immerse-list-stat-text {
color: var(--ft-text-muted);
}
.immerse-list-stat-percentage {
font-weight: 600;
color: var(--ft-text);
}
/* Insights */
.immerse-insights-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
}
.immerse-insight-card {
background: var(--ft-bg-secondary);
padding: 28px;
border-radius: 16px;
text-align: center;
border: 2px solid var(--ft-border);
position: relative;
overflow: visible;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.immerse-insight-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: var(--ft-primary);
border-radius: 16px 16px 0 0;
}
.immerse-insight-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
border-color: var(--ft-primary);
}
.immerse-insight-icon {
font-size: 48px;
margin-bottom: 12px;
display: block;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
}
.immerse-insight-label {
font-size: 10px;
font-weight: 700;
color: var(--ft-text-muted);
text-transform: uppercase;
letter-spacing: 1.5px;
margin-bottom: 16px;
display: block;
}
.immerse-insight-value {
font-size: 32px;
font-weight: 800;
color: var(--ft-primary);
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
line-height: 1.2;
display: block;
}
/* Daily Breakdown - Enhanced Visual Chart Style */
.immerse-daily-breakdown-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.immerse-daily-row {
display: grid;
grid-template-columns: 110px 1fr;
gap: 20px;
align-items: center;
padding: 20px;
background: linear-gradient(135deg, var(--ft-bg-secondary) 0%, var(--ft-bg) 100%);
border-radius: 12px;
border: 2px solid var(--ft-border);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.immerse-daily-row::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: linear-gradient(180deg, var(--ft-primary), var(--ft-success));
opacity: 0;
transition: opacity 0.3s ease;
}
.immerse-daily-row:hover::before {
opacity: 1;
}
.immerse-daily-row:hover {
transform: translateX(6px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
border-color: var(--ft-primary);
}
.immerse-daily-date {
font-weight: 800;
color: var(--ft-text);
font-size: 14px;
text-align: center;
display: flex;
flex-direction: column;
gap: 4px;
}
.immerse-daily-date-day {
font-size: 11px;
color: var(--ft-text-muted);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
}
.immerse-daily-date-num {
font-size: 16px;
color: var(--ft-primary);
}
.immerse-daily-bars {
display: flex;
flex-direction: column;
gap: 12px;
flex: 1;
}
.immerse-daily-bar-row {
display: flex;
align-items: center;
gap: 12px;
}
.immerse-daily-bar-label {
min-width: 90px;
font-size: 12px;
color: var(--ft-text-muted);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 6px;
}
.immerse-daily-bar-icon {
font-size: 16px;
}
.immerse-daily-bar-track {
flex: 1;
height: 12px;
background: var(--ft-bg);
border-radius: 6px;
overflow: hidden;
position: relative;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
}
.immerse-daily-bar-fill {
height: 100%;
border-radius: 6px;
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
/* Pie chart for daily breakdown */
.immerse-daily-pie-container {
display: flex;
align-items: center;
justify-content: center;
gap: 40px;
margin-top: 30px;
}
.immerse-daily-pie-chart {
position: relative;
width: 280px;
height: 280px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
/* Background set via inline style for proper gradient rendering */
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
transition: transform 0.3s ease;
}
.immerse-daily-pie-chart:hover {
transform: scale(1.05);
}
.immerse-daily-pie-center {
position: absolute;
width: 160px;
height: 160px;
background: var(--ft-bg);
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
.immerse-daily-pie-center-value {
font-size: 36px;
font-weight: 800;
color: var(--ft-text-primary);
line-height: 1;
}
.immerse-daily-pie-center-label {
font-size: 12px;
font-weight: 500;
color: var(--ft-text-secondary);
text-transform: uppercase;
letter-spacing: 1px;
margin-top: 4px;
}
.immerse-daily-pie-legend {
display: flex;
flex-direction: column;
gap: 20px;
}
.immerse-daily-pie-legend-item {
display: flex;
align-items: center;
gap: 12px;
}
.immerse-daily-pie-legend-color {
width: 24px;
height: 24px;
border-radius: 6px;
flex-shrink: 0;
}
.immerse-daily-pie-legend-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.immerse-daily-pie-legend-label {
font-size: 13px;
font-weight: 600;
color: var(--ft-text-primary);
display: flex;
align-items: center;
gap: 6px;
}
.immerse-daily-pie-legend-value {
font-size: 20px;
font-weight: 700;
color: var(--ft-text-primary);
}
.immerse-daily-pie-legend-percentage {
font-size: 12px;
color: var(--ft-text-secondary);
font-weight: 500;
}
.immerse-daily-bar-fill.tasks {
background: linear-gradient(90deg, var(--ft-primary), #7c3aed);
}
.immerse-daily-bar-fill.hours {
background: linear-gradient(90deg, var(--ft-success), #059669);
}
.immerse-daily-bar-fill.pomodoros {
background: linear-gradient(90deg, var(--ft-warning), #dc2626);
}
.immerse-daily-bar-value {
min-width: 50px;
text-align: right;
font-weight: 800;
font-size: 14px;
color: var(--ft-text);
font-feature-settings: 'tnum';
font-variant-numeric: tabular-nums;
}
/* Responsive adjustments for smaller screens */
@media (max-width: 768px) {
.immerse-report-view {
padding: 15px;
}
.immerse-stats-grid {
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
}
.immerse-insights-grid {
grid-template-columns: 1fr;
}
.immerse-report-filter-row {
flex-direction: column;
}
.immerse-report-filter-row .setting-item {
min-width: 100%;
}
/* Pie chart mobile optimization */
.immerse-daily-pie-container {
flex-direction: column;
gap: 30px;
}
.immerse-daily-pie-chart {
width: 220px;
height: 220px;
}
.immerse-daily-pie-center {
width: 130px;
height: 130px;
}
.immerse-daily-pie-center-value {
font-size: 28px;
}
/* Daily breakdown mobile optimization */
.immerse-daily-row {
flex-direction: column;
gap: 12px;
}
.immerse-daily-date {
flex-direction: row;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
.immerse-daily-bars {
width: 100%;
}
.immerse-daily-bar-label {
min-width: 70px;
font-size: 11px;
}
.immerse-daily-bar-track {
flex: 1;
}
.immerse-daily-bar-value {
min-width: 40px;
font-size: 12px;
}
/* Task list mobile optimization */
.immerse-task-item {
padding: 10px 12px;
font-size: 14px;
}
.immerse-task-actions {
opacity: 1;
}
.immerse-task-btn {
width: 36px;
height: 36px;
font-size: 16px;
}
/* Active card mobile optimization */
.immerse-active-card {
padding: 20px 16px;
}
.immerse-timer-time {
font-size: 40px;
}
/* Stats bar mobile optimization */
.immerse-stats-bar {
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.immerse-stat-item {
padding: 10px;
}
.immerse-stat-value {
font-size: 16px;
}
.immerse-stat-label {
font-size: 10px;
}
/* Modal optimization for mobile */
.immerse-modal {
padding: 16px;
max-height: 80vh;
overflow-y: auto;
}
.immerse-modal-buttons {
flex-direction: column;
gap: 10px;
}
.immerse-modal-buttons .immerse-btn {
width: 100%;
padding: 12px 16px;
font-size: 14px;
min-height: 44px;
}
/* Increase touch target sizes */
.immerse-checkbox {
width: 28px;
height: 28px;
min-width: 28px;
}
.immerse-filter-btn {
padding: 8px 14px;
font-size: 14px;
min-height: 40px;
}
/* Better spacing for buttons */
.immerse-header-actions {
width: 100%;
justify-content: stretch;
}
.immerse-header-actions .immerse-btn {
flex: 1;
min-height: 44px;
}
}
/* Extra small screens (phones in portrait) */
@media (max-width: 480px) {
.immerse-container {
padding: 12px;
}
.immerse-report-view {
padding: 12px;
}
/* Make pie chart smaller on very small screens */
.immerse-daily-pie-chart {
width: 180px;
height: 180px;
}
.immerse-daily-pie-center {
width: 110px;
height: 110px;
}
.immerse-daily-pie-center-value {
font-size: 24px;
}
.immerse-daily-pie-center-label {
font-size: 10px;
}
.immerse-daily-pie-legend-value {
font-size: 18px;
}
/* Stack stats grid in single column on very small screens */
.immerse-stats-grid {
grid-template-columns: 1fr;
}
/* Smaller buttons and text */
.immerse-btn {
padding: 6px 12px;
font-size: 13px;
}
.immerse-timer-time {
font-size: 36px;
}
.immerse-active-task-name {
font-size: 18px;
}
/* Quick filters wrap better */
.immerse-quick-filter-btn {
padding: 5px 10px;
font-size: 12px;
}
/* Report title smaller */
.immerse-report-title {
font-size: 24px;
}
.immerse-report-section-title {
font-size: 18px;
}
}
/* ============ Emoji Picker ============ */
.immerse-emoji-picker {
width: 85vw;
max-width: 400px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.immerse-emoji-picker .modal-content {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
box-sizing: border-box;
max-width: 100%;
}
.immerse-emoji-current {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding: 10px;
background: var(--ft-bg-secondary);
border-radius: var(--ft-radius-sm);
flex-shrink: 0;
}
.immerse-emoji-current-icon {
font-size: 20px;
}
.immerse-emoji-search {
margin-bottom: 12px;
flex-shrink: 0;
}
.immerse-emoji-search-input {
width: 100%;
padding: 8px 12px;
border-radius: var(--ft-radius-sm);
border: 1px solid var(--ft-border);
background: var(--ft-bg);
color: var(--ft-text);
font-size: 14px;
}
.immerse-emoji-categories {
flex: 1;
overflow-y: auto;
margin-bottom: 12px;
min-height: 0;
}
.immerse-emoji-category {
margin-bottom: 20px;
}
.immerse-emoji-category-title {
font-size: 14px;
font-weight: 600;
color: var(--ft-text-muted);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.immerse-emoji-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(32px, 1fr));
gap: 4px;
max-width: 100%;
box-sizing: border-box;
}
.immerse-emoji-button {
width: 32px;
height: 32px;
border: 1px solid var(--ft-border);
background: var(--ft-bg);
border-radius: 6px;
font-size: 18px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
box-sizing: border-box;
}
.immerse-emoji-button:hover {
background: var(--ft-bg-tertiary);
transform: scale(1.1);
border-color: var(--ft-primary);
}
.immerse-emoji-button.immerse-emoji-selected {
background: var(--ft-primary);
border-color: var(--ft-primary);
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3);
}
.immerse-emoji-custom {
display: flex;
align-items: center;
gap: 8px;
padding-top: 12px;
border-top: 1px solid var(--ft-border);
flex-shrink: 0;
}
.immerse-emoji-custom-input {
flex: 1;
padding: 8px 12px;
border-radius: var(--ft-radius-sm);
border: 1px solid var(--ft-border);
background: var(--ft-bg);
color: var(--ft-text);
font-size: 18px;
text-align: center;
}

View File

@@ -4,5 +4,7 @@
"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.0.9": "0.15.0",
"1.1.3": "0.15.0",
"1.1.4": "0.15.0"
}