7 Commits

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

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

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

5
.gitignore vendored
View File

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

View File

@@ -4,7 +4,7 @@ A powerful task management and focus timer plugin for [Obsidian](https://obsidia
![Immerse Banner](https://img.shields.io/badge/Obsidian-Plugin-7c3aed?style=for-the-badge&logo=obsidian&logoColor=white) ![Immerse Banner](https://img.shields.io/badge/Obsidian-Plugin-7c3aed?style=for-the-badge&logo=obsidian&logoColor=white)
![License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge) ![License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge)
![Version](https://img.shields.io/badge/Version-1.0.9-blue?style=for-the-badge) ![Version](https://img.shields.io/badge/Version-1.1.0-blue?style=for-the-badge)
## 🎯 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

555
main.js
View File

@@ -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 = {
@@ -57,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 = [
@@ -347,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", () => {
@@ -359,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) => {
@@ -600,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
@@ -636,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();
}); });
@@ -664,6 +948,11 @@ 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) { if (this.settings.enableReminders) {
@@ -723,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 {
@@ -770,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);
} }
@@ -791,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() {
@@ -806,7 +1287,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
const task = this.data.tasks.find((t) => t.id === taskId); const task = this.data.tasks.find((t) => t.id === taskId);
if (!task) if (!task)
return; return;
this.stopTimer(); this.stopTimer(true);
this.activeTaskId = taskId; this.activeTaskId = taskId;
task.isActive = true; task.isActive = true;
this.isBreakMode = false; this.isBreakMode = false;
@@ -835,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();
@@ -844,7 +1325,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
const task = this.data.tasks.find((t) => t.id === taskId); const task = this.data.tasks.find((t) => t.id === taskId);
if (!task) if (!task)
return; return;
this.stopTimer(); this.stopTimer(true);
this.activeTaskId = taskId; this.activeTaskId = taskId;
task.isActive = true; task.isActive = true;
this.isBreakMode = false; this.isBreakMode = false;
@@ -892,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 {
@@ -902,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();
@@ -914,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(() => {
@@ -960,12 +1441,12 @@ 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();
} }
stopTimer() { stopTimer(preserveActualTime = false) {
if (this.timerInterval) { if (this.timerInterval) {
window.clearInterval(this.timerInterval); window.clearInterval(this.timerInterval);
this.timerInterval = null; this.timerInterval = null;
@@ -974,9 +1455,11 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
const task = this.data.tasks.find((t) => t.id === this.activeTaskId); const task = this.data.tasks.find((t) => t.id === this.activeTaskId);
if (task) { if (task) {
task.isActive = false; task.isActive = false;
if (!preserveActualTime) {
task.actualMinutes = 0; task.actualMinutes = 0;
} }
} }
}
this.isTimerRunning = false; this.isTimerRunning = false;
this.activeTaskId = null; this.activeTaskId = null;
this.secondsWorkedOnCurrentTask = 0; this.secondsWorkedOnCurrentTask = 0;
@@ -991,7 +1474,7 @@ 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 ============
@@ -1058,7 +1541,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
} }
showReminder(task) { showReminder(task) {
const timeStr = task.scheduledTime; const timeStr = task.scheduledTime;
new import_obsidian3.Notice(`\u{1F514} Reminder: "${task.text}" is scheduled for ${timeStr}`, 8e3); new import_obsidian4.Notice(`\u{1F514} Reminder: "${task.text}" is scheduled for ${timeStr}`, 8e3);
if (this.settings.enableSounds) { if (this.settings.enableSounds) {
this.playAlertSound(); this.playAlertSound();
} }
@@ -1066,7 +1549,7 @@ var ImmersePlugin = class extends import_obsidian3.Plugin {
showOverdueNotice(task) { showOverdueNotice(task) {
const dateStr = task.scheduledDate; const dateStr = task.scheduledDate;
const timeStr = task.scheduledTime; const timeStr = task.scheduledTime;
new import_obsidian3.Notice(`\u26A0\uFE0F Overdue: "${task.text}" was scheduled for ${dateStr} ${timeStr}`, 1e4); new import_obsidian4.Notice(`\u26A0\uFE0F Overdue: "${task.text}" was scheduled for ${dateStr} ${timeStr}`, 1e4);
if (this.settings.enableSounds) { if (this.settings.enableSounds) {
this.playAlertSound(); this.playAlertSound();
} }
@@ -1083,7 +1566,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 {
@@ -1145,7 +1628,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() {
@@ -1183,14 +1666,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 {
@@ -1204,17 +1687,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;
} }
} }
@@ -1227,7 +1710,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) {
@@ -1293,7 +1776,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;
@@ -1303,41 +1786,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();
})); }));
@@ -1353,7 +1836,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) => {
@@ -1368,7 +1851,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",

View File

@@ -1,7 +1,7 @@
{ {
"id": "immerse", "id": "immerse",
"name": "Immerse", "name": "Immerse",
"version": "1.0.9", "version": "1.1.2",
"minAppVersion": "0.15.0", "minAppVersion": "0.15.0",
"description": "A Blitzit-inspired task management and focus timer plugin. Plan your day, track time with Pomodoro technique, and crush your tasks with satisfying checkoffs.", "description": "A Blitzit-inspired task management and focus timer plugin. Plan your day, track time with Pomodoro technique, and crush your tasks with satisfying checkoffs.",
"author": "Crib", "author": "Crib",

View File

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

View File

@@ -21,6 +21,7 @@ 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 ============
@@ -65,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();
@@ -107,6 +113,12 @@ 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));
@@ -191,6 +203,26 @@ 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'): ImmerseTask { createTask(text: string, estimatedMinutes: number = this.settings.defaultEstimateMinutes, list: string = 'work'): ImmerseTask {
@@ -246,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);
@@ -279,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
@@ -297,8 +544,8 @@ export default class ImmersePlugin extends Plugin {
const task = this.data.tasks.find(t => t.id === taskId); const task = this.data.tasks.find(t => t.id === taskId);
if (!task) return; if (!task) return;
// Stop any existing timer // Stop any existing timer, preserving actual time
this.stopTimer(); this.stopTimer(true);
// Set active task // Set active task
this.activeTaskId = taskId; this.activeTaskId = taskId;
@@ -359,7 +606,7 @@ export default class ImmersePlugin extends Plugin {
const task = this.data.tasks.find(t => t.id === taskId); const task = this.data.tasks.find(t => t.id === taskId);
if (!task) return; if (!task) return;
this.stopTimer(); this.stopTimer(true);
this.activeTaskId = taskId; this.activeTaskId = taskId;
task.isActive = true; task.isActive = true;
this.isBreakMode = false; this.isBreakMode = false;
@@ -549,7 +796,7 @@ export default class ImmersePlugin extends Plugin {
this.refreshView(); this.refreshView();
} }
stopTimer() { stopTimer(preserveActualTime: boolean = false) {
if (this.timerInterval) { if (this.timerInterval) {
window.clearInterval(this.timerInterval); window.clearInterval(this.timerInterval);
this.timerInterval = null; this.timerInterval = null;
@@ -559,11 +806,12 @@ export default class ImmersePlugin extends Plugin {
const task = this.data.tasks.find(t => t.id === this.activeTaskId); const task = this.data.tasks.find(t => t.id === this.activeTaskId);
if (task) { if (task) {
task.isActive = false; task.isActive = false;
// Reset actual time when manually stopping (not after a break) // Only reset actual time when manually stopping (not when resuming after break)
// This allows starting fresh next time if (!preserveActualTime) {
task.actualMinutes = 0; task.actualMinutes = 0;
} }
} }
}
this.isTimerRunning = false; this.isTimerRunning = false;
this.activeTaskId = null; this.activeTaskId = null;

View File

@@ -297,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
View File

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

View File

@@ -41,6 +41,29 @@ export interface ImmerseSettings {
defaultReminderMinutes: number; // Default minutes before task to remind defaultReminderMinutes: number; // Default minutes before task to remind
} }
// Daily statistics snapshot
export interface DailyStats {
date: string; // YYYY-MM-DD format
tasksCompleted: number;
totalMinutes: number;
pomodorosCompleted: number;
tasksByList: Record<string, number>; // listId -> count
minutesByList: Record<string, number>; // listId -> minutes
}
// Archived completed task (for historical reporting)
export interface CompletedTaskRecord {
id: string;
text: string;
list: string;
estimatedMinutes: number;
actualMinutes: number;
createdAt: number;
completedAt: number;
scheduledDate?: string;
wasOverdue: boolean; // was it completed after scheduled time?
}
export interface ImmerseData { export interface ImmerseData {
tasks: ImmerseTask[]; tasks: ImmerseTask[];
completedToday: number; completedToday: number;
@@ -48,6 +71,9 @@ export interface ImmerseData {
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: ImmerseSettings = { export const DEFAULT_SETTINGS: ImmerseSettings = {
@@ -79,10 +105,55 @@ export const DEFAULT_DATA: ImmerseData = {
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 = [

View File

@@ -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: '🔥' },
]; ];

View File

@@ -288,11 +288,16 @@
} }
.immerse-active-controls .immerse-btn { .immerse-active-controls .immerse-btn {
flex: 1; flex: 1 1 auto;
min-width: 80px; min-width: 100px;
max-width: 100%;
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
color: white; color: white;
border: none; border: none;
white-space: normal;
word-wrap: break-word;
line-height: 1.3;
padding: 8px 12px;
} }
.immerse-active-controls .immerse-btn:hover { .immerse-active-controls .immerse-btn:hover {
@@ -649,3 +654,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;
}
}