Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8207b3626e | |||
| f1af574eb9 | |||
| 2fad5d88ab | |||
| 683c4ddafe |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -21,4 +21,7 @@ data.json
|
|||||||
|
|
||||||
# Development/Documentation (not for distribution)
|
# Development/Documentation (not for distribution)
|
||||||
RELEASE-GUIDE.md
|
RELEASE-GUIDE.md
|
||||||
|
ROADMAP.md
|
||||||
|
deploy-test.bat
|
||||||
.claude/
|
.claude/
|
||||||
|
RELEASE_NOTES_v1.1.0.md
|
||||||
|
|||||||
29
README.md
29
README.md
@@ -4,7 +4,7 @@ A powerful task management and focus timer plugin for [Obsidian](https://obsidia
|
|||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## 🎯 Overview
|
## 🎯 Overview
|
||||||
|
|
||||||
@@ -48,6 +48,29 @@ Immerse brings the power of time-boxed task management directly into your Obsidi
|
|||||||
- **Pomodoro Count**: Track total pomodoros completed
|
- **Pomodoro Count**: Track total pomodoros completed
|
||||||
- **Daily Note Logging**: Automatically log completed tasks to your daily notes with timestamps and performance metrics
|
- **Daily Note Logging**: Automatically log completed tasks to your daily notes with timestamps and performance metrics
|
||||||
|
|
||||||
|
### 📈 Reporting & Analytics (New in v1.1.0!)
|
||||||
|
- **Comprehensive Reports**: View detailed productivity reports with date range filtering
|
||||||
|
- **Key Metrics Dashboard**: Track tasks done, tasks per day, hours per day, minutes per task, and day streaks
|
||||||
|
- **Visual Analytics**: Pie charts showing task distribution and time allocation
|
||||||
|
- **Time by List**: See how much time you spend on different task categories
|
||||||
|
- **Productivity Insights**: Discover your most productive hour, day, and month
|
||||||
|
- **Daily Breakdown**: Visual bar charts showing last 10 days of activity
|
||||||
|
- **Quick Filters**: Today, Last 7/30/90 days for easy report generation
|
||||||
|
|
||||||
|
### 🗓️ Task Scheduling & Reminders (New in v1.1.0!)
|
||||||
|
- **Schedule Tasks**: Set specific date and time for tasks
|
||||||
|
- **Smart Reminders**: Get notifications before task is due (5/10/15/30/60 minute options)
|
||||||
|
- **Overdue Detection**: Visual indicators (⚠️ red badge) for past-due tasks
|
||||||
|
- **Startup Checks**: Alerts when opening Obsidian if tasks are overdue
|
||||||
|
- **Background Monitoring**: Automatic 30-second checks for due tasks
|
||||||
|
- **Sound Alerts**: Optional audio notifications for reminders
|
||||||
|
|
||||||
|
### 📱 Mobile Optimized (New in v1.1.0!)
|
||||||
|
- **Responsive Design**: Fully optimized for mobile screens (tablets and phones)
|
||||||
|
- **Touch-Friendly**: Larger buttons and tap targets (44px minimum)
|
||||||
|
- **Adaptive Layouts**: Charts and visualizations scale appropriately
|
||||||
|
- **Mobile Testing**: Works great on Obsidian mobile app
|
||||||
|
|
||||||
### 🎨 User Experience
|
### 🎨 User Experience
|
||||||
- **Status Bar Timer**: Timer that stays visible while you work
|
- **Status Bar Timer**: Timer that stays visible while you work
|
||||||
- **Celebration Messages**: Fun, randomized messages when you complete tasks
|
- **Celebration Messages**: Fun, randomized messages when you complete tasks
|
||||||
@@ -169,7 +192,7 @@ When enabled, completed tasks are automatically appended to your daily note with
|
|||||||
- [x] Write project proposal | 💼 Work | ⏱️ 45min / 30min (15min over estimate) | ✅ 14:30
|
- [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).
|
**Requirements:** The core "Daily Notes" plugin must be enabled in Obsidian settings. Immerse respects your Daily Notes configuration (folder, date format, and template).
|
||||||
|
|
||||||
### Lists
|
### Lists
|
||||||
Customize your task lists with:
|
Customize your task lists with:
|
||||||
@@ -182,7 +205,7 @@ Default lists: Work 💼, Personal 🏠, Learning 📚
|
|||||||
## 🎨 Customization
|
## 🎨 Customization
|
||||||
|
|
||||||
### Adding Custom Lists
|
### Adding Custom Lists
|
||||||
1. Go to Settings → Focus Task → Lists
|
1. Go to Settings → Immerse → Lists
|
||||||
2. Click "+ Add List"
|
2. Click "+ Add List"
|
||||||
3. Set the name, emoji, and color
|
3. Set the name, emoji, and color
|
||||||
4. Click Save
|
4. Click Save
|
||||||
|
|||||||
166
RELEASE_NOTES_v1.1.0.md
Normal file
166
RELEASE_NOTES_v1.1.0.md
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# Immerse v1.1.0 Release Notes
|
||||||
|
|
||||||
|
Release Date: November 24, 2024
|
||||||
|
|
||||||
|
## 🎉 What's New
|
||||||
|
|
||||||
|
### 📈 Reporting & Analytics System
|
||||||
|
A complete analytics dashboard to track your productivity over time!
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- **Key Metrics Dashboard**: View tasks completed, tasks per day, hours per day, minutes per task, and current day streak
|
||||||
|
- **Visual Charts**: Beautiful pie chart showing distribution of tasks, hours, and pomodoros
|
||||||
|
- **Time by List Analysis**: See exactly how much time you spend on different task categories
|
||||||
|
- **Productivity Insights**: Discover your most productive hour, day, and month
|
||||||
|
- **Daily Breakdown**: Visual bar charts showing the last 10 days of activity with tasks, hours, and pomodoros
|
||||||
|
- **Flexible Filtering**: Quick filters (Today, Last 7/30/90 days) plus custom date range selection
|
||||||
|
- **Report View**: Dedicated view accessible via "📊 Reports" button
|
||||||
|
|
||||||
|
### 🗓️ Task Scheduling & Reminders
|
||||||
|
Never miss a deadline with the new scheduling system!
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- **Schedule Tasks**: Set specific date and time when tasks are due
|
||||||
|
- **Smart Reminders**: Choose notification timing (5, 10, 15, 30, or 60 minutes before due time)
|
||||||
|
- **Visual Indicators**:
|
||||||
|
- Blue 📅 badge shows scheduled date/time
|
||||||
|
- Red ⚠️ pulsing "OVERDUE" badge for past-due tasks
|
||||||
|
- Red left border highlight on overdue tasks
|
||||||
|
- **Background Monitoring**: Automatic checks every 30 seconds for upcoming/overdue tasks
|
||||||
|
- **Startup Alerts**: Get notified when opening Obsidian if tasks are overdue
|
||||||
|
- **Sound Notifications**: Optional audio alerts for reminders (respects sound settings)
|
||||||
|
- **Duplicate Prevention**: Smart tracking ensures you don't get reminded multiple times
|
||||||
|
|
||||||
|
### 📱 Mobile Optimization
|
||||||
|
Full responsive design for excellent mobile experience!
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Responsive Layouts**: All UI elements adapt to mobile screen sizes
|
||||||
|
- **Touch-Friendly**:
|
||||||
|
- Minimum 44px tap targets (Apple's recommended size)
|
||||||
|
- Larger checkboxes (28px on mobile)
|
||||||
|
- Larger action buttons
|
||||||
|
- **Optimized Views**:
|
||||||
|
- Pie charts scale down appropriately (220px on tablets, 180px on phones)
|
||||||
|
- Stats grid adapts (2 columns on tablets, 1 column on small phones)
|
||||||
|
- Daily breakdown bars stack vertically on mobile
|
||||||
|
- Modal buttons become full-width and stack
|
||||||
|
- **Always Visible Actions**: Task action buttons always visible on mobile (no hover needed)
|
||||||
|
- **Adaptive Typography**: Font sizes scale appropriately for readability
|
||||||
|
|
||||||
|
## 🔧 Technical Improvements
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Efficient report generation with on-demand calculation
|
||||||
|
- Optimized reminder checks with 30-second intervals
|
||||||
|
- Smart caching to prevent duplicate notifications
|
||||||
|
|
||||||
|
### Code Structure
|
||||||
|
- New `reportView.ts` file for analytics view
|
||||||
|
- Enhanced type definitions for scheduling and reporting
|
||||||
|
- Improved data tracking for historical statistics
|
||||||
|
- Better separation of concerns
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
- Fully compatible with Obsidian desktop and mobile
|
||||||
|
- Works with both light and dark themes
|
||||||
|
- Respects user's sound and notification preferences
|
||||||
|
|
||||||
|
## 📊 Statistics Tracked
|
||||||
|
|
||||||
|
The plugin now tracks comprehensive statistics including:
|
||||||
|
- Total tasks completed
|
||||||
|
- Total time spent (all-time and daily)
|
||||||
|
- Total pomodoros completed
|
||||||
|
- Tasks per day average
|
||||||
|
- Hours per day average
|
||||||
|
- Minutes per task average
|
||||||
|
- Current day streak
|
||||||
|
- Most productive hour/day/month
|
||||||
|
- Time breakdown by task list
|
||||||
|
|
||||||
|
## 🎨 UI/UX Enhancements
|
||||||
|
|
||||||
|
- New "📊 Reports" button in main view header
|
||||||
|
- Modern, clean report interface with gradient accents
|
||||||
|
- Color-coded progress bars using list colors
|
||||||
|
- Hover effects and animations for better interactivity
|
||||||
|
- Responsive filter controls
|
||||||
|
- Improved date picker styling and alignment
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Fixed pie chart rendering issues (now uses proper color values)
|
||||||
|
- Improved gradient calculation for proper segment display
|
||||||
|
- Better normalization of metrics (using time-based calculations)
|
||||||
|
- Fixed date input alignment in report filters
|
||||||
|
|
||||||
|
## ⚙️ Settings Updates
|
||||||
|
|
||||||
|
### New Settings:
|
||||||
|
- **Enable Reminders**: Toggle reminder notifications on/off
|
||||||
|
- **Default Reminder Minutes**: Set default reminder time for new scheduled tasks (default: 30 minutes)
|
||||||
|
|
||||||
|
### Updated Settings:
|
||||||
|
All existing settings remain compatible with no migration required.
|
||||||
|
|
||||||
|
## 📱 Mobile Testing Recommendations
|
||||||
|
|
||||||
|
To ensure the best experience on mobile:
|
||||||
|
|
||||||
|
1. **Test on actual device**: Install Obsidian mobile and test the plugin
|
||||||
|
2. **Browser DevTools**: Use Chrome/Edge DevTools device emulation
|
||||||
|
3. **Responsive breakpoints**:
|
||||||
|
- Desktop: >768px
|
||||||
|
- Tablet: ≤768px
|
||||||
|
- Phone: ≤480px
|
||||||
|
|
||||||
|
## 🔄 Upgrade Instructions
|
||||||
|
|
||||||
|
### From v1.0.x:
|
||||||
|
|
||||||
|
1. **Backup your data** (optional but recommended):
|
||||||
|
- Your tasks and settings are in `.obsidian/plugins/immerse/data.json`
|
||||||
|
- Make a copy before updating
|
||||||
|
|
||||||
|
2. **Update files**:
|
||||||
|
- Copy `main.js`, `manifest.json`, and `styles.css` to your vault
|
||||||
|
- **DO NOT replace `data.json`** - this contains your tasks!
|
||||||
|
|
||||||
|
3. **Reload Obsidian**:
|
||||||
|
- Press Ctrl/Cmd+R to reload
|
||||||
|
- Or restart Obsidian
|
||||||
|
|
||||||
|
4. **Verify**:
|
||||||
|
- Check that your tasks are still there
|
||||||
|
- Click "📊 Reports" to see your new analytics
|
||||||
|
- Try scheduling a task with a reminder
|
||||||
|
|
||||||
|
## 🚀 What's Next (v1.2.0)
|
||||||
|
|
||||||
|
Future enhancements we're considering:
|
||||||
|
- Calendar view integration
|
||||||
|
- Export reports (PDF/CSV)
|
||||||
|
- More chart types and visualizations
|
||||||
|
- Task templates
|
||||||
|
- Recurring tasks
|
||||||
|
- Advanced filtering options
|
||||||
|
|
||||||
|
## 💡 Feedback
|
||||||
|
|
||||||
|
Found a bug or have a feature request?
|
||||||
|
- Open an issue: [https://git.cribdev.com/crib/immerse/issues](https://git.cribdev.com/crib/immerse/issues)
|
||||||
|
- Join the discussion in the repository
|
||||||
|
|
||||||
|
## 🙏 Credits
|
||||||
|
|
||||||
|
Special thanks to:
|
||||||
|
- [Blitzit](https://www.blitzit.app/) for continued inspiration
|
||||||
|
- The Obsidian community for feedback and support
|
||||||
|
- Claude.ai for development assistance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Enjoy v1.1.0!** ⚡
|
||||||
|
|
||||||
|
Made with ❤️ for the Obsidian community
|
||||||
689
main.js
689
main.js
@@ -27,7 +27,7 @@ __export(main_exports, {
|
|||||||
default: () => ImmersePlugin
|
default: () => ImmersePlugin
|
||||||
});
|
});
|
||||||
module.exports = __toCommonJS(main_exports);
|
module.exports = __toCommonJS(main_exports);
|
||||||
var import_obsidian3 = require("obsidian");
|
var import_obsidian4 = require("obsidian");
|
||||||
|
|
||||||
// src/types.ts
|
// src/types.ts
|
||||||
var DEFAULT_SETTINGS = {
|
var DEFAULT_SETTINGS = {
|
||||||
@@ -46,7 +46,10 @@ var DEFAULT_SETTINGS = {
|
|||||||
autoStartBreak: true,
|
autoStartBreak: true,
|
||||||
tickSoundEnabled: false,
|
tickSoundEnabled: false,
|
||||||
// Daily note logging
|
// Daily note logging
|
||||||
logToDaily: false
|
logToDaily: false,
|
||||||
|
// Task reminders
|
||||||
|
enableReminders: true,
|
||||||
|
defaultReminderMinutes: 15
|
||||||
};
|
};
|
||||||
var DEFAULT_DATA = {
|
var DEFAULT_DATA = {
|
||||||
tasks: [],
|
tasks: [],
|
||||||
@@ -54,7 +57,9 @@ var DEFAULT_DATA = {
|
|||||||
totalFocusMinutesToday: 0,
|
totalFocusMinutesToday: 0,
|
||||||
streak: 0,
|
streak: 0,
|
||||||
lastActiveDate: "",
|
lastActiveDate: "",
|
||||||
pomodorosCompleted: 0
|
pomodorosCompleted: 0,
|
||||||
|
dailyStats: [],
|
||||||
|
completedTasksArchive: []
|
||||||
};
|
};
|
||||||
var VIEW_TYPE_IMMERSE = "immerse-view";
|
var VIEW_TYPE_IMMERSE = "immerse-view";
|
||||||
var CELEBRATION_MESSAGES = [
|
var CELEBRATION_MESSAGES = [
|
||||||
@@ -90,8 +95,12 @@ var QuickAddTaskModal = class extends import_obsidian.Modal {
|
|||||||
super(app);
|
super(app);
|
||||||
this.taskText = "";
|
this.taskText = "";
|
||||||
this.selectedList = "work";
|
this.selectedList = "work";
|
||||||
|
this.scheduledDate = "";
|
||||||
|
this.scheduledTime = "";
|
||||||
|
this.reminderMinutes = 0;
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.estimatedMinutes = plugin.settings.defaultEstimateMinutes;
|
this.estimatedMinutes = plugin.settings.defaultEstimateMinutes;
|
||||||
|
this.reminderMinutes = plugin.settings.defaultReminderMinutes;
|
||||||
if (plugin.settings.lists.length > 0) {
|
if (plugin.settings.lists.length > 0) {
|
||||||
this.selectedList = plugin.settings.lists[0].id;
|
this.selectedList = plugin.settings.lists[0].id;
|
||||||
}
|
}
|
||||||
@@ -137,6 +146,26 @@ var QuickAddTaskModal = class extends import_obsidian.Modal {
|
|||||||
dropdown.setValue(this.selectedList);
|
dropdown.setValue(this.selectedList);
|
||||||
dropdown.onChange((value) => this.selectedList = value);
|
dropdown.onChange((value) => this.selectedList = value);
|
||||||
});
|
});
|
||||||
|
new import_obsidian.Setting(contentEl).setName("\u{1F4C5} 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";
|
||||||
|
});
|
||||||
|
new import_obsidian.Setting(contentEl).setName("\u23F0 Scheduled Time").setDesc("Optional: What time?").addText((text) => {
|
||||||
|
text.setPlaceholder("HH:mm").setValue(this.scheduledTime).onChange((value) => this.scheduledTime = value);
|
||||||
|
text.inputEl.type = "time";
|
||||||
|
});
|
||||||
|
if (this.plugin.settings.enableReminders) {
|
||||||
|
new import_obsidian.Setting(contentEl).setName("\u{1F514} 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));
|
||||||
|
});
|
||||||
|
}
|
||||||
const buttonContainer = contentEl.createEl("div", { cls: "immerse-modal-buttons" });
|
const buttonContainer = contentEl.createEl("div", { cls: "immerse-modal-buttons" });
|
||||||
const cancelBtn = buttonContainer.createEl("button", { text: "Cancel", cls: "immerse-btn" });
|
const cancelBtn = buttonContainer.createEl("button", { text: "Cancel", cls: "immerse-btn" });
|
||||||
cancelBtn.addEventListener("click", () => this.close());
|
cancelBtn.addEventListener("click", () => this.close());
|
||||||
@@ -146,6 +175,15 @@ var QuickAddTaskModal = class extends import_obsidian.Modal {
|
|||||||
submitTask() {
|
submitTask() {
|
||||||
if (this.taskText.trim()) {
|
if (this.taskText.trim()) {
|
||||||
const task = this.plugin.createTask(this.taskText, this.estimatedMinutes, this.selectedList);
|
const task = this.plugin.createTask(this.taskText, this.estimatedMinutes, this.selectedList);
|
||||||
|
if (this.scheduledDate) {
|
||||||
|
task.scheduledDate = this.scheduledDate;
|
||||||
|
}
|
||||||
|
if (this.scheduledTime) {
|
||||||
|
task.scheduledTime = this.scheduledTime;
|
||||||
|
}
|
||||||
|
if (this.reminderMinutes > 0 && this.scheduledDate && this.scheduledTime) {
|
||||||
|
task.reminderMinutes = this.reminderMinutes;
|
||||||
|
}
|
||||||
this.plugin.addTask(task);
|
this.plugin.addTask(task);
|
||||||
new import_obsidian.Notice("\u2705 Task added!");
|
new import_obsidian.Notice("\u2705 Task added!");
|
||||||
this.close();
|
this.close();
|
||||||
@@ -201,6 +239,29 @@ var EditTaskModal = class extends import_obsidian.Modal {
|
|||||||
textarea.setValue(this.task.notes).onChange((value) => this.task.notes = value);
|
textarea.setValue(this.task.notes).onChange((value) => this.task.notes = value);
|
||||||
textarea.inputEl.rows = 4;
|
textarea.inputEl.rows = 4;
|
||||||
});
|
});
|
||||||
|
new import_obsidian.Setting(contentEl).setName("\u{1F4C5} 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 || void 0);
|
||||||
|
text.inputEl.type = "date";
|
||||||
|
});
|
||||||
|
new import_obsidian.Setting(contentEl).setName("\u23F0 Scheduled Time").setDesc("Optional: What time?").addText((text) => {
|
||||||
|
text.setPlaceholder("HH:mm").setValue(this.task.scheduledTime || "").onChange((value) => this.task.scheduledTime = value || void 0);
|
||||||
|
text.inputEl.type = "time";
|
||||||
|
});
|
||||||
|
if (this.plugin.settings.enableReminders) {
|
||||||
|
new import_obsidian.Setting(contentEl).setName("\u{1F514} 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 : void 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
if (this.task.actualMinutes > 0) {
|
if (this.task.actualMinutes > 0) {
|
||||||
new import_obsidian.Setting(contentEl).setName("Time Tracked").setDesc(`You've worked on this task for ${this.plugin.formatTimeHuman(this.task.actualMinutes)}`);
|
new import_obsidian.Setting(contentEl).setName("Time Tracked").setDesc(`You've worked on this task for ${this.plugin.formatTimeHuman(this.task.actualMinutes)}`);
|
||||||
}
|
}
|
||||||
@@ -288,6 +349,11 @@ var ImmerseView = class extends import_obsidian2.ItemView {
|
|||||||
const dateStr = today.toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric" });
|
const dateStr = today.toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric" });
|
||||||
titleSection.createEl("div", { text: dateStr, cls: "immerse-date" });
|
titleSection.createEl("div", { text: dateStr, cls: "immerse-date" });
|
||||||
const actions = header.createEl("div", { cls: "immerse-header-actions" });
|
const actions = header.createEl("div", { cls: "immerse-header-actions" });
|
||||||
|
const reportsBtn = actions.createEl("button", { cls: "immerse-btn" });
|
||||||
|
reportsBtn.innerHTML = "\u{1F4CA} Reports";
|
||||||
|
reportsBtn.addEventListener("click", () => {
|
||||||
|
this.plugin.activateReportView();
|
||||||
|
});
|
||||||
const addBtn = actions.createEl("button", { cls: "immerse-btn immerse-btn-primary" });
|
const addBtn = actions.createEl("button", { cls: "immerse-btn immerse-btn-primary" });
|
||||||
addBtn.innerHTML = "+ Add Task";
|
addBtn.innerHTML = "+ Add Task";
|
||||||
addBtn.addEventListener("click", () => {
|
addBtn.addEventListener("click", () => {
|
||||||
@@ -300,7 +366,7 @@ var ImmerseView = class extends import_obsidian2.ItemView {
|
|||||||
const statItems = [
|
const statItems = [
|
||||||
{ label: "Pending", value: stats.pendingCount.toString(), icon: "\u{1F4CB}" },
|
{ label: "Pending", value: stats.pendingCount.toString(), icon: "\u{1F4CB}" },
|
||||||
{ label: "Done Today", value: stats.completedToday.toString(), icon: "\u2705" },
|
{ label: "Done Today", value: stats.completedToday.toString(), icon: "\u2705" },
|
||||||
{ label: "Focus Time", value: this.plugin.formatTimeHuman(stats.totalFocusMinutesToday), icon: "\u23F1\uFE0F" },
|
{ label: "Today's Focus", value: this.plugin.formatTimeHuman(stats.totalFocusMinutesToday), icon: "\u23F1\uFE0F" },
|
||||||
{ label: "Streak", value: `${stats.streak} days`, icon: "\u{1F525}" }
|
{ label: "Streak", value: `${stats.streak} days`, icon: "\u{1F525}" }
|
||||||
];
|
];
|
||||||
statItems.forEach((stat) => {
|
statItems.forEach((stat) => {
|
||||||
@@ -459,8 +525,9 @@ var ImmerseView = class extends import_obsidian2.ItemView {
|
|||||||
}
|
}
|
||||||
renderTaskItem(container, task) {
|
renderTaskItem(container, task) {
|
||||||
const list = this.plugin.settings.lists.find((l) => l.id === task.list);
|
const list = this.plugin.settings.lists.find((l) => l.id === task.list);
|
||||||
|
const isOverdue = !task.completed && task.scheduledDate && task.scheduledTime && new Date(`${task.scheduledDate}T${task.scheduledTime}`).getTime() < Date.now();
|
||||||
const taskEl = container.createEl("div", {
|
const taskEl = container.createEl("div", {
|
||||||
cls: `immerse-task-item ${task.completed ? "completed" : ""} ${task.isActive ? "active" : ""}`
|
cls: `immerse-task-item ${task.completed ? "completed" : ""} ${task.isActive ? "active" : ""} ${isOverdue ? "overdue" : ""}`
|
||||||
});
|
});
|
||||||
const checkbox = taskEl.createEl("div", { cls: "immerse-checkbox" });
|
const checkbox = taskEl.createEl("div", { cls: "immerse-checkbox" });
|
||||||
checkbox.innerHTML = task.completed ? "\u2713" : "";
|
checkbox.innerHTML = task.completed ? "\u2713" : "";
|
||||||
@@ -495,6 +562,21 @@ var ImmerseView = class extends import_obsidian2.ItemView {
|
|||||||
actualSpan.addClass("immerse-overtime-text");
|
actualSpan.addClass("immerse-overtime-text");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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(`\u26A0\uFE0F OVERDUE: ${dateStr}${timeStr ? " " + timeStr : ""}`);
|
||||||
|
} else {
|
||||||
|
scheduleSpan.setText(`\u{1F4C5} ${dateStr}${timeStr ? " " + timeStr : ""}`);
|
||||||
|
}
|
||||||
|
if (task.reminderMinutes) {
|
||||||
|
scheduleSpan.title = `Reminder set for ${task.reminderMinutes} min before`;
|
||||||
|
}
|
||||||
|
}
|
||||||
const actions = taskEl.createEl("div", { cls: "immerse-task-actions" });
|
const actions = taskEl.createEl("div", { cls: "immerse-task-actions" });
|
||||||
if (!task.completed) {
|
if (!task.completed) {
|
||||||
const startBtn = actions.createEl("button", { cls: "immerse-task-btn", attr: { "aria-label": "Start Pomodoro" } });
|
const startBtn = actions.createEl("button", { cls: "immerse-task-btn", attr: { "aria-label": "Start Pomodoro" } });
|
||||||
@@ -525,8 +607,281 @@ var ImmerseView = class extends import_obsidian2.ItemView {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// src/reportView.ts
|
||||||
|
var import_obsidian3 = require("obsidian");
|
||||||
|
var VIEW_TYPE_REPORT = "immerse-report-view";
|
||||||
|
var ReportView = class extends import_obsidian3.ItemView {
|
||||||
|
constructor(leaf, plugin) {
|
||||||
|
super(leaf);
|
||||||
|
this.selectedListIds = [];
|
||||||
|
this.plugin = plugin;
|
||||||
|
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() {
|
||||||
|
return VIEW_TYPE_REPORT;
|
||||||
|
}
|
||||||
|
getDisplayText() {
|
||||||
|
return "\u{1F4CA} Reports";
|
||||||
|
}
|
||||||
|
getIcon() {
|
||||||
|
return "bar-chart-2";
|
||||||
|
}
|
||||||
|
async onOpen() {
|
||||||
|
const container = this.containerEl.children[1];
|
||||||
|
container.empty();
|
||||||
|
container.addClass("immerse-report-view");
|
||||||
|
this.renderContent();
|
||||||
|
}
|
||||||
|
async onClose() {
|
||||||
|
}
|
||||||
|
renderContent() {
|
||||||
|
const container = this.containerEl.children[1];
|
||||||
|
container.empty();
|
||||||
|
const header = container.createEl("div", { cls: "immerse-report-header" });
|
||||||
|
header.createEl("h2", { text: "\u{1F4CA} Reports", cls: "immerse-report-title" });
|
||||||
|
this.renderFilters(container);
|
||||||
|
const generateBtn = container.createEl("button", {
|
||||||
|
text: "\u{1F504} Generate Report",
|
||||||
|
cls: "immerse-btn immerse-btn-primary immerse-report-generate-btn"
|
||||||
|
});
|
||||||
|
generateBtn.addEventListener("click", () => this.renderReport(container));
|
||||||
|
this.renderReport(container);
|
||||||
|
}
|
||||||
|
renderFilters(container) {
|
||||||
|
const filtersSection = container.createEl("div", { cls: "immerse-report-filters" });
|
||||||
|
const dateRow = filtersSection.createEl("div", { cls: "immerse-report-filter-row" });
|
||||||
|
new import_obsidian3.Setting(dateRow).setName("Start Date").addText((text) => {
|
||||||
|
text.setValue(this.startDate).onChange((value) => this.startDate = value);
|
||||||
|
text.inputEl.type = "date";
|
||||||
|
});
|
||||||
|
new import_obsidian3.Setting(dateRow).setName("End Date").addText((text) => {
|
||||||
|
text.setValue(this.endDate).onChange((value) => this.endDate = value);
|
||||||
|
text.inputEl.type = "date";
|
||||||
|
});
|
||||||
|
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) {
|
||||||
|
const oldReport = container.querySelector(".immerse-report-content");
|
||||||
|
if (oldReport)
|
||||||
|
oldReport.remove();
|
||||||
|
const filters = {
|
||||||
|
startDate: this.startDate,
|
||||||
|
endDate: this.endDate,
|
||||||
|
listIds: this.selectedListIds.length > 0 ? this.selectedListIds : void 0
|
||||||
|
};
|
||||||
|
const reportData = this.plugin.generateReport(filters);
|
||||||
|
const reportContent = container.createEl("div", { cls: "immerse-report-content" });
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
this.renderSummaryStats(reportContent, reportData);
|
||||||
|
if (reportData.timeByList.length > 0) {
|
||||||
|
this.renderTimeByList(reportContent, reportData);
|
||||||
|
}
|
||||||
|
this.renderInsights(reportContent, reportData);
|
||||||
|
if (reportData.dailyBreakdown.length > 0) {
|
||||||
|
this.renderDailyBreakdown(reportContent, reportData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderSummaryStats(container, data) {
|
||||||
|
const statsGrid = container.createEl("div", { cls: "immerse-stats-grid" });
|
||||||
|
const stats = [
|
||||||
|
{ label: "TASKS DONE", value: data.totalTasks.toString(), icon: "\u2713" },
|
||||||
|
{ label: "TASKS PER DAY", value: data.tasksPerDay.toFixed(1), icon: "\u{1F4C5}" },
|
||||||
|
{ label: "HOURS PER DAY", value: data.hoursPerDay.toFixed(1), icon: "\u23F0" },
|
||||||
|
{ label: "MINS PER TASK", value: data.minsPerTask.toString(), icon: "\u23F1\uFE0F" },
|
||||||
|
{ label: "DAY STREAK", value: data.currentStreak.toString(), icon: "\u{1F525}" },
|
||||||
|
{ label: "TOTAL HOURS", value: (data.totalMinutes / 60).toFixed(1), icon: "\u231A" }
|
||||||
|
];
|
||||||
|
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, data) {
|
||||||
|
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" });
|
||||||
|
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" });
|
||||||
|
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;
|
||||||
|
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, data) {
|
||||||
|
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 !== void 0) {
|
||||||
|
const card = insightsGrid.createEl("div", { cls: "immerse-insight-card" });
|
||||||
|
card.createEl("span", { text: "\u{1F550}", 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: "\u{1F4C5}", 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: "\u{1F5D3}\uFE0F", 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, data) {
|
||||||
|
const pieContainer = container.createEl("div", { cls: "immerse-daily-pie-container" });
|
||||||
|
const totalTasks = data.totalTasks;
|
||||||
|
const totalMinutes = data.totalMinutes;
|
||||||
|
const totalPomodoros = data.totalPomodoros;
|
||||||
|
const taskMinutes = totalTasks * data.minsPerTask;
|
||||||
|
const pomodoroMinutes = totalPomodoros * 25;
|
||||||
|
const totalTime = taskMinutes + totalMinutes + pomodoroMinutes;
|
||||||
|
if (totalTime === 0)
|
||||||
|
return;
|
||||||
|
const tasksPercent = taskMinutes / totalTime * 100;
|
||||||
|
const hoursPercent = totalMinutes / totalTime * 100;
|
||||||
|
const pomodorosPercent = pomodoroMinutes / totalTime * 100;
|
||||||
|
const tasksDeg = tasksPercent / 100 * 360;
|
||||||
|
const hoursDeg = tasksDeg + hoursPercent / 100 * 360;
|
||||||
|
const pieChart = pieContainer.createEl("div", { cls: "immerse-daily-pie-chart" });
|
||||||
|
const gradient = `conic-gradient(from 0deg, #6366f1 0deg ${tasksDeg}deg, #22c55e ${tasksDeg}deg ${hoursDeg}deg, #f59e0b ${hoursDeg}deg 360deg)`;
|
||||||
|
pieChart.style.background = gradient;
|
||||||
|
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" });
|
||||||
|
const legend = pieContainer.createEl("div", { cls: "immerse-daily-pie-legend" });
|
||||||
|
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: "\u2713" });
|
||||||
|
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" });
|
||||||
|
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: "\u23F1\uFE0F" });
|
||||||
|
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" });
|
||||||
|
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: "\u{1F345}" });
|
||||||
|
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, data) {
|
||||||
|
const section = container.createEl("div", { cls: "immerse-report-section" });
|
||||||
|
section.createEl("h3", { text: "Daily Breakdown", cls: "immerse-report-section-title" });
|
||||||
|
this.renderPieChart(section, data);
|
||||||
|
const breakdownContainer = section.createEl("div", { cls: "immerse-daily-breakdown-container" });
|
||||||
|
const recentData = data.dailyBreakdown.slice(-10);
|
||||||
|
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" });
|
||||||
|
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" });
|
||||||
|
const barsContainer = row.createEl("div", { cls: "immerse-daily-bars" });
|
||||||
|
const tasksRow = barsContainer.createEl("div", { cls: "immerse-daily-bar-row" });
|
||||||
|
const tasksLabel = tasksRow.createEl("span", { cls: "immerse-daily-bar-label" });
|
||||||
|
tasksLabel.createEl("span", { text: "\u2713", 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" });
|
||||||
|
const hoursRow = barsContainer.createEl("div", { cls: "immerse-daily-bar-row" });
|
||||||
|
const hoursLabel = hoursRow.createEl("span", { cls: "immerse-daily-bar-label" });
|
||||||
|
hoursLabel.createEl("span", { text: "\u23F1\uFE0F", 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" });
|
||||||
|
const pomodorosRow = barsContainer.createEl("div", { cls: "immerse-daily-bar-row" });
|
||||||
|
const pomodorosLabel = pomodorosRow.createEl("span", { cls: "immerse-daily-bar-label" });
|
||||||
|
pomodorosLabel.createEl("span", { text: "\u{1F345}", 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" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// src/main.ts
|
// src/main.ts
|
||||||
var ImmersePlugin = class extends import_obsidian3.Plugin {
|
var ImmersePlugin = class extends import_obsidian4.Plugin {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
// Timer state
|
// Timer state
|
||||||
@@ -544,7 +899,11 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
this.secondsWorkedOnCurrentTask = 0;
|
this.secondsWorkedOnCurrentTask = 0;
|
||||||
// Status bar element
|
// Status bar element
|
||||||
this.statusBarEl = null;
|
this.statusBarEl = null;
|
||||||
|
// Reminder system
|
||||||
|
this.reminderCheckInterval = null;
|
||||||
|
this.notifiedReminders = /* @__PURE__ */ new Set();
|
||||||
}
|
}
|
||||||
|
// Track which reminders have been shown
|
||||||
async onload() {
|
async onload() {
|
||||||
await this.loadAllData();
|
await this.loadAllData();
|
||||||
this.checkDailyReset();
|
this.checkDailyReset();
|
||||||
@@ -557,6 +916,10 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
VIEW_TYPE_IMMERSE,
|
VIEW_TYPE_IMMERSE,
|
||||||
(leaf) => new ImmerseView(leaf, this)
|
(leaf) => new ImmerseView(leaf, this)
|
||||||
);
|
);
|
||||||
|
this.registerView(
|
||||||
|
VIEW_TYPE_REPORT,
|
||||||
|
(leaf) => new ReportView(leaf, this)
|
||||||
|
);
|
||||||
this.addRibbonIcon("zap", "Open Immerse", () => {
|
this.addRibbonIcon("zap", "Open Immerse", () => {
|
||||||
this.activateView();
|
this.activateView();
|
||||||
});
|
});
|
||||||
@@ -585,11 +948,20 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
name: "Complete Current Task",
|
name: "Complete Current Task",
|
||||||
callback: () => this.completeActiveTask()
|
callback: () => this.completeActiveTask()
|
||||||
});
|
});
|
||||||
|
this.addCommand({
|
||||||
|
id: "view-reports",
|
||||||
|
name: "View Reports",
|
||||||
|
callback: () => this.activateReportView()
|
||||||
|
});
|
||||||
this.addSettingTab(new ImmerseSettingTab(this.app, this));
|
this.addSettingTab(new ImmerseSettingTab(this.app, this));
|
||||||
this.createStatusBar();
|
this.createStatusBar();
|
||||||
|
if (this.settings.enableReminders) {
|
||||||
|
this.startReminderSystem();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onunload() {
|
onunload() {
|
||||||
this.stopTimer();
|
this.stopTimer();
|
||||||
|
this.stopReminderSystem();
|
||||||
}
|
}
|
||||||
async loadAllData() {
|
async loadAllData() {
|
||||||
const loaded = await this.loadData();
|
const loaded = await this.loadData();
|
||||||
@@ -640,6 +1012,20 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
workspace.revealLeaf(leaf);
|
workspace.revealLeaf(leaf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async activateReportView() {
|
||||||
|
const { workspace } = this.app;
|
||||||
|
const existingLeaves = workspace.getLeavesOfType(VIEW_TYPE_REPORT);
|
||||||
|
if (existingLeaves.length > 0) {
|
||||||
|
workspace.revealLeaf(existingLeaves[0]);
|
||||||
|
} else {
|
||||||
|
const leaf = workspace.getLeaf("tab");
|
||||||
|
await leaf.setViewState({
|
||||||
|
type: VIEW_TYPE_REPORT,
|
||||||
|
active: true
|
||||||
|
});
|
||||||
|
workspace.revealLeaf(leaf);
|
||||||
|
}
|
||||||
|
}
|
||||||
// ============ Task Management ============
|
// ============ Task Management ============
|
||||||
createTask(text, estimatedMinutes = this.settings.defaultEstimateMinutes, list = "work") {
|
createTask(text, estimatedMinutes = this.settings.defaultEstimateMinutes, list = "work") {
|
||||||
return {
|
return {
|
||||||
@@ -687,6 +1073,8 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
task.isActive = false;
|
task.isActive = false;
|
||||||
this.data.completedToday++;
|
this.data.completedToday++;
|
||||||
this.data.lastActiveDate = new Date().toDateString();
|
this.data.lastActiveDate = new Date().toDateString();
|
||||||
|
this.archiveCompletedTask(task);
|
||||||
|
this.updateDailyStats(task);
|
||||||
if (this.settings.enableCelebrations) {
|
if (this.settings.enableCelebrations) {
|
||||||
this.showCelebration(task);
|
this.showCelebration(task);
|
||||||
}
|
}
|
||||||
@@ -708,9 +1096,185 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
if (this.activeTaskId) {
|
if (this.activeTaskId) {
|
||||||
this.completeTask(this.activeTaskId);
|
this.completeTask(this.activeTaskId);
|
||||||
} else {
|
} else {
|
||||||
new import_obsidian3.Notice("No active task to complete");
|
new import_obsidian4.Notice("No active task to complete");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ============ Data Archiving & Statistics ============
|
||||||
|
archiveCompletedTask(task) {
|
||||||
|
const wasOverdue = task.scheduledDate && task.scheduledTime && new Date(`${task.scheduledDate}T${task.scheduledTime}`).getTime() < (task.completedAt || Date.now());
|
||||||
|
const record = {
|
||||||
|
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) {
|
||||||
|
const today = new Date().toISOString().split("T")[0];
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
todayStats.tasksCompleted++;
|
||||||
|
todayStats.totalMinutes += task.actualMinutes;
|
||||||
|
todayStats.pomodorosCompleted = this.data.pomodorosCompleted;
|
||||||
|
todayStats.tasksByList[task.list] = (todayStats.tasksByList[task.list] || 0) + 1;
|
||||||
|
todayStats.minutesByList[task.list] = (todayStats.minutesByList[task.list] || 0) + task.actualMinutes;
|
||||||
|
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) {
|
||||||
|
const { startDate, endDate, listIds } = filters;
|
||||||
|
const filteredStats = this.data.dailyStats.filter((stat) => {
|
||||||
|
return stat.date >= startDate && stat.date <= endDate;
|
||||||
|
});
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
const totalTasks = filteredTasks.length;
|
||||||
|
const totalMinutes = filteredTasks.reduce((sum, task) => sum + task.actualMinutes, 0);
|
||||||
|
const totalPomodoros = filteredStats.reduce((sum, stat) => sum + stat.pomodorosCompleted, 0);
|
||||||
|
const daysWithData = filteredStats.length || 1;
|
||||||
|
const tasksPerDay = totalTasks / daysWithData;
|
||||||
|
const hoursPerDay = totalMinutes / 60 / daysWithData;
|
||||||
|
const minsPerTask = totalTasks > 0 ? totalMinutes / totalTasks : 0;
|
||||||
|
const timeByListMap = {};
|
||||||
|
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);
|
||||||
|
const dailyBreakdown = filteredStats.map((stat) => ({
|
||||||
|
date: stat.date,
|
||||||
|
tasks: stat.tasksCompleted,
|
||||||
|
hours: Math.round(stat.totalMinutes / 60 * 10) / 10,
|
||||||
|
pomodoros: stat.pomodorosCompleted
|
||||||
|
}));
|
||||||
|
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) {
|
||||||
|
if (tasks.length === 0)
|
||||||
|
return void 0;
|
||||||
|
const hourCounts = {};
|
||||||
|
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 : void 0;
|
||||||
|
}
|
||||||
|
calculateMostProductiveDay(stats) {
|
||||||
|
if (stats.length === 0)
|
||||||
|
return void 0;
|
||||||
|
const dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||||
|
const dayCounts = {};
|
||||||
|
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 : void 0;
|
||||||
|
}
|
||||||
|
calculateMostProductiveMonth(stats) {
|
||||||
|
if (stats.length === 0)
|
||||||
|
return void 0;
|
||||||
|
const monthNames = [
|
||||||
|
"January",
|
||||||
|
"February",
|
||||||
|
"March",
|
||||||
|
"April",
|
||||||
|
"May",
|
||||||
|
"June",
|
||||||
|
"July",
|
||||||
|
"August",
|
||||||
|
"September",
|
||||||
|
"October",
|
||||||
|
"November",
|
||||||
|
"December"
|
||||||
|
];
|
||||||
|
const monthCounts = {};
|
||||||
|
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 : void 0;
|
||||||
|
}
|
||||||
// ============ Timer Management ============
|
// ============ Timer Management ============
|
||||||
// Sync timer based on timestamp when app returns from background
|
// Sync timer based on timestamp when app returns from background
|
||||||
syncTimerFromTimestamp() {
|
syncTimerFromTimestamp() {
|
||||||
@@ -752,7 +1316,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
if (this.settings.enableSounds) {
|
if (this.settings.enableSounds) {
|
||||||
this.playAlertSound();
|
this.playAlertSound();
|
||||||
}
|
}
|
||||||
new import_obsidian3.Notice(`\u23F0 Time's up for: ${task.text}`);
|
new import_obsidian4.Notice(`\u23F0 Time's up for: ${task.text}`);
|
||||||
}
|
}
|
||||||
}, 1e3);
|
}, 1e3);
|
||||||
this.saveAllData();
|
this.saveAllData();
|
||||||
@@ -809,7 +1373,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
if (this.settings.enableSounds) {
|
if (this.settings.enableSounds) {
|
||||||
this.playAlertSound();
|
this.playAlertSound();
|
||||||
}
|
}
|
||||||
new import_obsidian3.Notice("\u{1F345} Pomodoro complete! Time for a break.");
|
new import_obsidian4.Notice("\u{1F345} Pomodoro complete! Time for a break.");
|
||||||
if (this.settings.autoStartBreak) {
|
if (this.settings.autoStartBreak) {
|
||||||
this.startBreak();
|
this.startBreak();
|
||||||
} else {
|
} else {
|
||||||
@@ -819,7 +1383,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
if (this.settings.enableSounds) {
|
if (this.settings.enableSounds) {
|
||||||
this.playAlertSound();
|
this.playAlertSound();
|
||||||
}
|
}
|
||||||
new import_obsidian3.Notice("\u26A1 Break over! Ready to focus?");
|
new import_obsidian4.Notice("\u26A1 Break over! Ready to focus?");
|
||||||
this.refreshView();
|
this.refreshView();
|
||||||
}
|
}
|
||||||
this.saveAllData();
|
this.saveAllData();
|
||||||
@@ -831,7 +1395,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
this.currentTimerSeconds = (isLongBreak ? this.settings.longBreakMinutes : this.settings.pomodoroBreakMinutes) * 60;
|
this.currentTimerSeconds = (isLongBreak ? this.settings.longBreakMinutes : this.settings.pomodoroBreakMinutes) * 60;
|
||||||
this.timerStartTimestamp = Date.now();
|
this.timerStartTimestamp = Date.now();
|
||||||
this.pausedTimeRemaining = this.currentTimerSeconds;
|
this.pausedTimeRemaining = this.currentTimerSeconds;
|
||||||
new import_obsidian3.Notice(isLongBreak ? "\u2615 Long break time!" : "\u2615 Short break time!");
|
new import_obsidian4.Notice(isLongBreak ? "\u2615 Long break time!" : "\u2615 Short break time!");
|
||||||
this.refreshView();
|
this.refreshView();
|
||||||
if (!this.timerInterval) {
|
if (!this.timerInterval) {
|
||||||
this.timerInterval = window.setInterval(() => {
|
this.timerInterval = window.setInterval(() => {
|
||||||
@@ -877,7 +1441,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
}
|
}
|
||||||
}, 1e3);
|
}, 1e3);
|
||||||
} else {
|
} else {
|
||||||
new import_obsidian3.Notice("No active task. Select a task first.");
|
new import_obsidian4.Notice("No active task. Select a task first.");
|
||||||
}
|
}
|
||||||
this.updateStatusBar();
|
this.updateStatusBar();
|
||||||
this.refreshView();
|
this.refreshView();
|
||||||
@@ -908,13 +1472,13 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
if (pendingTasks.length > 0) {
|
if (pendingTasks.length > 0) {
|
||||||
this.startPomodoro(pendingTasks[0].id);
|
this.startPomodoro(pendingTasks[0].id);
|
||||||
} else {
|
} else {
|
||||||
new import_obsidian3.Notice("No pending tasks. Add a task first!");
|
new import_obsidian4.Notice("No pending tasks. Add a task first!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// ============ Status Bar Timer ============
|
// ============ Status Bar Timer ============
|
||||||
createStatusBar() {
|
createStatusBar() {
|
||||||
this.statusBarEl = this.addStatusBarItem();
|
this.statusBarEl = this.addStatusBarItem();
|
||||||
this.statusBarEl.addClass("focus-task-status-bar");
|
this.statusBarEl.addClass("immerse-status-bar");
|
||||||
this.updateStatusBar();
|
this.updateStatusBar();
|
||||||
this.statusBarEl.addEventListener("click", () => {
|
this.statusBarEl.addEventListener("click", () => {
|
||||||
this.activateView();
|
this.activateView();
|
||||||
@@ -929,10 +1493,63 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
const timeStr = this.formatTime(this.currentTimerSeconds);
|
const timeStr = this.formatTime(this.currentTimerSeconds);
|
||||||
const icon = this.isTimerRunning ? "\u25B6" : "\u23F8";
|
const icon = this.isTimerRunning ? "\u25B6" : "\u23F8";
|
||||||
this.statusBarEl.setText(`\u26A1 ${icon} ${timeStr} - ${taskName}${task && task.text.length > 20 ? "..." : ""}`);
|
this.statusBarEl.setText(`\u26A1 ${icon} ${timeStr} - ${taskName}${task && task.text.length > 20 ? "..." : ""}`);
|
||||||
this.statusBarEl.addClass("focus-task-status-active");
|
this.statusBarEl.addClass("immerse-status-active");
|
||||||
} else {
|
} else {
|
||||||
this.statusBarEl.setText("\u26A1 Immerse");
|
this.statusBarEl.setText("\u26A1 Immerse");
|
||||||
this.statusBarEl.removeClass("focus-task-status-active");
|
this.statusBarEl.removeClass("immerse-status-active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ============ Reminder System ============
|
||||||
|
startReminderSystem() {
|
||||||
|
this.reminderCheckInterval = window.setInterval(() => {
|
||||||
|
this.checkReminders();
|
||||||
|
}, 3e4);
|
||||||
|
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();
|
||||||
|
this.data.tasks.filter((task) => !task.completed && task.scheduledDate && task.scheduledTime).forEach((task) => {
|
||||||
|
const reminderKey = `${task.id}-${task.scheduledDate}-${task.scheduledTime}`;
|
||||||
|
if (this.notifiedReminders.has(reminderKey))
|
||||||
|
return;
|
||||||
|
const scheduledDateTime = new Date(`${task.scheduledDate}T${task.scheduledTime}`);
|
||||||
|
const scheduledTime = scheduledDateTime.getTime();
|
||||||
|
if (currentTime > scheduledTime) {
|
||||||
|
this.showOverdueNotice(task);
|
||||||
|
this.notifiedReminders.add(reminderKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (task.reminderMinutes) {
|
||||||
|
const reminderTime = scheduledTime - task.reminderMinutes * 60 * 1e3;
|
||||||
|
if (currentTime >= reminderTime) {
|
||||||
|
this.showReminder(task);
|
||||||
|
this.notifiedReminders.add(reminderKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
showReminder(task) {
|
||||||
|
const timeStr = task.scheduledTime;
|
||||||
|
new import_obsidian4.Notice(`\u{1F514} Reminder: "${task.text}" is scheduled for ${timeStr}`, 8e3);
|
||||||
|
if (this.settings.enableSounds) {
|
||||||
|
this.playAlertSound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showOverdueNotice(task) {
|
||||||
|
const dateStr = task.scheduledDate;
|
||||||
|
const timeStr = task.scheduledTime;
|
||||||
|
new import_obsidian4.Notice(`\u26A0\uFE0F Overdue: "${task.text}" was scheduled for ${dateStr} ${timeStr}`, 1e4);
|
||||||
|
if (this.settings.enableSounds) {
|
||||||
|
this.playAlertSound();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// ============ Sounds & Celebrations ============
|
// ============ Sounds & Celebrations ============
|
||||||
@@ -947,7 +1564,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
messages = OVERTIME_MESSAGES;
|
messages = OVERTIME_MESSAGES;
|
||||||
}
|
}
|
||||||
const celebration = messages[Math.floor(Math.random() * messages.length)];
|
const celebration = messages[Math.floor(Math.random() * messages.length)];
|
||||||
new import_obsidian3.Notice(`${celebration.emoji} ${celebration.message}${extraMessage}`);
|
new import_obsidian4.Notice(`${celebration.emoji} ${celebration.message}${extraMessage}`);
|
||||||
}
|
}
|
||||||
playCompletionSound() {
|
playCompletionSound() {
|
||||||
try {
|
try {
|
||||||
@@ -1009,7 +1626,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
await this.appendToDailyNote(taskEntry);
|
await this.appendToDailyNote(taskEntry);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to log task to daily note:", e);
|
console.error("Failed to log task to daily note:", e);
|
||||||
new import_obsidian3.Notice("Failed to log task to daily note. Make sure Daily Notes core plugin is enabled.");
|
new import_obsidian4.Notice("Failed to log task to daily note. Make sure Daily Notes core plugin is enabled.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getDailyNoteSettings() {
|
getDailyNoteSettings() {
|
||||||
@@ -1047,14 +1664,14 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
const { vault } = this.app;
|
const { vault } = this.app;
|
||||||
const dailySettings = this.getDailyNoteSettings();
|
const dailySettings = this.getDailyNoteSettings();
|
||||||
if (!dailySettings) {
|
if (!dailySettings) {
|
||||||
new import_obsidian3.Notice("Daily Notes core plugin is not enabled. Please enable it in Settings \u2192 Core plugins.");
|
new import_obsidian4.Notice("Daily Notes core plugin is not enabled. Please enable it in Settings \u2192 Core plugins.");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const filename = this.formatDailyNoteDate(dailySettings.format);
|
const filename = this.formatDailyNoteDate(dailySettings.format);
|
||||||
const folder = dailySettings.folder ? `${dailySettings.folder}/` : "";
|
const folder = dailySettings.folder ? `${dailySettings.folder}/` : "";
|
||||||
const path = `${folder}${filename}.md`;
|
const path = `${folder}${filename}.md`;
|
||||||
let file = vault.getAbstractFileByPath(path);
|
let file = vault.getAbstractFileByPath(path);
|
||||||
if (file && file instanceof import_obsidian3.TFile) {
|
if (file && file instanceof import_obsidian4.TFile) {
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -1068,17 +1685,17 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
if (dailySettings.template) {
|
if (dailySettings.template) {
|
||||||
const templatePath = dailySettings.template.endsWith(".md") ? dailySettings.template : `${dailySettings.template}.md`;
|
const templatePath = dailySettings.template.endsWith(".md") ? dailySettings.template : `${dailySettings.template}.md`;
|
||||||
const templateFile = vault.getAbstractFileByPath(templatePath);
|
const templateFile = vault.getAbstractFileByPath(templatePath);
|
||||||
if (templateFile && templateFile instanceof import_obsidian3.TFile) {
|
if (templateFile && templateFile instanceof import_obsidian4.TFile) {
|
||||||
content = await vault.read(templateFile);
|
content = await vault.read(templateFile);
|
||||||
content = content.replace(/{{date}}/g, filename).replace(/{{time}}/g, new Date().toLocaleTimeString()).replace(/{{title}}/g, filename);
|
content = content.replace(/{{date}}/g, filename).replace(/{{time}}/g, new Date().toLocaleTimeString()).replace(/{{title}}/g, filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const newFile = await vault.create(path, content);
|
const newFile = await vault.create(path, content);
|
||||||
new import_obsidian3.Notice(`\u{1F4DD} Created daily note: ${filename}`);
|
new import_obsidian4.Notice(`\u{1F4DD} Created daily note: ${filename}`);
|
||||||
return newFile;
|
return newFile;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to create daily note:", e);
|
console.error("Failed to create daily note:", e);
|
||||||
new import_obsidian3.Notice("Failed to create daily note");
|
new import_obsidian4.Notice("Failed to create daily note");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1091,7 +1708,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
const existingContent = await vault.read(file);
|
const existingContent = await vault.read(file);
|
||||||
const newContent = existingContent.trimEnd() + "\n" + content + "\n";
|
const newContent = existingContent.trimEnd() + "\n" + content + "\n";
|
||||||
await vault.modify(file, newContent);
|
await vault.modify(file, newContent);
|
||||||
new import_obsidian3.Notice("\u{1F4DD} Task logged to daily note");
|
new import_obsidian4.Notice("\u{1F4DD} Task logged to daily note");
|
||||||
}
|
}
|
||||||
// ============ Utilities ============
|
// ============ Utilities ============
|
||||||
formatTime(seconds) {
|
formatTime(seconds) {
|
||||||
@@ -1157,7 +1774,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var ImmerseSettingTab = class extends import_obsidian3.PluginSettingTab {
|
var ImmerseSettingTab = class extends import_obsidian4.PluginSettingTab {
|
||||||
constructor(app, plugin) {
|
constructor(app, plugin) {
|
||||||
super(app, plugin);
|
super(app, plugin);
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
@@ -1167,41 +1784,41 @@ var ImmerseSettingTab = class extends import_obsidian3.PluginSettingTab {
|
|||||||
containerEl.empty();
|
containerEl.empty();
|
||||||
containerEl.createEl("h1", { text: "\u26A1 Immerse Settings" });
|
containerEl.createEl("h1", { text: "\u26A1 Immerse Settings" });
|
||||||
containerEl.createEl("h2", { text: "\u{1F345} Pomodoro Timer" });
|
containerEl.createEl("h2", { text: "\u{1F345} Pomodoro Timer" });
|
||||||
new import_obsidian3.Setting(containerEl).setName("Work Duration").setDesc("Length of each work session in minutes").addSlider((slider) => slider.setLimits(5, 60, 5).setValue(this.plugin.settings.pomodoroWorkMinutes).setDynamicTooltip().onChange(async (value) => {
|
new import_obsidian4.Setting(containerEl).setName("Work Duration").setDesc("Length of each work session in minutes").addSlider((slider) => slider.setLimits(5, 60, 5).setValue(this.plugin.settings.pomodoroWorkMinutes).setDynamicTooltip().onChange(async (value) => {
|
||||||
this.plugin.settings.pomodoroWorkMinutes = value;
|
this.plugin.settings.pomodoroWorkMinutes = value;
|
||||||
await this.plugin.saveAllData();
|
await this.plugin.saveAllData();
|
||||||
}));
|
}));
|
||||||
new import_obsidian3.Setting(containerEl).setName("Short Break Duration").setDesc("Length of short breaks in minutes").addSlider((slider) => slider.setLimits(1, 15, 1).setValue(this.plugin.settings.pomodoroBreakMinutes).setDynamicTooltip().onChange(async (value) => {
|
new import_obsidian4.Setting(containerEl).setName("Short Break Duration").setDesc("Length of short breaks in minutes").addSlider((slider) => slider.setLimits(1, 15, 1).setValue(this.plugin.settings.pomodoroBreakMinutes).setDynamicTooltip().onChange(async (value) => {
|
||||||
this.plugin.settings.pomodoroBreakMinutes = value;
|
this.plugin.settings.pomodoroBreakMinutes = value;
|
||||||
await this.plugin.saveAllData();
|
await this.plugin.saveAllData();
|
||||||
}));
|
}));
|
||||||
new import_obsidian3.Setting(containerEl).setName("Long Break Duration").setDesc("Length of long breaks in minutes").addSlider((slider) => slider.setLimits(5, 30, 5).setValue(this.plugin.settings.longBreakMinutes).setDynamicTooltip().onChange(async (value) => {
|
new import_obsidian4.Setting(containerEl).setName("Long Break Duration").setDesc("Length of long breaks in minutes").addSlider((slider) => slider.setLimits(5, 30, 5).setValue(this.plugin.settings.longBreakMinutes).setDynamicTooltip().onChange(async (value) => {
|
||||||
this.plugin.settings.longBreakMinutes = value;
|
this.plugin.settings.longBreakMinutes = value;
|
||||||
await this.plugin.saveAllData();
|
await this.plugin.saveAllData();
|
||||||
}));
|
}));
|
||||||
new import_obsidian3.Setting(containerEl).setName("Long Break Interval").setDesc("Number of pomodoros before a long break").addSlider((slider) => slider.setLimits(2, 6, 1).setValue(this.plugin.settings.longBreakInterval).setDynamicTooltip().onChange(async (value) => {
|
new import_obsidian4.Setting(containerEl).setName("Long Break Interval").setDesc("Number of pomodoros before a long break").addSlider((slider) => slider.setLimits(2, 6, 1).setValue(this.plugin.settings.longBreakInterval).setDynamicTooltip().onChange(async (value) => {
|
||||||
this.plugin.settings.longBreakInterval = value;
|
this.plugin.settings.longBreakInterval = value;
|
||||||
await this.plugin.saveAllData();
|
await this.plugin.saveAllData();
|
||||||
}));
|
}));
|
||||||
new import_obsidian3.Setting(containerEl).setName("Auto-start Breaks").setDesc("Automatically start break timer after work session").addToggle((toggle) => toggle.setValue(this.plugin.settings.autoStartBreak).onChange(async (value) => {
|
new import_obsidian4.Setting(containerEl).setName("Auto-start Breaks").setDesc("Automatically start break timer after work session").addToggle((toggle) => toggle.setValue(this.plugin.settings.autoStartBreak).onChange(async (value) => {
|
||||||
this.plugin.settings.autoStartBreak = value;
|
this.plugin.settings.autoStartBreak = value;
|
||||||
await this.plugin.saveAllData();
|
await this.plugin.saveAllData();
|
||||||
}));
|
}));
|
||||||
containerEl.createEl("h2", { text: "\u2699\uFE0F General" });
|
containerEl.createEl("h2", { text: "\u2699\uFE0F General" });
|
||||||
new import_obsidian3.Setting(containerEl).setName("Default Time Estimate").setDesc("Default estimated time for new tasks in minutes").addSlider((slider) => slider.setLimits(5, 120, 5).setValue(this.plugin.settings.defaultEstimateMinutes).setDynamicTooltip().onChange(async (value) => {
|
new import_obsidian4.Setting(containerEl).setName("Default Time Estimate").setDesc("Default estimated time for new tasks in minutes").addSlider((slider) => slider.setLimits(5, 120, 5).setValue(this.plugin.settings.defaultEstimateMinutes).setDynamicTooltip().onChange(async (value) => {
|
||||||
this.plugin.settings.defaultEstimateMinutes = value;
|
this.plugin.settings.defaultEstimateMinutes = value;
|
||||||
await this.plugin.saveAllData();
|
await this.plugin.saveAllData();
|
||||||
}));
|
}));
|
||||||
new import_obsidian3.Setting(containerEl).setName("Enable Sounds").setDesc("Play sounds for timer completion and task completion").addToggle((toggle) => toggle.setValue(this.plugin.settings.enableSounds).onChange(async (value) => {
|
new import_obsidian4.Setting(containerEl).setName("Enable Sounds").setDesc("Play sounds for timer completion and task completion").addToggle((toggle) => toggle.setValue(this.plugin.settings.enableSounds).onChange(async (value) => {
|
||||||
this.plugin.settings.enableSounds = value;
|
this.plugin.settings.enableSounds = value;
|
||||||
await this.plugin.saveAllData();
|
await this.plugin.saveAllData();
|
||||||
}));
|
}));
|
||||||
new import_obsidian3.Setting(containerEl).setName("Enable Celebrations").setDesc("Show celebration messages when completing tasks").addToggle((toggle) => toggle.setValue(this.plugin.settings.enableCelebrations).onChange(async (value) => {
|
new import_obsidian4.Setting(containerEl).setName("Enable Celebrations").setDesc("Show celebration messages when completing tasks").addToggle((toggle) => toggle.setValue(this.plugin.settings.enableCelebrations).onChange(async (value) => {
|
||||||
this.plugin.settings.enableCelebrations = value;
|
this.plugin.settings.enableCelebrations = value;
|
||||||
await this.plugin.saveAllData();
|
await this.plugin.saveAllData();
|
||||||
}));
|
}));
|
||||||
containerEl.createEl("h2", { text: "\u{1F4DD} Daily Note Integration" });
|
containerEl.createEl("h2", { text: "\u{1F4DD} Daily Note Integration" });
|
||||||
new import_obsidian3.Setting(containerEl).setName("Log completed tasks to daily note").setDesc("When you complete a task, add an entry to your daily note. Uses the core Daily Notes plugin settings.").addToggle((toggle) => toggle.setValue(this.plugin.settings.logToDaily).onChange(async (value) => {
|
new import_obsidian4.Setting(containerEl).setName("Log completed tasks to daily note").setDesc("When you complete a task, add an entry to your daily note. Uses the core Daily Notes plugin settings.").addToggle((toggle) => toggle.setValue(this.plugin.settings.logToDaily).onChange(async (value) => {
|
||||||
this.plugin.settings.logToDaily = value;
|
this.plugin.settings.logToDaily = value;
|
||||||
await this.plugin.saveAllData();
|
await this.plugin.saveAllData();
|
||||||
}));
|
}));
|
||||||
@@ -1217,7 +1834,7 @@ var ImmerseSettingTab = class extends import_obsidian3.PluginSettingTab {
|
|||||||
`;
|
`;
|
||||||
containerEl.createEl("h2", { text: "\u{1F4CB} Lists" });
|
containerEl.createEl("h2", { text: "\u{1F4CB} Lists" });
|
||||||
this.plugin.settings.lists.forEach((list, index) => {
|
this.plugin.settings.lists.forEach((list, index) => {
|
||||||
new import_obsidian3.Setting(containerEl).setName(`${list.icon} ${list.name}`).addText((text) => text.setValue(list.name).setPlaceholder("List name").onChange(async (value) => {
|
new import_obsidian4.Setting(containerEl).setName(`${list.icon} ${list.name}`).addText((text) => text.setValue(list.name).setPlaceholder("List name").onChange(async (value) => {
|
||||||
this.plugin.settings.lists[index].name = value;
|
this.plugin.settings.lists[index].name = value;
|
||||||
await this.plugin.saveAllData();
|
await this.plugin.saveAllData();
|
||||||
})).addText((text) => text.setValue(list.icon).setPlaceholder("Emoji").onChange(async (value) => {
|
})).addText((text) => text.setValue(list.icon).setPlaceholder("Emoji").onChange(async (value) => {
|
||||||
@@ -1232,7 +1849,7 @@ var ImmerseSettingTab = class extends import_obsidian3.PluginSettingTab {
|
|||||||
this.display();
|
this.display();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
new import_obsidian3.Setting(containerEl).addButton((btn) => btn.setButtonText("+ Add List").onClick(async () => {
|
new import_obsidian4.Setting(containerEl).addButton((btn) => btn.setButtonText("+ Add List").onClick(async () => {
|
||||||
this.plugin.settings.lists.push({
|
this.plugin.settings.lists.push({
|
||||||
id: this.plugin.generateId(),
|
id: this.plugin.generateId(),
|
||||||
name: "New List",
|
name: "New List",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "immerse",
|
"id": "immerse",
|
||||||
"name": "Immerse",
|
"name": "Immerse",
|
||||||
"version": "1.0.9",
|
"version": "1.1.0",
|
||||||
"minAppVersion": "0.15.0",
|
"minAppVersion": "0.15.0",
|
||||||
"description": "A Blitzit-inspired task management and focus timer plugin. Plan your day, track time with Pomodoro technique, and crush your tasks with satisfying checkoffs.",
|
"description": "A Blitzit-inspired task management and focus timer plugin. Plan your day, track time with Pomodoro technique, and crush your tasks with satisfying checkoffs.",
|
||||||
"author": "Crib",
|
"author": "Crib",
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ async function packageRelease() {
|
|||||||
console.log(`\nFiles included:`);
|
console.log(`\nFiles included:`);
|
||||||
FILES_TO_INCLUDE.forEach(file => console.log(` - ${file}`));
|
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(`\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) {
|
} catch (error) {
|
||||||
console.error('❌ Error packaging release:', error);
|
console.error('❌ Error packaging release:', error);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immerse",
|
"name": "immerse",
|
||||||
"version": "1.0.9",
|
"version": "1.1.0",
|
||||||
"description": "A Blitzit-inspired task management and focus timer plugin for Obsidian",
|
"description": "A Blitzit-inspired task management and focus timer plugin for Obsidian",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
369
src/main.ts
369
src/main.ts
@@ -9,9 +9,9 @@ import {
|
|||||||
} from 'obsidian';
|
} from 'obsidian';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FocusTask,
|
ImmerseTask,
|
||||||
FocusTaskSettings,
|
ImmerseSettings,
|
||||||
FocusTaskData,
|
ImmerseData,
|
||||||
DEFAULT_SETTINGS,
|
DEFAULT_SETTINGS,
|
||||||
DEFAULT_DATA,
|
DEFAULT_DATA,
|
||||||
VIEW_TYPE_IMMERSE,
|
VIEW_TYPE_IMMERSE,
|
||||||
@@ -21,13 +21,14 @@ import {
|
|||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
import { ImmerseView } from './view';
|
import { ImmerseView } from './view';
|
||||||
|
import { ReportView, VIEW_TYPE_REPORT } from './reportView';
|
||||||
import { QuickAddTaskModal } from './modals';
|
import { QuickAddTaskModal } from './modals';
|
||||||
|
|
||||||
// ============ Main Plugin Class ============
|
// ============ Main Plugin Class ============
|
||||||
|
|
||||||
export default class ImmersePlugin extends Plugin {
|
export default class ImmersePlugin extends Plugin {
|
||||||
settings: FocusTaskSettings;
|
settings: ImmerseSettings;
|
||||||
data: FocusTaskData;
|
data: ImmerseData;
|
||||||
|
|
||||||
// Timer state
|
// Timer state
|
||||||
timerInterval: number | null = null;
|
timerInterval: number | null = null;
|
||||||
@@ -48,6 +49,10 @@ export default class ImmersePlugin extends Plugin {
|
|||||||
// Status bar element
|
// Status bar element
|
||||||
statusBarEl: HTMLElement | null = null;
|
statusBarEl: HTMLElement | null = null;
|
||||||
|
|
||||||
|
// Reminder system
|
||||||
|
private reminderCheckInterval: number | null = null;
|
||||||
|
private notifiedReminders: Set<string> = new Set(); // Track which reminders have been shown
|
||||||
|
|
||||||
async onload() {
|
async onload() {
|
||||||
await this.loadAllData();
|
await this.loadAllData();
|
||||||
|
|
||||||
@@ -61,12 +66,17 @@ export default class ImmersePlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register the main view
|
// Register views
|
||||||
this.registerView(
|
this.registerView(
|
||||||
VIEW_TYPE_IMMERSE,
|
VIEW_TYPE_IMMERSE,
|
||||||
(leaf) => new ImmerseView(leaf, this)
|
(leaf) => new ImmerseView(leaf, this)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.registerView(
|
||||||
|
VIEW_TYPE_REPORT,
|
||||||
|
(leaf) => new ReportView(leaf, this)
|
||||||
|
);
|
||||||
|
|
||||||
// Add ribbon icon
|
// Add ribbon icon
|
||||||
this.addRibbonIcon('zap', 'Open Immerse', () => {
|
this.addRibbonIcon('zap', 'Open Immerse', () => {
|
||||||
this.activateView();
|
this.activateView();
|
||||||
@@ -103,15 +113,27 @@ export default class ImmersePlugin extends Plugin {
|
|||||||
callback: () => this.completeActiveTask(),
|
callback: () => this.completeActiveTask(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: 'view-reports',
|
||||||
|
name: 'View Reports',
|
||||||
|
callback: () => this.activateReportView(),
|
||||||
|
});
|
||||||
|
|
||||||
// Add settings tab
|
// Add settings tab
|
||||||
this.addSettingTab(new ImmerseSettingTab(this.app, this));
|
this.addSettingTab(new ImmerseSettingTab(this.app, this));
|
||||||
|
|
||||||
// Create status bar timer
|
// Create status bar timer
|
||||||
this.createStatusBar();
|
this.createStatusBar();
|
||||||
|
|
||||||
|
// Start reminder checking system
|
||||||
|
if (this.settings.enableReminders) {
|
||||||
|
this.startReminderSystem();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
this.stopTimer();
|
this.stopTimer();
|
||||||
|
this.stopReminderSystem();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadAllData() {
|
async loadAllData() {
|
||||||
@@ -181,9 +203,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 ============
|
// ============ Task Management ============
|
||||||
|
|
||||||
createTask(text: string, estimatedMinutes: number = this.settings.defaultEstimateMinutes, list: string = 'work'): FocusTask {
|
createTask(text: string, estimatedMinutes: number = this.settings.defaultEstimateMinutes, list: string = 'work'): ImmerseTask {
|
||||||
return {
|
return {
|
||||||
id: this.generateId(),
|
id: this.generateId(),
|
||||||
text,
|
text,
|
||||||
@@ -201,13 +243,13 @@ export default class ImmersePlugin extends Plugin {
|
|||||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
addTask(task: FocusTask) {
|
addTask(task: ImmerseTask) {
|
||||||
this.data.tasks.push(task);
|
this.data.tasks.push(task);
|
||||||
this.saveAllData();
|
this.saveAllData();
|
||||||
this.refreshView();
|
this.refreshView();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTask(taskId: string, updates: Partial<FocusTask>) {
|
updateTask(taskId: string, updates: Partial<ImmerseTask>) {
|
||||||
const task = this.data.tasks.find(t => t.id === taskId);
|
const task = this.data.tasks.find(t => t.id === taskId);
|
||||||
if (task) {
|
if (task) {
|
||||||
Object.assign(task, updates);
|
Object.assign(task, updates);
|
||||||
@@ -236,6 +278,12 @@ export default class ImmersePlugin extends Plugin {
|
|||||||
this.data.completedToday++;
|
this.data.completedToday++;
|
||||||
this.data.lastActiveDate = new Date().toDateString();
|
this.data.lastActiveDate = new Date().toDateString();
|
||||||
|
|
||||||
|
// Archive task for historical reporting
|
||||||
|
this.archiveCompletedTask(task);
|
||||||
|
|
||||||
|
// Update daily stats
|
||||||
|
this.updateDailyStats(task);
|
||||||
|
|
||||||
// Show celebration
|
// Show celebration
|
||||||
if (this.settings.enableCelebrations) {
|
if (this.settings.enableCelebrations) {
|
||||||
this.showCelebration(task);
|
this.showCelebration(task);
|
||||||
@@ -269,6 +317,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 ============
|
// ============ Timer Management ============
|
||||||
|
|
||||||
// Sync timer based on timestamp when app returns from background
|
// Sync timer based on timestamp when app returns from background
|
||||||
@@ -578,7 +835,7 @@ export default class ImmersePlugin extends Plugin {
|
|||||||
|
|
||||||
createStatusBar() {
|
createStatusBar() {
|
||||||
this.statusBarEl = this.addStatusBarItem();
|
this.statusBarEl = this.addStatusBarItem();
|
||||||
this.statusBarEl.addClass('focus-task-status-bar');
|
this.statusBarEl.addClass('immerse-status-bar');
|
||||||
this.updateStatusBar();
|
this.updateStatusBar();
|
||||||
|
|
||||||
// Click to open panel
|
// Click to open panel
|
||||||
@@ -597,16 +854,94 @@ export default class ImmersePlugin extends Plugin {
|
|||||||
const icon = this.isTimerRunning ? '▶' : '⏸';
|
const icon = this.isTimerRunning ? '▶' : '⏸';
|
||||||
|
|
||||||
this.statusBarEl.setText(`⚡ ${icon} ${timeStr} - ${taskName}${task && task.text.length > 20 ? '...' : ''}`);
|
this.statusBarEl.setText(`⚡ ${icon} ${timeStr} - ${taskName}${task && task.text.length > 20 ? '...' : ''}`);
|
||||||
this.statusBarEl.addClass('focus-task-status-active');
|
this.statusBarEl.addClass('immerse-status-active');
|
||||||
} else {
|
} else {
|
||||||
this.statusBarEl.setText('⚡ Immerse');
|
this.statusBarEl.setText('⚡ Immerse');
|
||||||
this.statusBarEl.removeClass('focus-task-status-active');
|
this.statusBarEl.removeClass('immerse-status-active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ Reminder System ============
|
||||||
|
|
||||||
|
startReminderSystem() {
|
||||||
|
// Check for reminders every 30 seconds
|
||||||
|
this.reminderCheckInterval = window.setInterval(() => {
|
||||||
|
this.checkReminders();
|
||||||
|
}, 30000);
|
||||||
|
// Also check immediately
|
||||||
|
this.checkReminders();
|
||||||
|
}
|
||||||
|
|
||||||
|
stopReminderSystem() {
|
||||||
|
if (this.reminderCheckInterval) {
|
||||||
|
window.clearInterval(this.reminderCheckInterval);
|
||||||
|
this.reminderCheckInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkReminders() {
|
||||||
|
if (!this.settings.enableReminders) return;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const currentTime = now.getTime();
|
||||||
|
|
||||||
|
// Check each incomplete task with scheduling
|
||||||
|
this.data.tasks
|
||||||
|
.filter(task => !task.completed && task.scheduledDate && task.scheduledTime)
|
||||||
|
.forEach(task => {
|
||||||
|
const reminderKey = `${task.id}-${task.scheduledDate}-${task.scheduledTime}`;
|
||||||
|
|
||||||
|
// Skip if we already notified for this reminder
|
||||||
|
if (this.notifiedReminders.has(reminderKey)) return;
|
||||||
|
|
||||||
|
// Parse the scheduled date and time
|
||||||
|
const scheduledDateTime = new Date(`${task.scheduledDate}T${task.scheduledTime}`);
|
||||||
|
const scheduledTime = scheduledDateTime.getTime();
|
||||||
|
|
||||||
|
// Check for overdue tasks (scheduled time has passed)
|
||||||
|
if (currentTime > scheduledTime) {
|
||||||
|
this.showOverdueNotice(task);
|
||||||
|
this.notifiedReminders.add(reminderKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for tasks with reminders set
|
||||||
|
if (task.reminderMinutes) {
|
||||||
|
const reminderTime = scheduledTime - (task.reminderMinutes * 60 * 1000);
|
||||||
|
|
||||||
|
// Show reminder if it's past the reminder time but before the scheduled time
|
||||||
|
if (currentTime >= reminderTime) {
|
||||||
|
this.showReminder(task);
|
||||||
|
this.notifiedReminders.add(reminderKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showReminder(task: ImmerseTask) {
|
||||||
|
const timeStr = task.scheduledTime;
|
||||||
|
new Notice(`🔔 Reminder: "${task.text}" is scheduled for ${timeStr}`, 8000);
|
||||||
|
|
||||||
|
// Play alert sound if enabled
|
||||||
|
if (this.settings.enableSounds) {
|
||||||
|
this.playAlertSound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showOverdueNotice(task: ImmerseTask) {
|
||||||
|
const dateStr = task.scheduledDate;
|
||||||
|
const timeStr = task.scheduledTime;
|
||||||
|
new Notice(`⚠️ Overdue: "${task.text}" was scheduled for ${dateStr} ${timeStr}`, 10000);
|
||||||
|
|
||||||
|
// Play alert sound if enabled
|
||||||
|
if (this.settings.enableSounds) {
|
||||||
|
this.playAlertSound();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ Sounds & Celebrations ============
|
// ============ Sounds & Celebrations ============
|
||||||
|
|
||||||
showCelebration(task: FocusTask) {
|
showCelebration(task: ImmerseTask) {
|
||||||
let messages = CELEBRATION_MESSAGES;
|
let messages = CELEBRATION_MESSAGES;
|
||||||
let extraMessage = '';
|
let extraMessage = '';
|
||||||
|
|
||||||
@@ -673,7 +1008,7 @@ export default class ImmersePlugin extends Plugin {
|
|||||||
|
|
||||||
// ============ Daily Note Logging ============
|
// ============ Daily Note Logging ============
|
||||||
|
|
||||||
async logTaskToDailyNote(task: FocusTask) {
|
async logTaskToDailyNote(task: ImmerseTask) {
|
||||||
try {
|
try {
|
||||||
const list = this.settings.lists.find(l => l.id === task.list);
|
const list = this.settings.lists.find(l => l.id === task.list);
|
||||||
|
|
||||||
@@ -862,15 +1197,15 @@ export default class ImmersePlugin extends Plugin {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getTasksByList(listId: string): FocusTask[] {
|
getTasksByList(listId: string): ImmerseTask[] {
|
||||||
return this.data.tasks.filter(t => t.list === listId);
|
return this.data.tasks.filter(t => t.list === listId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPendingTasks(): FocusTask[] {
|
getPendingTasks(): ImmerseTask[] {
|
||||||
return this.data.tasks.filter(t => !t.completed);
|
return this.data.tasks.filter(t => !t.completed);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTodaysTasks(): FocusTask[] {
|
getTodaysTasks(): ImmerseTask[] {
|
||||||
const today = new Date().toDateString();
|
const today = new Date().toDateString();
|
||||||
return this.data.tasks.filter(t => {
|
return this.data.tasks.filter(t => {
|
||||||
if (t.scheduledDate === today) return true;
|
if (t.scheduledDate === today) return true;
|
||||||
|
|||||||
352
src/modals.ts
352
src/modals.ts
@@ -5,7 +5,7 @@ import {
|
|||||||
Setting,
|
Setting,
|
||||||
} from 'obsidian';
|
} from 'obsidian';
|
||||||
|
|
||||||
import { FocusTask } from './types';
|
import { ImmerseTask } from './types';
|
||||||
import ImmersePlugin from './main';
|
import ImmersePlugin from './main';
|
||||||
|
|
||||||
// ============ Quick Add Task Modal ============
|
// ============ Quick Add Task Modal ============
|
||||||
@@ -15,11 +15,15 @@ export class QuickAddTaskModal extends Modal {
|
|||||||
taskText: string = '';
|
taskText: string = '';
|
||||||
estimatedMinutes: number;
|
estimatedMinutes: number;
|
||||||
selectedList: string = 'work';
|
selectedList: string = 'work';
|
||||||
|
scheduledDate: string = '';
|
||||||
|
scheduledTime: string = '';
|
||||||
|
reminderMinutes: number = 0;
|
||||||
|
|
||||||
constructor(app: App, plugin: ImmersePlugin) {
|
constructor(app: App, plugin: ImmersePlugin) {
|
||||||
super(app);
|
super(app);
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.estimatedMinutes = plugin.settings.defaultEstimateMinutes;
|
this.estimatedMinutes = plugin.settings.defaultEstimateMinutes;
|
||||||
|
this.reminderMinutes = plugin.settings.defaultReminderMinutes;
|
||||||
if (plugin.settings.lists.length > 0) {
|
if (plugin.settings.lists.length > 0) {
|
||||||
this.selectedList = plugin.settings.lists[0].id;
|
this.selectedList = plugin.settings.lists[0].id;
|
||||||
}
|
}
|
||||||
@@ -82,6 +86,45 @@ export class QuickAddTaskModal extends Modal {
|
|||||||
dropdown.onChange(value => this.selectedList = value);
|
dropdown.onChange(value => this.selectedList = value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Scheduled date
|
||||||
|
new Setting(contentEl)
|
||||||
|
.setName('📅 Scheduled Date')
|
||||||
|
.setDesc('Optional: When do you plan to work on this?')
|
||||||
|
.addText(text => {
|
||||||
|
text.setPlaceholder('YYYY-MM-DD')
|
||||||
|
.setValue(this.scheduledDate)
|
||||||
|
.onChange(value => this.scheduledDate = value);
|
||||||
|
text.inputEl.type = 'date';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scheduled time
|
||||||
|
new Setting(contentEl)
|
||||||
|
.setName('⏰ Scheduled Time')
|
||||||
|
.setDesc('Optional: What time?')
|
||||||
|
.addText(text => {
|
||||||
|
text.setPlaceholder('HH:mm')
|
||||||
|
.setValue(this.scheduledTime)
|
||||||
|
.onChange(value => this.scheduledTime = value);
|
||||||
|
text.inputEl.type = 'time';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reminder
|
||||||
|
if (this.plugin.settings.enableReminders) {
|
||||||
|
new Setting(contentEl)
|
||||||
|
.setName('🔔 Reminder')
|
||||||
|
.setDesc('Remind me before the scheduled time')
|
||||||
|
.addDropdown(dropdown => {
|
||||||
|
dropdown.addOption('0', 'No reminder');
|
||||||
|
dropdown.addOption('5', '5 minutes before');
|
||||||
|
dropdown.addOption('10', '10 minutes before');
|
||||||
|
dropdown.addOption('15', '15 minutes before');
|
||||||
|
dropdown.addOption('30', '30 minutes before');
|
||||||
|
dropdown.addOption('60', '1 hour before');
|
||||||
|
dropdown.setValue(this.reminderMinutes.toString());
|
||||||
|
dropdown.onChange(value => this.reminderMinutes = parseInt(value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
const buttonContainer = contentEl.createEl('div', { cls: 'immerse-modal-buttons' });
|
const buttonContainer = contentEl.createEl('div', { cls: 'immerse-modal-buttons' });
|
||||||
|
|
||||||
@@ -95,6 +138,16 @@ export class QuickAddTaskModal extends Modal {
|
|||||||
submitTask() {
|
submitTask() {
|
||||||
if (this.taskText.trim()) {
|
if (this.taskText.trim()) {
|
||||||
const task = this.plugin.createTask(this.taskText, this.estimatedMinutes, this.selectedList);
|
const task = this.plugin.createTask(this.taskText, this.estimatedMinutes, this.selectedList);
|
||||||
|
// Add scheduling data if provided
|
||||||
|
if (this.scheduledDate) {
|
||||||
|
task.scheduledDate = this.scheduledDate;
|
||||||
|
}
|
||||||
|
if (this.scheduledTime) {
|
||||||
|
task.scheduledTime = this.scheduledTime;
|
||||||
|
}
|
||||||
|
if (this.reminderMinutes > 0 && this.scheduledDate && this.scheduledTime) {
|
||||||
|
task.reminderMinutes = this.reminderMinutes;
|
||||||
|
}
|
||||||
this.plugin.addTask(task);
|
this.plugin.addTask(task);
|
||||||
new Notice('✅ Task added!');
|
new Notice('✅ Task added!');
|
||||||
this.close();
|
this.close();
|
||||||
@@ -113,9 +166,9 @@ export class QuickAddTaskModal extends Modal {
|
|||||||
|
|
||||||
export class EditTaskModal extends Modal {
|
export class EditTaskModal extends Modal {
|
||||||
plugin: ImmersePlugin;
|
plugin: ImmersePlugin;
|
||||||
task: FocusTask;
|
task: ImmerseTask;
|
||||||
|
|
||||||
constructor(app: App, plugin: ImmersePlugin, task: FocusTask) {
|
constructor(app: App, plugin: ImmersePlugin, task: ImmerseTask) {
|
||||||
super(app);
|
super(app);
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.task = { ...task };
|
this.task = { ...task };
|
||||||
@@ -177,6 +230,48 @@ export class EditTaskModal extends Modal {
|
|||||||
textarea.inputEl.rows = 4;
|
textarea.inputEl.rows = 4;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Scheduled date
|
||||||
|
new Setting(contentEl)
|
||||||
|
.setName('📅 Scheduled Date')
|
||||||
|
.setDesc('Optional: When do you plan to work on this?')
|
||||||
|
.addText(text => {
|
||||||
|
text.setPlaceholder('YYYY-MM-DD')
|
||||||
|
.setValue(this.task.scheduledDate || '')
|
||||||
|
.onChange(value => this.task.scheduledDate = value || undefined);
|
||||||
|
text.inputEl.type = 'date';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scheduled time
|
||||||
|
new Setting(contentEl)
|
||||||
|
.setName('⏰ Scheduled Time')
|
||||||
|
.setDesc('Optional: What time?')
|
||||||
|
.addText(text => {
|
||||||
|
text.setPlaceholder('HH:mm')
|
||||||
|
.setValue(this.task.scheduledTime || '')
|
||||||
|
.onChange(value => this.task.scheduledTime = value || undefined);
|
||||||
|
text.inputEl.type = 'time';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reminder
|
||||||
|
if (this.plugin.settings.enableReminders) {
|
||||||
|
new Setting(contentEl)
|
||||||
|
.setName('🔔 Reminder')
|
||||||
|
.setDesc('Remind me before the scheduled time')
|
||||||
|
.addDropdown(dropdown => {
|
||||||
|
dropdown.addOption('0', 'No reminder');
|
||||||
|
dropdown.addOption('5', '5 minutes before');
|
||||||
|
dropdown.addOption('10', '10 minutes before');
|
||||||
|
dropdown.addOption('15', '15 minutes before');
|
||||||
|
dropdown.addOption('30', '30 minutes before');
|
||||||
|
dropdown.addOption('60', '1 hour before');
|
||||||
|
dropdown.setValue((this.task.reminderMinutes || 0).toString());
|
||||||
|
dropdown.onChange(value => {
|
||||||
|
const minutes = parseInt(value);
|
||||||
|
this.task.reminderMinutes = minutes > 0 ? minutes : undefined;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Show actual time if task has been worked on
|
// Show actual time if task has been worked on
|
||||||
if (this.task.actualMinutes > 0) {
|
if (this.task.actualMinutes > 0) {
|
||||||
new Setting(contentEl)
|
new Setting(contentEl)
|
||||||
@@ -202,3 +297,254 @@ export class EditTaskModal extends Modal {
|
|||||||
contentEl.empty();
|
contentEl.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ Report Modal ============
|
||||||
|
|
||||||
|
export class ReportModal extends Modal {
|
||||||
|
plugin: ImmersePlugin;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
selectedListIds: string[] = [];
|
||||||
|
|
||||||
|
constructor(app: App, plugin: ImmersePlugin) {
|
||||||
|
super(app);
|
||||||
|
this.plugin = plugin;
|
||||||
|
|
||||||
|
// Default to last 7 days
|
||||||
|
const today = new Date();
|
||||||
|
this.endDate = today.toISOString().split('T')[0];
|
||||||
|
const weekAgo = new Date(today);
|
||||||
|
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||||
|
this.startDate = weekAgo.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpen() {
|
||||||
|
const { contentEl } = this;
|
||||||
|
contentEl.addClass('immerse-modal', 'immerse-report-modal');
|
||||||
|
contentEl.empty();
|
||||||
|
|
||||||
|
// Header
|
||||||
|
const header = contentEl.createEl('div', { cls: 'immerse-report-header' });
|
||||||
|
header.createEl('h2', { text: '📊 Reports', cls: 'immerse-report-title' });
|
||||||
|
|
||||||
|
// Filters section
|
||||||
|
this.renderFilters(contentEl);
|
||||||
|
|
||||||
|
// Generate button
|
||||||
|
const generateBtn = contentEl.createEl('button', {
|
||||||
|
text: '🔄 Generate Report',
|
||||||
|
cls: 'immerse-btn immerse-btn-primary immerse-report-generate-btn'
|
||||||
|
});
|
||||||
|
generateBtn.addEventListener('click', () => this.renderReport(contentEl));
|
||||||
|
|
||||||
|
// Initial report render
|
||||||
|
this.renderReport(contentEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFilters(container: Element) {
|
||||||
|
const filtersSection = container.createEl('div', { cls: 'immerse-report-filters' });
|
||||||
|
|
||||||
|
// Date range
|
||||||
|
const dateRow = filtersSection.createEl('div', { cls: 'immerse-report-filter-row' });
|
||||||
|
|
||||||
|
new Setting(dateRow)
|
||||||
|
.setName('Start Date')
|
||||||
|
.addText(text => {
|
||||||
|
text.setValue(this.startDate)
|
||||||
|
.onChange(value => this.startDate = value);
|
||||||
|
text.inputEl.type = 'date';
|
||||||
|
});
|
||||||
|
|
||||||
|
new Setting(dateRow)
|
||||||
|
.setName('End Date')
|
||||||
|
.addText(text => {
|
||||||
|
text.setValue(this.endDate)
|
||||||
|
.onChange(value => this.endDate = value);
|
||||||
|
text.inputEl.type = 'date';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Quick filters
|
||||||
|
const quickFilters = filtersSection.createEl('div', { cls: 'immerse-report-quick-filters' });
|
||||||
|
quickFilters.createEl('span', { text: 'Quick select: ', cls: 'immerse-filter-label' });
|
||||||
|
|
||||||
|
const filters = [
|
||||||
|
{ label: 'Today', days: 0 },
|
||||||
|
{ label: 'Last 7 days', days: 7 },
|
||||||
|
{ label: 'Last 30 days', days: 30 },
|
||||||
|
{ label: 'Last 90 days', days: 90 },
|
||||||
|
];
|
||||||
|
|
||||||
|
filters.forEach(filter => {
|
||||||
|
const btn = quickFilters.createEl('button', {
|
||||||
|
text: filter.label,
|
||||||
|
cls: 'immerse-quick-filter-btn'
|
||||||
|
});
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const today = new Date();
|
||||||
|
this.endDate = today.toISOString().split('T')[0];
|
||||||
|
const startDate = new Date(today);
|
||||||
|
startDate.setDate(startDate.getDate() - filter.days);
|
||||||
|
this.startDate = startDate.toISOString().split('T')[0];
|
||||||
|
this.renderReport(container);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderReport(container: Element) {
|
||||||
|
// Remove old report if exists
|
||||||
|
const oldReport = container.querySelector('.immerse-report-content');
|
||||||
|
if (oldReport) oldReport.remove();
|
||||||
|
|
||||||
|
// Generate report data
|
||||||
|
const filters: import('./types').ReportFilters = {
|
||||||
|
startDate: this.startDate,
|
||||||
|
endDate: this.endDate,
|
||||||
|
listIds: this.selectedListIds.length > 0 ? this.selectedListIds : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const reportData = this.plugin.generateReport(filters);
|
||||||
|
|
||||||
|
// Create report content
|
||||||
|
const reportContent = container.createEl('div', { cls: 'immerse-report-content' });
|
||||||
|
|
||||||
|
// Check if we have data
|
||||||
|
if (reportData.totalTasks === 0) {
|
||||||
|
reportContent.createEl('div', {
|
||||||
|
text: 'No data available for the selected period. Complete some tasks to see your stats!',
|
||||||
|
cls: 'immerse-no-data-message'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary stats
|
||||||
|
this.renderSummaryStats(reportContent, reportData);
|
||||||
|
|
||||||
|
// Time by list (donut chart)
|
||||||
|
if (reportData.timeByList.length > 0) {
|
||||||
|
this.renderTimeByList(reportContent, reportData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Productivity insights
|
||||||
|
this.renderInsights(reportContent, reportData);
|
||||||
|
|
||||||
|
// Daily breakdown (bar chart - simplified text version)
|
||||||
|
if (reportData.dailyBreakdown.length > 0) {
|
||||||
|
this.renderDailyBreakdown(reportContent, reportData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSummaryStats(container: Element, data: import('./types').ReportData) {
|
||||||
|
const statsGrid = container.createEl('div', { cls: 'immerse-stats-grid' });
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{ label: 'TASKS DONE', value: data.totalTasks.toString(), icon: '✓' },
|
||||||
|
{ label: 'TASKS PER DAY', value: data.tasksPerDay.toFixed(1), icon: '📅' },
|
||||||
|
{ label: 'HOURS PER DAY', value: data.hoursPerDay.toFixed(1), icon: '⏰' },
|
||||||
|
{ label: 'MINS PER TASK', value: data.minsPerTask.toString(), icon: '⏱️' },
|
||||||
|
{ label: 'DAY STREAK', value: data.currentStreak.toString(), icon: '🔥' },
|
||||||
|
{ label: 'TOTAL HOURS', value: (data.totalMinutes / 60).toFixed(1), icon: '⌚' },
|
||||||
|
];
|
||||||
|
|
||||||
|
stats.forEach(stat => {
|
||||||
|
const statCard = statsGrid.createEl('div', { cls: 'immerse-stat-card' });
|
||||||
|
statCard.createEl('div', { text: stat.label, cls: 'immerse-stat-label' });
|
||||||
|
const valueRow = statCard.createEl('div', { cls: 'immerse-stat-value-row' });
|
||||||
|
valueRow.createEl('span', { text: stat.icon, cls: 'immerse-stat-icon' });
|
||||||
|
valueRow.createEl('span', { text: stat.value, cls: 'immerse-stat-value' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTimeByList(container: Element, data: import('./types').ReportData) {
|
||||||
|
const section = container.createEl('div', { cls: 'immerse-report-section' });
|
||||||
|
section.createEl('h3', { text: 'Time by List', cls: 'immerse-report-section-title' });
|
||||||
|
|
||||||
|
const listContainer = section.createEl('div', { cls: 'immerse-time-by-list' });
|
||||||
|
|
||||||
|
data.timeByList.forEach(item => {
|
||||||
|
const listItem = listContainer.createEl('div', { cls: 'immerse-list-stat-item' });
|
||||||
|
|
||||||
|
// List info
|
||||||
|
const listInfo = listItem.createEl('div', { cls: 'immerse-list-info' });
|
||||||
|
listInfo.createEl('span', { text: item.listIcon, cls: 'immerse-list-icon' });
|
||||||
|
listInfo.createEl('span', { text: item.listName, cls: 'immerse-list-name' });
|
||||||
|
|
||||||
|
// Progress bar
|
||||||
|
const progressBar = listItem.createEl('div', { cls: 'immerse-list-progress' });
|
||||||
|
const progress = progressBar.createEl('div', { cls: 'immerse-list-progress-fill' });
|
||||||
|
progress.style.width = `${item.percentage}%`;
|
||||||
|
progress.style.backgroundColor = item.listColor;
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
const stats = listItem.createEl('div', { cls: 'immerse-list-stats' });
|
||||||
|
stats.createEl('span', {
|
||||||
|
text: `${item.taskCount} tasks`,
|
||||||
|
cls: 'immerse-list-stat-text'
|
||||||
|
});
|
||||||
|
stats.createEl('span', {
|
||||||
|
text: `${this.plugin.formatTimeHuman(item.minutes)}`,
|
||||||
|
cls: 'immerse-list-stat-text'
|
||||||
|
});
|
||||||
|
stats.createEl('span', {
|
||||||
|
text: `${item.percentage.toFixed(1)}%`,
|
||||||
|
cls: 'immerse-list-stat-percentage'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInsights(container: Element, data: import('./types').ReportData) {
|
||||||
|
const section = container.createEl('div', { cls: 'immerse-report-section' });
|
||||||
|
section.createEl('h3', { text: 'Productivity Insights', cls: 'immerse-report-section-title' });
|
||||||
|
|
||||||
|
const insightsGrid = section.createEl('div', { cls: 'immerse-insights-grid' });
|
||||||
|
|
||||||
|
if (data.mostProductiveHour !== undefined) {
|
||||||
|
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
|
||||||
|
card.createEl('div', { text: 'MOST PRODUCTIVE HOUR', cls: 'immerse-insight-label' });
|
||||||
|
card.createEl('div', {
|
||||||
|
text: `${data.mostProductiveHour}:00 - ${data.mostProductiveHour + 1}:00`,
|
||||||
|
cls: 'immerse-insight-value'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.mostProductiveDay) {
|
||||||
|
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
|
||||||
|
card.createEl('div', { text: 'MOST PRODUCTIVE DAY', cls: 'immerse-insight-label' });
|
||||||
|
card.createEl('div', { text: data.mostProductiveDay, cls: 'immerse-insight-value' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.mostProductiveMonth) {
|
||||||
|
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
|
||||||
|
card.createEl('div', { text: 'MOST PRODUCTIVE MONTH', cls: 'immerse-insight-label' });
|
||||||
|
card.createEl('div', { text: data.mostProductiveMonth, cls: 'immerse-insight-value' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDailyBreakdown(container: Element, data: import('./types').ReportData) {
|
||||||
|
const section = container.createEl('div', { cls: 'immerse-report-section' });
|
||||||
|
section.createEl('h3', { text: 'Daily Breakdown', cls: 'immerse-report-section-title' });
|
||||||
|
|
||||||
|
const table = section.createEl('table', { cls: 'immerse-daily-table' });
|
||||||
|
const thead = table.createEl('thead');
|
||||||
|
const headerRow = thead.createEl('tr');
|
||||||
|
headerRow.createEl('th', { text: 'Date' });
|
||||||
|
headerRow.createEl('th', { text: 'Tasks' });
|
||||||
|
headerRow.createEl('th', { text: 'Hours' });
|
||||||
|
headerRow.createEl('th', { text: 'Pomodoros' });
|
||||||
|
|
||||||
|
const tbody = table.createEl('tbody');
|
||||||
|
// Show last 14 days max
|
||||||
|
const recentData = data.dailyBreakdown.slice(-14);
|
||||||
|
recentData.forEach(day => {
|
||||||
|
const row = tbody.createEl('tr');
|
||||||
|
row.createEl('td', { text: day.date });
|
||||||
|
row.createEl('td', { text: day.tasks.toString() });
|
||||||
|
row.createEl('td', { text: day.hours.toFixed(1) });
|
||||||
|
row.createEl('td', { text: day.pomodoros.toString() });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
const { contentEl } = this;
|
||||||
|
contentEl.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
394
src/reportView.ts
Normal file
394
src/reportView.ts
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
import {
|
||||||
|
ItemView,
|
||||||
|
WorkspaceLeaf,
|
||||||
|
Setting,
|
||||||
|
} from 'obsidian';
|
||||||
|
|
||||||
|
import { ReportFilters, ReportData } from './types';
|
||||||
|
import ImmersePlugin from './main';
|
||||||
|
|
||||||
|
export const VIEW_TYPE_REPORT = 'immerse-report-view';
|
||||||
|
|
||||||
|
export class ReportView extends ItemView {
|
||||||
|
plugin: ImmersePlugin;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
selectedListIds: string[] = [];
|
||||||
|
|
||||||
|
constructor(leaf: WorkspaceLeaf, plugin: ImmersePlugin) {
|
||||||
|
super(leaf);
|
||||||
|
this.plugin = plugin;
|
||||||
|
|
||||||
|
// Default to last 7 days
|
||||||
|
const today = new Date();
|
||||||
|
this.endDate = today.toISOString().split('T')[0];
|
||||||
|
const weekAgo = new Date(today);
|
||||||
|
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||||
|
this.startDate = weekAgo.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
getViewType(): string {
|
||||||
|
return VIEW_TYPE_REPORT;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayText(): string {
|
||||||
|
return '📊 Reports';
|
||||||
|
}
|
||||||
|
|
||||||
|
getIcon(): string {
|
||||||
|
return 'bar-chart-2';
|
||||||
|
}
|
||||||
|
|
||||||
|
async onOpen() {
|
||||||
|
const container = this.containerEl.children[1];
|
||||||
|
container.empty();
|
||||||
|
container.addClass('immerse-report-view');
|
||||||
|
|
||||||
|
this.renderContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onClose() {
|
||||||
|
// Nothing to clean up
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent() {
|
||||||
|
const container = this.containerEl.children[1];
|
||||||
|
container.empty();
|
||||||
|
|
||||||
|
// Header
|
||||||
|
const header = container.createEl('div', { cls: 'immerse-report-header' });
|
||||||
|
header.createEl('h2', { text: '📊 Reports', cls: 'immerse-report-title' });
|
||||||
|
|
||||||
|
// Filters section
|
||||||
|
this.renderFilters(container);
|
||||||
|
|
||||||
|
// Generate button
|
||||||
|
const generateBtn = container.createEl('button', {
|
||||||
|
text: '🔄 Generate Report',
|
||||||
|
cls: 'immerse-btn immerse-btn-primary immerse-report-generate-btn'
|
||||||
|
});
|
||||||
|
generateBtn.addEventListener('click', () => this.renderReport(container));
|
||||||
|
|
||||||
|
// Initial report render
|
||||||
|
this.renderReport(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFilters(container: Element) {
|
||||||
|
const filtersSection = container.createEl('div', { cls: 'immerse-report-filters' });
|
||||||
|
|
||||||
|
// Date range
|
||||||
|
const dateRow = filtersSection.createEl('div', { cls: 'immerse-report-filter-row' });
|
||||||
|
|
||||||
|
new Setting(dateRow)
|
||||||
|
.setName('Start Date')
|
||||||
|
.addText(text => {
|
||||||
|
text.setValue(this.startDate)
|
||||||
|
.onChange(value => this.startDate = value);
|
||||||
|
text.inputEl.type = 'date';
|
||||||
|
});
|
||||||
|
|
||||||
|
new Setting(dateRow)
|
||||||
|
.setName('End Date')
|
||||||
|
.addText(text => {
|
||||||
|
text.setValue(this.endDate)
|
||||||
|
.onChange(value => this.endDate = value);
|
||||||
|
text.inputEl.type = 'date';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Quick filters
|
||||||
|
const quickFilters = filtersSection.createEl('div', { cls: 'immerse-report-quick-filters' });
|
||||||
|
quickFilters.createEl('span', { text: 'Quick select: ', cls: 'immerse-filter-label' });
|
||||||
|
|
||||||
|
const filters = [
|
||||||
|
{ label: 'Today', days: 0 },
|
||||||
|
{ label: 'Last 7 days', days: 7 },
|
||||||
|
{ label: 'Last 30 days', days: 30 },
|
||||||
|
{ label: 'Last 90 days', days: 90 },
|
||||||
|
];
|
||||||
|
|
||||||
|
filters.forEach(filter => {
|
||||||
|
const btn = quickFilters.createEl('button', {
|
||||||
|
text: filter.label,
|
||||||
|
cls: 'immerse-quick-filter-btn'
|
||||||
|
});
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const today = new Date();
|
||||||
|
this.endDate = today.toISOString().split('T')[0];
|
||||||
|
const startDate = new Date(today);
|
||||||
|
startDate.setDate(startDate.getDate() - filter.days);
|
||||||
|
this.startDate = startDate.toISOString().split('T')[0];
|
||||||
|
this.renderReport(container);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderReport(container: Element) {
|
||||||
|
// Remove old report if exists
|
||||||
|
const oldReport = container.querySelector('.immerse-report-content');
|
||||||
|
if (oldReport) oldReport.remove();
|
||||||
|
|
||||||
|
// Generate report data
|
||||||
|
const filters: ReportFilters = {
|
||||||
|
startDate: this.startDate,
|
||||||
|
endDate: this.endDate,
|
||||||
|
listIds: this.selectedListIds.length > 0 ? this.selectedListIds : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const reportData = this.plugin.generateReport(filters);
|
||||||
|
|
||||||
|
// Create report content
|
||||||
|
const reportContent = container.createEl('div', { cls: 'immerse-report-content' });
|
||||||
|
|
||||||
|
// Check if we have data
|
||||||
|
if (reportData.totalTasks === 0) {
|
||||||
|
reportContent.createEl('div', {
|
||||||
|
text: 'No data available for the selected period. Complete some tasks to see your stats!',
|
||||||
|
cls: 'immerse-no-data-message'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary stats
|
||||||
|
this.renderSummaryStats(reportContent, reportData);
|
||||||
|
|
||||||
|
// Time by list
|
||||||
|
if (reportData.timeByList.length > 0) {
|
||||||
|
this.renderTimeByList(reportContent, reportData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Productivity insights
|
||||||
|
this.renderInsights(reportContent, reportData);
|
||||||
|
|
||||||
|
// Daily breakdown
|
||||||
|
if (reportData.dailyBreakdown.length > 0) {
|
||||||
|
this.renderDailyBreakdown(reportContent, reportData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSummaryStats(container: Element, data: ReportData) {
|
||||||
|
const statsGrid = container.createEl('div', { cls: 'immerse-stats-grid' });
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{ label: 'TASKS DONE', value: data.totalTasks.toString(), icon: '✓' },
|
||||||
|
{ label: 'TASKS PER DAY', value: data.tasksPerDay.toFixed(1), icon: '📅' },
|
||||||
|
{ label: 'HOURS PER DAY', value: data.hoursPerDay.toFixed(1), icon: '⏰' },
|
||||||
|
{ label: 'MINS PER TASK', value: data.minsPerTask.toString(), icon: '⏱️' },
|
||||||
|
{ label: 'DAY STREAK', value: data.currentStreak.toString(), icon: '🔥' },
|
||||||
|
{ label: 'TOTAL HOURS', value: (data.totalMinutes / 60).toFixed(1), icon: '⌚' },
|
||||||
|
];
|
||||||
|
|
||||||
|
stats.forEach(stat => {
|
||||||
|
const statCard = statsGrid.createEl('div', { cls: 'immerse-stat-card' });
|
||||||
|
statCard.createEl('div', { text: stat.label, cls: 'immerse-stat-label' });
|
||||||
|
const valueRow = statCard.createEl('div', { cls: 'immerse-stat-value-row' });
|
||||||
|
valueRow.createEl('span', { text: stat.icon, cls: 'immerse-stat-icon' });
|
||||||
|
valueRow.createEl('span', { text: stat.value, cls: 'immerse-stat-value' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTimeByList(container: Element, data: ReportData) {
|
||||||
|
const section = container.createEl('div', { cls: 'immerse-report-section' });
|
||||||
|
section.createEl('h3', { text: 'Time by List', cls: 'immerse-report-section-title' });
|
||||||
|
|
||||||
|
const listContainer = section.createEl('div', { cls: 'immerse-time-by-list' });
|
||||||
|
|
||||||
|
data.timeByList.forEach(item => {
|
||||||
|
const listItem = listContainer.createEl('div', { cls: 'immerse-list-stat-item' });
|
||||||
|
|
||||||
|
// List info
|
||||||
|
const listInfo = listItem.createEl('div', { cls: 'immerse-list-info' });
|
||||||
|
listInfo.createEl('span', { text: item.listIcon, cls: 'immerse-list-icon' });
|
||||||
|
listInfo.createEl('span', { text: item.listName, cls: 'immerse-list-name' });
|
||||||
|
|
||||||
|
// Progress bar
|
||||||
|
const progressBar = listItem.createEl('div', { cls: 'immerse-list-progress' });
|
||||||
|
const progress = progressBar.createEl('div', { cls: 'immerse-list-progress-fill' });
|
||||||
|
progress.style.width = `${item.percentage}%`;
|
||||||
|
progress.style.background = item.listColor;
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
const stats = listItem.createEl('div', { cls: 'immerse-list-stats' });
|
||||||
|
stats.createEl('span', {
|
||||||
|
text: `${item.taskCount} tasks`,
|
||||||
|
cls: 'immerse-list-stat-text'
|
||||||
|
});
|
||||||
|
stats.createEl('span', {
|
||||||
|
text: `${this.plugin.formatTimeHuman(item.minutes)}`,
|
||||||
|
cls: 'immerse-list-stat-text'
|
||||||
|
});
|
||||||
|
stats.createEl('span', {
|
||||||
|
text: `${item.percentage.toFixed(1)}%`,
|
||||||
|
cls: 'immerse-list-stat-percentage'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInsights(container: Element, data: ReportData) {
|
||||||
|
const section = container.createEl('div', { cls: 'immerse-report-section' });
|
||||||
|
section.createEl('h3', { text: 'Productivity Insights', cls: 'immerse-report-section-title' });
|
||||||
|
|
||||||
|
const insightsGrid = section.createEl('div', { cls: 'immerse-insights-grid' });
|
||||||
|
|
||||||
|
if (data.mostProductiveHour !== undefined) {
|
||||||
|
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
|
||||||
|
card.createEl('span', { text: '🕐', cls: 'immerse-insight-icon' });
|
||||||
|
card.createEl('span', { text: 'MOST PRODUCTIVE HOUR', cls: 'immerse-insight-label' });
|
||||||
|
card.createEl('span', {
|
||||||
|
text: `${data.mostProductiveHour}:00 - ${data.mostProductiveHour + 1}:00`,
|
||||||
|
cls: 'immerse-insight-value'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.mostProductiveDay) {
|
||||||
|
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
|
||||||
|
card.createEl('span', { text: '📅', cls: 'immerse-insight-icon' });
|
||||||
|
card.createEl('span', { text: 'MOST PRODUCTIVE DAY', cls: 'immerse-insight-label' });
|
||||||
|
card.createEl('span', { text: data.mostProductiveDay, cls: 'immerse-insight-value' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.mostProductiveMonth) {
|
||||||
|
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
|
||||||
|
card.createEl('span', { text: '🗓️', cls: 'immerse-insight-icon' });
|
||||||
|
card.createEl('span', { text: 'MOST PRODUCTIVE MONTH', cls: 'immerse-insight-label' });
|
||||||
|
card.createEl('span', { text: data.mostProductiveMonth, cls: 'immerse-insight-value' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPieChart(container: Element, data: ReportData) {
|
||||||
|
const pieContainer = container.createEl('div', { cls: 'immerse-daily-pie-container' });
|
||||||
|
|
||||||
|
// Calculate totals - normalize to minutes for fair comparison
|
||||||
|
const totalTasks = data.totalTasks;
|
||||||
|
const totalMinutes = data.totalMinutes;
|
||||||
|
const totalPomodoros = data.totalPomodoros;
|
||||||
|
|
||||||
|
// For pie chart, let's use minutes as the base unit
|
||||||
|
// Assume average task takes data.minsPerTask minutes
|
||||||
|
const taskMinutes = totalTasks * data.minsPerTask;
|
||||||
|
const pomodoroMinutes = totalPomodoros * 25; // Standard pomodoro is 25 minutes
|
||||||
|
|
||||||
|
const totalTime = taskMinutes + totalMinutes + pomodoroMinutes;
|
||||||
|
|
||||||
|
if (totalTime === 0) return;
|
||||||
|
|
||||||
|
// Calculate percentages based on time
|
||||||
|
const tasksPercent = (taskMinutes / totalTime) * 100;
|
||||||
|
const hoursPercent = (totalMinutes / totalTime) * 100;
|
||||||
|
const pomodorosPercent = (pomodoroMinutes / totalTime) * 100;
|
||||||
|
|
||||||
|
// Calculate degrees for conic gradient
|
||||||
|
const tasksDeg = (tasksPercent / 100) * 360;
|
||||||
|
const hoursDeg = tasksDeg + (hoursPercent / 100) * 360;
|
||||||
|
|
||||||
|
// Create pie chart with segments
|
||||||
|
const pieChart = pieContainer.createEl('div', { cls: 'immerse-daily-pie-chart' });
|
||||||
|
|
||||||
|
// Build conic-gradient string with explicit color stops
|
||||||
|
const gradient = `conic-gradient(from 0deg, #6366f1 0deg ${tasksDeg}deg, #22c55e ${tasksDeg}deg ${hoursDeg}deg, #f59e0b ${hoursDeg}deg 360deg)`;
|
||||||
|
pieChart.style.background = gradient;
|
||||||
|
|
||||||
|
// Center circle with total
|
||||||
|
const center = pieChart.createEl('div', { cls: 'immerse-daily-pie-center' });
|
||||||
|
center.createEl('div', { text: data.totalTasks.toString(), cls: 'immerse-daily-pie-center-value' });
|
||||||
|
center.createEl('div', { text: 'TOTAL TASKS', cls: 'immerse-daily-pie-center-label' });
|
||||||
|
|
||||||
|
// Legend
|
||||||
|
const legend = pieContainer.createEl('div', { cls: 'immerse-daily-pie-legend' });
|
||||||
|
|
||||||
|
// Tasks legend item
|
||||||
|
const tasksItem = legend.createEl('div', { cls: 'immerse-daily-pie-legend-item' });
|
||||||
|
const tasksColor = tasksItem.createEl('div', { cls: 'immerse-daily-pie-legend-color' });
|
||||||
|
tasksColor.style.background = '#6366f1';
|
||||||
|
const tasksInfo = tasksItem.createEl('div', { cls: 'immerse-daily-pie-legend-info' });
|
||||||
|
const tasksLabel = tasksInfo.createEl('div', { cls: 'immerse-daily-pie-legend-label' });
|
||||||
|
tasksLabel.createEl('span', { text: '✓' });
|
||||||
|
tasksLabel.appendText('Tasks Completed');
|
||||||
|
tasksInfo.createEl('div', { text: totalTasks.toString(), cls: 'immerse-daily-pie-legend-value' });
|
||||||
|
tasksInfo.createEl('div', { text: `${tasksPercent.toFixed(1)}%`, cls: 'immerse-daily-pie-legend-percentage' });
|
||||||
|
|
||||||
|
// Hours legend item
|
||||||
|
const hoursItem = legend.createEl('div', { cls: 'immerse-daily-pie-legend-item' });
|
||||||
|
const hoursColor = hoursItem.createEl('div', { cls: 'immerse-daily-pie-legend-color' });
|
||||||
|
hoursColor.style.background = '#22c55e';
|
||||||
|
const hoursInfo = hoursItem.createEl('div', { cls: 'immerse-daily-pie-legend-info' });
|
||||||
|
const hoursLabel = hoursInfo.createEl('div', { cls: 'immerse-daily-pie-legend-label' });
|
||||||
|
hoursLabel.createEl('span', { text: '⏱️' });
|
||||||
|
hoursLabel.appendText('Total Hours');
|
||||||
|
hoursInfo.createEl('div', { text: (totalMinutes / 60).toFixed(1), cls: 'immerse-daily-pie-legend-value' });
|
||||||
|
hoursInfo.createEl('div', { text: `${hoursPercent.toFixed(1)}%`, cls: 'immerse-daily-pie-legend-percentage' });
|
||||||
|
|
||||||
|
// Pomodoros legend item
|
||||||
|
const pomodorosItem = legend.createEl('div', { cls: 'immerse-daily-pie-legend-item' });
|
||||||
|
const pomodorosColor = pomodorosItem.createEl('div', { cls: 'immerse-daily-pie-legend-color' });
|
||||||
|
pomodorosColor.style.background = '#f59e0b';
|
||||||
|
const pomodorosInfo = pomodorosItem.createEl('div', { cls: 'immerse-daily-pie-legend-info' });
|
||||||
|
const pomodorosLabel = pomodorosInfo.createEl('div', { cls: 'immerse-daily-pie-legend-label' });
|
||||||
|
pomodorosLabel.createEl('span', { text: '🍅' });
|
||||||
|
pomodorosLabel.appendText('Pomodoros');
|
||||||
|
pomodorosInfo.createEl('div', { text: totalPomodoros.toString(), cls: 'immerse-daily-pie-legend-value' });
|
||||||
|
pomodorosInfo.createEl('div', { text: `${pomodorosPercent.toFixed(1)}%`, cls: 'immerse-daily-pie-legend-percentage' });
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDailyBreakdown(container: Element, data: ReportData) {
|
||||||
|
const section = container.createEl('div', { cls: 'immerse-report-section' });
|
||||||
|
section.createEl('h3', { text: 'Daily Breakdown', cls: 'immerse-report-section-title' });
|
||||||
|
|
||||||
|
// Add pie chart for overall summary
|
||||||
|
this.renderPieChart(section, data);
|
||||||
|
|
||||||
|
const breakdownContainer = section.createEl('div', { cls: 'immerse-daily-breakdown-container' });
|
||||||
|
|
||||||
|
// Show last 10 days max
|
||||||
|
const recentData = data.dailyBreakdown.slice(-10);
|
||||||
|
|
||||||
|
// Find max values for scaling bars
|
||||||
|
const maxTasks = Math.max(...recentData.map(d => d.tasks), 1);
|
||||||
|
const maxHours = Math.max(...recentData.map(d => d.hours), 1);
|
||||||
|
const maxPomodoros = Math.max(...recentData.map(d => d.pomodoros), 1);
|
||||||
|
|
||||||
|
recentData.forEach(day => {
|
||||||
|
const row = breakdownContainer.createEl('div', { cls: 'immerse-daily-row' });
|
||||||
|
|
||||||
|
// Date column with formatted date
|
||||||
|
const dateEl = row.createEl('div', { cls: 'immerse-daily-date' });
|
||||||
|
const date = new Date(day.date + 'T00:00:00');
|
||||||
|
const dayName = date.toLocaleDateString('en-US', { weekday: 'short' }).toUpperCase();
|
||||||
|
const monthDay = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||||
|
dateEl.createEl('div', { text: dayName, cls: 'immerse-daily-date-day' });
|
||||||
|
dateEl.createEl('div', { text: monthDay, cls: 'immerse-daily-date-num' });
|
||||||
|
|
||||||
|
// Bars column
|
||||||
|
const barsContainer = row.createEl('div', { cls: 'immerse-daily-bars' });
|
||||||
|
|
||||||
|
// Tasks bar
|
||||||
|
const tasksRow = barsContainer.createEl('div', { cls: 'immerse-daily-bar-row' });
|
||||||
|
const tasksLabel = tasksRow.createEl('span', { cls: 'immerse-daily-bar-label' });
|
||||||
|
tasksLabel.createEl('span', { text: '✓', cls: 'immerse-daily-bar-icon' });
|
||||||
|
tasksLabel.appendText('Tasks');
|
||||||
|
const tasksTrack = tasksRow.createEl('div', { cls: 'immerse-daily-bar-track' });
|
||||||
|
const tasksFill = tasksTrack.createEl('div', { cls: 'immerse-daily-bar-fill tasks' });
|
||||||
|
tasksFill.style.width = `${(day.tasks / maxTasks) * 100}%`;
|
||||||
|
tasksRow.createEl('span', { text: day.tasks.toString(), cls: 'immerse-daily-bar-value' });
|
||||||
|
|
||||||
|
// Hours bar
|
||||||
|
const hoursRow = barsContainer.createEl('div', { cls: 'immerse-daily-bar-row' });
|
||||||
|
const hoursLabel = hoursRow.createEl('span', { cls: 'immerse-daily-bar-label' });
|
||||||
|
hoursLabel.createEl('span', { text: '⏱️', cls: 'immerse-daily-bar-icon' });
|
||||||
|
hoursLabel.appendText('Hours');
|
||||||
|
const hoursTrack = hoursRow.createEl('div', { cls: 'immerse-daily-bar-track' });
|
||||||
|
const hoursFill = hoursTrack.createEl('div', { cls: 'immerse-daily-bar-fill hours' });
|
||||||
|
hoursFill.style.width = `${(day.hours / maxHours) * 100}%`;
|
||||||
|
hoursRow.createEl('span', { text: day.hours.toFixed(1), cls: 'immerse-daily-bar-value' });
|
||||||
|
|
||||||
|
// Pomodoros bar
|
||||||
|
const pomodorosRow = barsContainer.createEl('div', { cls: 'immerse-daily-bar-row' });
|
||||||
|
const pomodorosLabel = pomodorosRow.createEl('span', { cls: 'immerse-daily-bar-label' });
|
||||||
|
pomodorosLabel.createEl('span', { text: '🍅', cls: 'immerse-daily-bar-icon' });
|
||||||
|
pomodorosLabel.appendText('Pomodoros');
|
||||||
|
const pomodorosTrack = pomodorosRow.createEl('div', { cls: 'immerse-daily-bar-track' });
|
||||||
|
const pomodorosFill = pomodorosTrack.createEl('div', { cls: 'immerse-daily-bar-fill pomodoros' });
|
||||||
|
pomodorosFill.style.width = `${(day.pomodoros / maxPomodoros) * 100}%`;
|
||||||
|
pomodorosRow.createEl('span', { text: day.pomodoros.toString(), cls: 'immerse-daily-bar-value' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/types.ts
93
src/types.ts
@@ -1,6 +1,6 @@
|
|||||||
// ============ Types & Interfaces ============
|
// ============ Types & Interfaces ============
|
||||||
|
|
||||||
export interface FocusTask {
|
export interface ImmerseTask {
|
||||||
id: string;
|
id: string;
|
||||||
text: string;
|
text: string;
|
||||||
completed: boolean;
|
completed: boolean;
|
||||||
@@ -10,7 +10,9 @@ export interface FocusTask {
|
|||||||
completedAt?: number;
|
completedAt?: number;
|
||||||
list: string;
|
list: string;
|
||||||
notes: string;
|
notes: string;
|
||||||
scheduledDate?: string;
|
scheduledDate?: string; // Date in YYYY-MM-DD format
|
||||||
|
scheduledTime?: string; // Time in HH:mm format (24-hour)
|
||||||
|
reminderMinutes?: number; // Minutes before scheduled time to remind (0 = no reminder)
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,7 +23,7 @@ export interface TaskList {
|
|||||||
icon: string;
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FocusTaskSettings {
|
export interface ImmerseSettings {
|
||||||
pomodoroWorkMinutes: number;
|
pomodoroWorkMinutes: number;
|
||||||
pomodoroBreakMinutes: number;
|
pomodoroBreakMinutes: number;
|
||||||
longBreakMinutes: number;
|
longBreakMinutes: number;
|
||||||
@@ -34,18 +36,47 @@ export interface FocusTaskSettings {
|
|||||||
tickSoundEnabled: boolean;
|
tickSoundEnabled: boolean;
|
||||||
// Daily note logging
|
// Daily note logging
|
||||||
logToDaily: boolean;
|
logToDaily: boolean;
|
||||||
|
// Task reminders
|
||||||
|
enableReminders: boolean;
|
||||||
|
defaultReminderMinutes: number; // Default minutes before task to remind
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FocusTaskData {
|
// Daily statistics snapshot
|
||||||
tasks: FocusTask[];
|
export interface DailyStats {
|
||||||
|
date: string; // YYYY-MM-DD format
|
||||||
|
tasksCompleted: number;
|
||||||
|
totalMinutes: number;
|
||||||
|
pomodorosCompleted: number;
|
||||||
|
tasksByList: Record<string, number>; // listId -> count
|
||||||
|
minutesByList: Record<string, number>; // listId -> minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archived completed task (for historical reporting)
|
||||||
|
export interface CompletedTaskRecord {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
list: string;
|
||||||
|
estimatedMinutes: number;
|
||||||
|
actualMinutes: number;
|
||||||
|
createdAt: number;
|
||||||
|
completedAt: number;
|
||||||
|
scheduledDate?: string;
|
||||||
|
wasOverdue: boolean; // was it completed after scheduled time?
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImmerseData {
|
||||||
|
tasks: ImmerseTask[];
|
||||||
completedToday: number;
|
completedToday: number;
|
||||||
totalFocusMinutesToday: number;
|
totalFocusMinutesToday: number;
|
||||||
streak: number;
|
streak: number;
|
||||||
lastActiveDate: string;
|
lastActiveDate: string;
|
||||||
pomodorosCompleted: number;
|
pomodorosCompleted: number;
|
||||||
|
// Historical data for reporting
|
||||||
|
dailyStats: DailyStats[]; // Array of daily statistics
|
||||||
|
completedTasksArchive: CompletedTaskRecord[]; // All completed tasks history
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: FocusTaskSettings = {
|
export const DEFAULT_SETTINGS: ImmerseSettings = {
|
||||||
pomodoroWorkMinutes: 25,
|
pomodoroWorkMinutes: 25,
|
||||||
pomodoroBreakMinutes: 5,
|
pomodoroBreakMinutes: 5,
|
||||||
longBreakMinutes: 15,
|
longBreakMinutes: 15,
|
||||||
@@ -62,19 +93,67 @@ export const DEFAULT_SETTINGS: FocusTaskSettings = {
|
|||||||
tickSoundEnabled: false,
|
tickSoundEnabled: false,
|
||||||
// Daily note logging
|
// Daily note logging
|
||||||
logToDaily: false,
|
logToDaily: false,
|
||||||
|
// Task reminders
|
||||||
|
enableReminders: true,
|
||||||
|
defaultReminderMinutes: 15,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_DATA: FocusTaskData = {
|
export const DEFAULT_DATA: ImmerseData = {
|
||||||
tasks: [],
|
tasks: [],
|
||||||
completedToday: 0,
|
completedToday: 0,
|
||||||
totalFocusMinutesToday: 0,
|
totalFocusMinutesToday: 0,
|
||||||
streak: 0,
|
streak: 0,
|
||||||
lastActiveDate: '',
|
lastActiveDate: '',
|
||||||
pomodorosCompleted: 0,
|
pomodorosCompleted: 0,
|
||||||
|
dailyStats: [],
|
||||||
|
completedTasksArchive: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VIEW_TYPE_IMMERSE = 'immerse-view';
|
export const VIEW_TYPE_IMMERSE = 'immerse-view';
|
||||||
|
|
||||||
|
// ============ Reporting Types ============
|
||||||
|
|
||||||
|
export interface ReportFilters {
|
||||||
|
startDate: string; // YYYY-MM-DD
|
||||||
|
endDate: string; // YYYY-MM-DD
|
||||||
|
listIds?: string[]; // Filter by specific lists (undefined = all)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportData {
|
||||||
|
// Summary stats
|
||||||
|
totalTasks: number;
|
||||||
|
totalMinutes: number;
|
||||||
|
totalPomodoros: number;
|
||||||
|
tasksPerDay: number;
|
||||||
|
hoursPerDay: number;
|
||||||
|
minsPerTask: number;
|
||||||
|
currentStreak: number;
|
||||||
|
|
||||||
|
// Time breakdown by list
|
||||||
|
timeByList: Array<{
|
||||||
|
listId: string;
|
||||||
|
listName: string;
|
||||||
|
listIcon: string;
|
||||||
|
listColor: string;
|
||||||
|
minutes: number;
|
||||||
|
taskCount: number;
|
||||||
|
percentage: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// Daily breakdown for charts
|
||||||
|
dailyBreakdown: Array<{
|
||||||
|
date: string; // YYYY-MM-DD
|
||||||
|
tasks: number;
|
||||||
|
hours: number;
|
||||||
|
pomodoros: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// Productivity insights
|
||||||
|
mostProductiveHour?: number; // 0-23
|
||||||
|
mostProductiveDay?: string; // day name
|
||||||
|
mostProductiveMonth?: string; // month name
|
||||||
|
}
|
||||||
|
|
||||||
// ============ Celebration Messages ============
|
// ============ Celebration Messages ============
|
||||||
|
|
||||||
export const CELEBRATION_MESSAGES = [
|
export const CELEBRATION_MESSAGES = [
|
||||||
|
|||||||
35
src/view.ts
35
src/view.ts
@@ -3,7 +3,7 @@ import {
|
|||||||
WorkspaceLeaf,
|
WorkspaceLeaf,
|
||||||
} from 'obsidian';
|
} from 'obsidian';
|
||||||
|
|
||||||
import { VIEW_TYPE_IMMERSE, FocusTask } from './types';
|
import { VIEW_TYPE_IMMERSE, ImmerseTask } from './types';
|
||||||
import { QuickAddTaskModal, EditTaskModal } from './modals';
|
import { QuickAddTaskModal, EditTaskModal } from './modals';
|
||||||
import ImmersePlugin from './main';
|
import ImmersePlugin from './main';
|
||||||
|
|
||||||
@@ -107,6 +107,12 @@ export class ImmerseView extends ItemView {
|
|||||||
|
|
||||||
const actions = header.createEl('div', { cls: 'immerse-header-actions' });
|
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' });
|
const addBtn = actions.createEl('button', { cls: 'immerse-btn immerse-btn-primary' });
|
||||||
addBtn.innerHTML = '+ Add Task';
|
addBtn.innerHTML = '+ Add Task';
|
||||||
addBtn.addEventListener('click', () => {
|
addBtn.addEventListener('click', () => {
|
||||||
@@ -121,7 +127,7 @@ export class ImmerseView extends ItemView {
|
|||||||
const statItems = [
|
const statItems = [
|
||||||
{ label: 'Pending', value: stats.pendingCount.toString(), icon: '📋' },
|
{ label: 'Pending', value: stats.pendingCount.toString(), icon: '📋' },
|
||||||
{ label: 'Done Today', value: stats.completedToday.toString(), icon: '✅' },
|
{ label: 'Done Today', value: stats.completedToday.toString(), icon: '✅' },
|
||||||
{ label: 'Focus Time', value: this.plugin.formatTimeHuman(stats.totalFocusMinutesToday), icon: '⏱️' },
|
{ label: 'Today\'s Focus', value: this.plugin.formatTimeHuman(stats.totalFocusMinutesToday), icon: '⏱️' },
|
||||||
{ label: 'Streak', value: `${stats.streak} days`, icon: '🔥' },
|
{ label: 'Streak', value: `${stats.streak} days`, icon: '🔥' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -324,11 +330,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);
|
const list = this.plugin.settings.lists.find(l => l.id === task.list);
|
||||||
|
|
||||||
|
// Check if task is overdue
|
||||||
|
const isOverdue = !task.completed && task.scheduledDate && task.scheduledTime &&
|
||||||
|
new Date(`${task.scheduledDate}T${task.scheduledTime}`).getTime() < Date.now();
|
||||||
|
|
||||||
const taskEl = container.createEl('div', {
|
const taskEl = container.createEl('div', {
|
||||||
cls: `immerse-task-item ${task.completed ? 'completed' : ''} ${task.isActive ? 'active' : ''}`
|
cls: `immerse-task-item ${task.completed ? 'completed' : ''} ${task.isActive ? 'active' : ''} ${isOverdue ? 'overdue' : ''}`
|
||||||
});
|
});
|
||||||
|
|
||||||
// Checkbox
|
// Checkbox
|
||||||
@@ -372,6 +382,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
|
// Actions
|
||||||
const actions = taskEl.createEl('div', { cls: 'immerse-task-actions' });
|
const actions = taskEl.createEl('div', { cls: 'immerse-task-actions' });
|
||||||
|
|
||||||
|
|||||||
821
styles.css
821
styles.css
@@ -464,6 +464,33 @@
|
|||||||
color: var(--ft-warning);
|
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 */
|
/* Task Actions */
|
||||||
.immerse-task-actions {
|
.immerse-task-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -622,3 +649,797 @@
|
|||||||
margin-top: 8px;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user