12 Commits

Author SHA1 Message Date
2fad5d88ab feat: Add task scheduling and reminders system
Add comprehensive scheduling functionality with reminder notifications:

- Add scheduledDate, scheduledTime, and reminderMinutes fields to ImmerseTask
- Add enableReminders and defaultReminderMinutes to plugin settings
- Implement reminder notification system with 30-second background checks
- Add overdue task detection with visual indicators
- Add native HTML5 date/time pickers in Quick Add and Edit modals
- Add reminder dropdown with 5/10/15/30/60 minute options
- Display scheduled date/time with blue badge (📅) in task list
- Show pulsing red "OVERDUE" badge (⚠️) for past-due tasks
- Add red left border highlight for overdue tasks
- Implement startup check for due/overdue tasks when plugin loads
- Show overdue notices when Obsidian opens if tasks are past due
- Play alert sounds for reminders (if sounds enabled)
- Track shown reminders to prevent duplicate notifications
2025-11-23 21:12:21 +01:00
683c4ddafe Complete internal code renaming from FocusTask to Immerse types
- Renamed FocusTask → ImmerseTask
- Renamed FocusTaskSettings → ImmerseSettings
- Renamed FocusTaskData → ImmerseData
- Updated CSS classes: focus-task-status-* → immerse-status-*
- Updated package release script zip filename
- Updated remaining README references
- Rebuilt main.js with new type names
2025-11-23 20:12:28 +01:00
331a2b41df Release v1.0.9: Rename plugin from Focus Task to Immerse 2025-11-23 19:56:06 +01:00
364935af66 Updated README.md 2025-11-23 16:02:14 +01:00
2f2346b4c8 Release v1.0.8: Timer Accuracy Fix 2025-11-23 15:49:08 +01:00
c8f5c69102 General Bugfixes 2025-11-23 15:33:51 +01:00
2800a7507e Release v1.0.7: General bugfixes 2025-11-23 15:06:12 +01:00
2f861c2fcb Release v1.0.6: Background Timer Fix 2025-11-23 13:15:45 +01:00
9abdd10ada Release v1.0.5: Break Timer & Time Tracking Improvements 2025-11-23 11:49:45 +01:00
e66d9b4d25 General Bug fixes on Timers/Breaks 2025-11-23 11:46:13 +01:00
285ac7a7c9 Updated README.md 2025-11-23 11:45:37 +01:00
9e35a2603c Updated README.md 2025-11-22 20:48:35 +01:00
12 changed files with 1203 additions and 447 deletions

8
.gitignore vendored
View File

@@ -1,5 +1,7 @@
# Build output # Build output
*.js.map *.js.map
release/
*.zip
# npm # npm
node_modules/ node_modules/
@@ -16,3 +18,9 @@ Thumbs.db
# Obsidian # Obsidian
data.json data.json
# Development/Documentation (not for distribution)
RELEASE-GUIDE.md
ROADMAP.md
deploy-test.bat
.claude/

View File

@@ -1,16 +1,16 @@
# ⚡ Focus Task # ⚡ Immerse
A powerful task management and focus timer plugin for [Obsidian](https://obsidian.md), heavily inspired by [Blitzit](https://www.blitzit.app/). A powerful task management and focus timer plugin for [Obsidian](https://obsidian.md), heavily inspired by [Blitzit](https://www.blitzit.app/).
![Focus Task 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.3-blue?style=for-the-badge) ![Version](https://img.shields.io/badge/Version-1.0.9-blue?style=for-the-badge)
## 🎯 Overview ## 🎯 Overview
Focus Task brings the power of time-boxed task management directly into your Obsidian vault. Plan your day, track time with the Pomodoro technique, and crush your tasks with satisfying checkoffs - all without leaving your notes. Immerse brings the power of time-boxed task management directly into your Obsidian vault. Plan your day, track time with the Pomodoro technique, and crush your tasks with satisfying checkoffs - all without leaving your notes.
### Why Focus Task? ### Why Immerse?
- **Stay in Flow**: No need to switch between apps - manage tasks where you take notes - **Stay in Flow**: No need to switch between apps - manage tasks where you take notes
- **Time Awareness**: Know exactly how long tasks take vs. your estimates - **Time Awareness**: Know exactly how long tasks take vs. your estimates
@@ -46,9 +46,10 @@ Focus Task brings the power of time-boxed task management directly into your Obs
- **Streak Counter**: Build momentum with consecutive productive days - **Streak Counter**: Build momentum with consecutive productive days
- **Time Comparison**: Compare estimated vs. actual time to improve planning - **Time Comparison**: Compare estimated vs. actual time to improve planning
- **Pomodoro Count**: Track total pomodoros completed - **Pomodoro Count**: Track total pomodoros completed
- **Daily Note Logging**: Automatically log completed tasks to your daily notes with timestamps and performance metrics
### 🎨 User Experience ### 🎨 User Experience
- **Status Bar Timer**: timer that stays visible while you work - **Status Bar Timer**: Timer that stays visible while you work
- **Celebration Messages**: Fun, randomized messages when you complete tasks - **Celebration Messages**: Fun, randomized messages when you complete tasks
- **Sound Notifications**: Audio alerts for timer completion and task completion - **Sound Notifications**: Audio alerts for timer completion and task completion
- **Keyboard Shortcuts**: Quick access to common actions - **Keyboard Shortcuts**: Quick access to common actions
@@ -59,20 +60,23 @@ Focus Task brings the power of time-boxed task management directly into your Obs
### From Obsidian Community Plugins (Coming Soon) ### From Obsidian Community Plugins (Coming Soon)
1. Open Obsidian Settings 1. Open Obsidian Settings
2. Go to Community Plugins 2. Go to Community Plugins
3. Search for "Focus Task" 3. Search for "Immerse"
4. Click Install, then Enable 4. Click Install, then Enable
### Manual Installation ### Manual Installation
1. Download the latest release from the [releases page](https://git.cribdev.com/crib/focus-task/releases) 1. Download the latest release from the [releases page](https://git.cribdev.com/crib/immerse/releases)
2. Extract the files to your vault's `.obsidian/plugins/focus-task/` folder 2. Extract the files to your vault's `.obsidian/plugins/immerse/` folder
3. Reload Obsidian 3. **⚠️ IMPORTANT**: When updating, do NOT replace or delete the existing `data.json` file - this contains all your tasks, settings, and progress!
4. Enable the plugin in Settings → Community Plugins 4. Reload Obsidian
5. Enable the plugin in Settings → Community Plugins
> **Note**: Only copy the three plugin files (`main.js`, `manifest.json`, `styles.css`) when updating. Your `data.json` file stores all your tasks and settings and should never be replaced.
### Building from Source ### Building from Source
```bash ```bash
# Clone the repository # Clone the repository
git clone https://git.cribdev.com/crib/focus-task.git git clone https://git.cribdev.com/crib/immerse.git
cd focus-task cd immerse
# Install dependencies # Install dependencies
npm install npm install
@@ -81,14 +85,14 @@ npm install
npm run build npm run build
# Copy to your vault # Copy to your vault
cp main.js manifest.json styles.css /path/to/your/vault/.obsidian/plugins/focus-task/ cp main.js manifest.json styles.css /path/to/your/vault/.obsidian/plugins/immerse/
``` ```
## 📖 Usage ## 📖 Usage
### Getting Started ### Getting Started
1. **Open Focus Task**: Click the ⚡ icon in the ribbon or use the command palette (`Ctrl/Cmd + P` → "Open Focus Task Panel") 1. **Open Immerse**: Click the ⚡ icon in the ribbon or use the command palette (`Ctrl/Cmd + P` → "Open Immerse Panel")
2. **Add a Task**: Click "+ Add Task" and fill in: 2. **Add a Task**: Click "+ Add Task" and fill in:
- Task description - Task description
@@ -103,7 +107,7 @@ cp main.js manifest.json styles.css /path/to/your/vault/.obsidian/plugins/focus-
| Action | Command | | Action | Command |
|--------|---------| |--------|---------|
| Open Panel | `Ctrl/Cmd + P` → "Open Focus Task Panel" | | Open Panel | `Ctrl/Cmd + P` → "Open Immerse Panel" |
| Quick Add Task | `Ctrl/Cmd + P` → "Quick Add Task" | | Quick Add Task | `Ctrl/Cmd + P` → "Quick Add Task" |
| Toggle Timer | `Ctrl/Cmd + P` → "Toggle Timer" | | Toggle Timer | `Ctrl/Cmd + P` → "Toggle Timer" |
| Complete Task | `Ctrl/Cmd + P` → "Complete Current Task" | | Complete Task | `Ctrl/Cmd + P` → "Complete Current Task" |
@@ -136,7 +140,7 @@ Best for: Tracking time on open-ended tasks
| Short Break | Length of short breaks | 5 min | | Short Break | Length of short breaks | 5 min |
| Long Break | Length of long breaks | 15 min | | Long Break | Length of long breaks | 15 min |
| Long Break Interval | Pomodoros before a long break | 4 | | Long Break Interval | Pomodoros before a long break | 4 |
| Auto-start Breaks | Automatically start break timer | Off | | Auto-start Breaks | Automatically start break timer | On |
### General ### General
| Setting | Description | Default | | Setting | Description | Default |
@@ -146,6 +150,27 @@ Best for: Tracking time on open-ended tasks
| Enable Celebrations | Show celebration messages | On | | Enable Celebrations | Show celebration messages | On |
| Show Floating Timer | Display draggable timer widget | On | | Show Floating Timer | Display draggable timer widget | On |
### Daily Note Integration
| Setting | Description | Default |
|---------|-------------|---------|
| Log to Daily Note | Automatically log completed tasks to your daily note | Off |
When enabled, completed tasks are automatically appended to your daily note with:
- Task name and list category
- Time spent vs. estimated time
- Completion timestamp
- Performance indicator (under/over/on target)
**Example entry:**
```
- [x] Write project proposal | 💼 Work | ⏱️ 45min / 30min (15min over estimate) | ✅ 14:30
```
**Requirements:** The core "Daily Notes" plugin must be enabled in Obsidian settings. Immerse respects your Daily Notes configuration (folder, date format, and template).
### Lists ### Lists
Customize your task lists with: Customize your task lists with:
- Custom names - Custom names
@@ -157,18 +182,18 @@ Default lists: Work 💼, Personal 🏠, Learning 📚
## 🎨 Customization ## 🎨 Customization
### Adding Custom Lists ### Adding Custom Lists
1. Go to Settings → Focus Task → Lists 1. Go to Settings → Immerse → Lists
2. Click "+ Add List" 2. Click "+ Add List"
3. Set the name, emoji, and color 3. Set the name, emoji, and color
4. Click Save 4. Click Save
### Theming ### Theming
Focus Task respects your Obsidian theme and adapts to both light and dark modes automatically. Immerse respects your Obsidian theme and adapts to both light and dark modes automatically.
## 📁 Project Structure ## 📁 Project Structure
``` ```
focus-task/ immerse/
├── src/ ├── src/
│ ├── main.ts # Main plugin class │ ├── main.ts # Main plugin class
│ ├── types.ts # TypeScript interfaces │ ├── types.ts # TypeScript interfaces
@@ -193,7 +218,7 @@ Blitzit's approach to productivity resonated with me:
- Satisfying task completion experience - Satisfying task completion experience
- Progress insights - Progress insights
I wanted to bring this experience directly into Obsidian, where I already manage my notes and knowledge. Focus Task is my attempt to capture the essence of what makes Blitzit great while leveraging Obsidian's powerful ecosystem. I wanted to bring this experience directly into Obsidian, where I already manage my notes and knowledge. Immerse is my attempt to capture the essence of what makes Blitzit great while leveraging Obsidian's powerful ecosystem.
**If you're looking for a dedicated productivity app, I highly recommend checking out [Blitzit](https://www.blitzit.app/)!** **If you're looking for a dedicated productivity app, I highly recommend checking out [Blitzit](https://www.blitzit.app/)!**
@@ -211,8 +236,8 @@ Contributions are welcome! Feel free to:
```bash ```bash
# Clone the repo # Clone the repo
git clone https://git.cribdev.com/crib/focus-task.git git clone https://git.cribdev.com/crib/immerse.git
cd focus-task cd immerse
# Install dependencies # Install dependencies
npm install npm install
@@ -221,7 +246,7 @@ npm install
npm run dev npm run dev
# Create symlink to your test vault # Create symlink to your test vault
ln -s $(pwd) /path/to/vault/.obsidian/plugins/focus-task ln -s $(pwd) /path/to/vault/.obsidian/plugins/immerse
``` ```
## 📜 License ## 📜 License
@@ -230,8 +255,8 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
## 🔗 Links ## 🔗 Links
- **Repository**: [https://git.cribdev.com/crib/focus-task](https://git.cribdev.com/crib/focus-task) - **Repository**: [https://git.cribdev.com/crib/immerse](https://git.cribdev.com/crib/immerse)
- **Issues**: [https://git.cribdev.com/crib/focus-task/issues](https://git.cribdev.com/crib/focus-task/issues) - **Issues**: [https://git.cribdev.com/crib/immerse/issues](https://git.cribdev.com/crib/immerse/issues)
- **Inspiration**: [Blitzit App](https://www.blitzit.app/) - **Inspiration**: [Blitzit App](https://www.blitzit.app/)
--- ---
@@ -240,4 +265,6 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
Made with ❤️ for the Obsidian community Made with ❤️ for the Obsidian community
<br> <br>
Inspired by <a href="https://www.blitzit.app/">Blitzit</a> ⚡ Inspired by <a href="https://www.blitzit.app/">Blitzit</a> ⚡
<br><br>
<em>✨ Vibe coded with assistance from <a href="https://claude.ai">Claude.ai</a></em>
</p> </p>

474
main.js
View File

@@ -24,7 +24,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
// src/main.ts // src/main.ts
var main_exports = {}; var main_exports = {};
__export(main_exports, { __export(main_exports, {
default: () => FocusTaskPlugin default: () => ImmersePlugin
}); });
module.exports = __toCommonJS(main_exports); module.exports = __toCommonJS(main_exports);
var import_obsidian3 = require("obsidian"); var import_obsidian3 = require("obsidian");
@@ -43,10 +43,13 @@ var DEFAULT_SETTINGS = {
{ id: "personal", name: "Personal", color: "#22c55e", icon: "\u{1F3E0}" }, { id: "personal", name: "Personal", color: "#22c55e", icon: "\u{1F3E0}" },
{ id: "learning", name: "Learning", color: "#f59e0b", icon: "\u{1F4DA}" } { id: "learning", name: "Learning", color: "#f59e0b", icon: "\u{1F4DA}" }
], ],
autoStartBreak: false, autoStartBreak: true,
tickSoundEnabled: false, tickSoundEnabled: false,
// Daily note logging // Daily note logging
logToDaily: false logToDaily: false,
// Task reminders
enableReminders: true,
defaultReminderMinutes: 15
}; };
var DEFAULT_DATA = { var DEFAULT_DATA = {
tasks: [], tasks: [],
@@ -56,7 +59,7 @@ var DEFAULT_DATA = {
lastActiveDate: "", lastActiveDate: "",
pomodorosCompleted: 0 pomodorosCompleted: 0
}; };
var VIEW_TYPE_FOCUS_TASK = "focus-task-view"; var VIEW_TYPE_IMMERSE = "immerse-view";
var CELEBRATION_MESSAGES = [ var CELEBRATION_MESSAGES = [
{ emoji: "\u{1F4A5}", message: "Crushed it!" }, { emoji: "\u{1F4A5}", message: "Crushed it!" },
{ emoji: "\u{1F525}", message: "On fire!" }, { emoji: "\u{1F525}", message: "On fire!" },
@@ -90,15 +93,19 @@ var QuickAddTaskModal = class extends import_obsidian.Modal {
super(app); super(app);
this.taskText = ""; this.taskText = "";
this.selectedList = "work"; this.selectedList = "work";
this.scheduledDate = "";
this.scheduledTime = "";
this.reminderMinutes = 0;
this.plugin = plugin; this.plugin = plugin;
this.estimatedMinutes = plugin.settings.defaultEstimateMinutes; this.estimatedMinutes = plugin.settings.defaultEstimateMinutes;
this.reminderMinutes = plugin.settings.defaultReminderMinutes;
if (plugin.settings.lists.length > 0) { if (plugin.settings.lists.length > 0) {
this.selectedList = plugin.settings.lists[0].id; this.selectedList = plugin.settings.lists[0].id;
} }
} }
onOpen() { onOpen() {
const { contentEl } = this; const { contentEl } = this;
contentEl.addClass("focus-task-modal"); contentEl.addClass("immerse-modal");
contentEl.createEl("h2", { text: "\u26A1 Add New Task" }); contentEl.createEl("h2", { text: "\u26A1 Add New Task" });
new import_obsidian.Setting(contentEl).setName("Task").addText((text) => { new import_obsidian.Setting(contentEl).setName("Task").addText((text) => {
text.setPlaceholder("What do you need to do?").onChange((value) => this.taskText = value); text.setPlaceholder("What do you need to do?").onChange((value) => this.taskText = value);
@@ -137,15 +144,44 @@ var QuickAddTaskModal = class extends import_obsidian.Modal {
dropdown.setValue(this.selectedList); dropdown.setValue(this.selectedList);
dropdown.onChange((value) => this.selectedList = value); dropdown.onChange((value) => this.selectedList = value);
}); });
const buttonContainer = contentEl.createEl("div", { cls: "focus-task-modal-buttons" }); new import_obsidian.Setting(contentEl).setName("\u{1F4C5} Scheduled Date").setDesc("Optional: When do you plan to work on this?").addText((text) => {
const cancelBtn = buttonContainer.createEl("button", { text: "Cancel", cls: "focus-task-btn" }); text.setPlaceholder("YYYY-MM-DD").setValue(this.scheduledDate).onChange((value) => this.scheduledDate = value);
text.inputEl.type = "date";
});
new import_obsidian.Setting(contentEl).setName("\u23F0 Scheduled Time").setDesc("Optional: What time?").addText((text) => {
text.setPlaceholder("HH:mm").setValue(this.scheduledTime).onChange((value) => this.scheduledTime = value);
text.inputEl.type = "time";
});
if (this.plugin.settings.enableReminders) {
new import_obsidian.Setting(contentEl).setName("\u{1F514} Reminder").setDesc("Remind me before the scheduled time").addDropdown((dropdown) => {
dropdown.addOption("0", "No reminder");
dropdown.addOption("5", "5 minutes before");
dropdown.addOption("10", "10 minutes before");
dropdown.addOption("15", "15 minutes before");
dropdown.addOption("30", "30 minutes before");
dropdown.addOption("60", "1 hour before");
dropdown.setValue(this.reminderMinutes.toString());
dropdown.onChange((value) => this.reminderMinutes = parseInt(value));
});
}
const buttonContainer = contentEl.createEl("div", { cls: "immerse-modal-buttons" });
const cancelBtn = buttonContainer.createEl("button", { text: "Cancel", cls: "immerse-btn" });
cancelBtn.addEventListener("click", () => this.close()); cancelBtn.addEventListener("click", () => this.close());
const addBtn = buttonContainer.createEl("button", { text: "Add Task", cls: "focus-task-btn focus-task-btn-primary" }); const addBtn = buttonContainer.createEl("button", { text: "Add Task", cls: "immerse-btn immerse-btn-primary" });
addBtn.addEventListener("click", () => this.submitTask()); addBtn.addEventListener("click", () => this.submitTask());
} }
submitTask() { submitTask() {
if (this.taskText.trim()) { if (this.taskText.trim()) {
const task = this.plugin.createTask(this.taskText, this.estimatedMinutes, this.selectedList); const task = this.plugin.createTask(this.taskText, this.estimatedMinutes, this.selectedList);
if (this.scheduledDate) {
task.scheduledDate = this.scheduledDate;
}
if (this.scheduledTime) {
task.scheduledTime = this.scheduledTime;
}
if (this.reminderMinutes > 0 && this.scheduledDate && this.scheduledTime) {
task.reminderMinutes = this.reminderMinutes;
}
this.plugin.addTask(task); this.plugin.addTask(task);
new import_obsidian.Notice("\u2705 Task added!"); new import_obsidian.Notice("\u2705 Task added!");
this.close(); this.close();
@@ -166,7 +202,7 @@ var EditTaskModal = class extends import_obsidian.Modal {
} }
onOpen() { onOpen() {
const { contentEl } = this; const { contentEl } = this;
contentEl.addClass("focus-task-modal"); contentEl.addClass("immerse-modal");
contentEl.createEl("h2", { text: "\u270F\uFE0F Edit Task" }); contentEl.createEl("h2", { text: "\u270F\uFE0F Edit Task" });
new import_obsidian.Setting(contentEl).setName("Task").addText((text) => text.setValue(this.task.text).onChange((value) => this.task.text = value)); new import_obsidian.Setting(contentEl).setName("Task").addText((text) => text.setValue(this.task.text).onChange((value) => this.task.text = value));
new import_obsidian.Setting(contentEl).setName("Estimated Time").addDropdown((dropdown) => { new import_obsidian.Setting(contentEl).setName("Estimated Time").addDropdown((dropdown) => {
@@ -201,13 +237,36 @@ var EditTaskModal = class extends import_obsidian.Modal {
textarea.setValue(this.task.notes).onChange((value) => this.task.notes = value); textarea.setValue(this.task.notes).onChange((value) => this.task.notes = value);
textarea.inputEl.rows = 4; textarea.inputEl.rows = 4;
}); });
new import_obsidian.Setting(contentEl).setName("\u{1F4C5} Scheduled Date").setDesc("Optional: When do you plan to work on this?").addText((text) => {
text.setPlaceholder("YYYY-MM-DD").setValue(this.task.scheduledDate || "").onChange((value) => this.task.scheduledDate = value || void 0);
text.inputEl.type = "date";
});
new import_obsidian.Setting(contentEl).setName("\u23F0 Scheduled Time").setDesc("Optional: What time?").addText((text) => {
text.setPlaceholder("HH:mm").setValue(this.task.scheduledTime || "").onChange((value) => this.task.scheduledTime = value || void 0);
text.inputEl.type = "time";
});
if (this.plugin.settings.enableReminders) {
new import_obsidian.Setting(contentEl).setName("\u{1F514} Reminder").setDesc("Remind me before the scheduled time").addDropdown((dropdown) => {
dropdown.addOption("0", "No reminder");
dropdown.addOption("5", "5 minutes before");
dropdown.addOption("10", "10 minutes before");
dropdown.addOption("15", "15 minutes before");
dropdown.addOption("30", "30 minutes before");
dropdown.addOption("60", "1 hour before");
dropdown.setValue((this.task.reminderMinutes || 0).toString());
dropdown.onChange((value) => {
const minutes = parseInt(value);
this.task.reminderMinutes = minutes > 0 ? minutes : void 0;
});
});
}
if (this.task.actualMinutes > 0) { if (this.task.actualMinutes > 0) {
new import_obsidian.Setting(contentEl).setName("Time Tracked").setDesc(`You've worked on this task for ${this.plugin.formatTimeHuman(this.task.actualMinutes)}`); new import_obsidian.Setting(contentEl).setName("Time Tracked").setDesc(`You've worked on this task for ${this.plugin.formatTimeHuman(this.task.actualMinutes)}`);
} }
const buttonContainer = contentEl.createEl("div", { cls: "focus-task-modal-buttons" }); const buttonContainer = contentEl.createEl("div", { cls: "immerse-modal-buttons" });
const cancelBtn = buttonContainer.createEl("button", { text: "Cancel", cls: "focus-task-btn" }); const cancelBtn = buttonContainer.createEl("button", { text: "Cancel", cls: "immerse-btn" });
cancelBtn.addEventListener("click", () => this.close()); cancelBtn.addEventListener("click", () => this.close());
const saveBtn = buttonContainer.createEl("button", { text: "Save", cls: "focus-task-btn focus-task-btn-primary" }); const saveBtn = buttonContainer.createEl("button", { text: "Save", cls: "immerse-btn immerse-btn-primary" });
saveBtn.addEventListener("click", () => { saveBtn.addEventListener("click", () => {
this.plugin.updateTask(this.task.id, this.task); this.plugin.updateTask(this.task.id, this.task);
new import_obsidian.Notice("\u2705 Task updated!"); new import_obsidian.Notice("\u2705 Task updated!");
@@ -221,7 +280,7 @@ var EditTaskModal = class extends import_obsidian.Modal {
}; };
// src/view.ts // src/view.ts
var FocusTaskView = class extends import_obsidian2.ItemView { var ImmerseView = class extends import_obsidian2.ItemView {
constructor(leaf, plugin) { constructor(leaf, plugin) {
super(leaf); super(leaf);
this.currentFilter = "all"; this.currentFilter = "all";
@@ -233,10 +292,10 @@ var FocusTaskView = class extends import_obsidian2.ItemView {
this.plugin = plugin; this.plugin = plugin;
} }
getViewType() { getViewType() {
return VIEW_TYPE_FOCUS_TASK; return VIEW_TYPE_IMMERSE;
} }
getDisplayText() { getDisplayText() {
return "Focus Task"; return "Immerse";
} }
getIcon() { getIcon() {
return "zap"; return "zap";
@@ -270,7 +329,7 @@ var FocusTaskView = class extends import_obsidian2.ItemView {
refresh() { refresh() {
const container = this.containerEl.children[1]; const container = this.containerEl.children[1];
container.empty(); container.empty();
container.addClass("focus-task-container"); container.addClass("immerse-container");
this.timerTimeEl = null; this.timerTimeEl = null;
this.progressBarEl = null; this.progressBarEl = null;
this.actualTimeEl = null; this.actualTimeEl = null;
@@ -281,14 +340,14 @@ var FocusTaskView = class extends import_obsidian2.ItemView {
this.renderTaskList(container); this.renderTaskList(container);
} }
renderHeader(container) { renderHeader(container) {
const header = container.createEl("div", { cls: "focus-task-header" }); const header = container.createEl("div", { cls: "immerse-header" });
const titleSection = header.createEl("div", { cls: "focus-task-title-section" }); const titleSection = header.createEl("div", { cls: "immerse-title-section" });
titleSection.createEl("h2", { text: "\u26A1 Focus Task", cls: "focus-task-title" }); titleSection.createEl("h2", { text: "\u26A1 Immerse", cls: "immerse-title" });
const today = new Date(); const today = new Date();
const dateStr = today.toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric" }); const dateStr = today.toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric" });
titleSection.createEl("div", { text: dateStr, cls: "focus-task-date" }); titleSection.createEl("div", { text: dateStr, cls: "immerse-date" });
const actions = header.createEl("div", { cls: "focus-task-header-actions" }); const actions = header.createEl("div", { cls: "immerse-header-actions" });
const addBtn = actions.createEl("button", { cls: "focus-task-btn focus-task-btn-primary" }); const addBtn = actions.createEl("button", { cls: "immerse-btn immerse-btn-primary" });
addBtn.innerHTML = "+ Add Task"; addBtn.innerHTML = "+ Add Task";
addBtn.addEventListener("click", () => { addBtn.addEventListener("click", () => {
new QuickAddTaskModal(this.app, this.plugin).open(); new QuickAddTaskModal(this.app, this.plugin).open();
@@ -296,7 +355,7 @@ var FocusTaskView = class extends import_obsidian2.ItemView {
} }
renderStatsBar(container) { renderStatsBar(container) {
const stats = this.plugin.getStats(); const stats = this.plugin.getStats();
const statsBar = container.createEl("div", { cls: "focus-task-stats-bar" }); const statsBar = container.createEl("div", { cls: "immerse-stats-bar" });
const statItems = [ const statItems = [
{ label: "Pending", value: stats.pendingCount.toString(), icon: "\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" },
@@ -304,33 +363,35 @@ var FocusTaskView = class extends import_obsidian2.ItemView {
{ label: "Streak", value: `${stats.streak} days`, icon: "\u{1F525}" } { label: "Streak", value: `${stats.streak} days`, icon: "\u{1F525}" }
]; ];
statItems.forEach((stat) => { statItems.forEach((stat) => {
const item = statsBar.createEl("div", { cls: "focus-task-stat-item" }); const item = statsBar.createEl("div", { cls: "immerse-stat-item" });
item.createEl("div", { cls: "focus-task-stat-icon", text: stat.icon }); item.createEl("div", { cls: "immerse-stat-icon", text: stat.icon });
item.createEl("div", { cls: "focus-task-stat-value", text: stat.value }); item.createEl("div", { cls: "immerse-stat-value", text: stat.value });
item.createEl("div", { cls: "focus-task-stat-label", text: stat.label }); item.createEl("div", { cls: "immerse-stat-label", text: stat.label });
}); });
} }
renderActiveTask(container) { renderActiveTask(container) {
const activeSection = container.createEl("div", { cls: "focus-task-active-section" }); const activeSection = container.createEl("div", { cls: "immerse-active-section" });
if (this.plugin.activeTaskId) { if (this.plugin.activeTaskId) {
const task = this.plugin.data.tasks.find((t) => t.id === this.plugin.activeTaskId); const task = this.plugin.data.tasks.find((t) => t.id === this.plugin.activeTaskId);
if (task) { if (task) {
activeSection.addClass("focus-task-has-active"); activeSection.addClass("immerse-has-active");
const activeCard = activeSection.createEl("div", { cls: "focus-task-active-card" }); const activeCard = activeSection.createEl("div", { cls: "immerse-active-card" });
if (this.plugin.isBreakMode) { if (this.plugin.isBreakMode) {
activeCard.addClass("focus-task-break-card"); activeCard.addClass("immerse-break-card");
activeCard.createEl("div", { cls: "focus-task-active-label", text: "\u2615 BREAK TIME" }); const breakLabel = this.plugin.currentTimerSeconds > 0 ? "\u2615 BREAK TIME" : "\u2728 BREAK COMPLETE";
activeCard.createEl("div", { cls: "immerse-active-label", text: breakLabel });
} else { } else {
activeCard.createEl("div", { cls: "focus-task-active-label", text: "\u{1F3AF} FOCUSING ON" }); const workLabel = this.plugin.currentTimerSeconds > 0 ? "\u{1F3AF} FOCUSING ON" : "\u{1F345} POMODORO COMPLETE";
activeCard.createEl("div", { cls: "immerse-active-label", text: workLabel });
} }
activeCard.createEl("div", { cls: "focus-task-active-task-name", text: task.text }); activeCard.createEl("div", { cls: "immerse-active-task-name", text: task.text });
const timerDisplay = activeCard.createEl("div", { cls: "focus-task-timer-display" }); const timerDisplay = activeCard.createEl("div", { cls: "immerse-timer-display" });
this.timerTimeEl = timerDisplay.createEl("span", { this.timerTimeEl = timerDisplay.createEl("span", {
cls: "focus-task-timer-time", cls: "immerse-timer-time",
text: this.plugin.formatTime(this.plugin.currentTimerSeconds) text: this.plugin.formatTime(this.plugin.currentTimerSeconds)
}); });
const progressWrap = activeCard.createEl("div", { cls: "focus-task-progress-wrap" }); const progressWrap = activeCard.createEl("div", { cls: "immerse-progress-wrap" });
this.progressBarEl = progressWrap.createEl("div", { cls: "focus-task-progress-bar" }); this.progressBarEl = progressWrap.createEl("div", { cls: "immerse-progress-bar" });
let progressPercent = 0; let progressPercent = 0;
if (this.plugin.isBreakMode) { if (this.plugin.isBreakMode) {
const breakDuration = this.plugin.pomodoroCount % this.plugin.settings.longBreakInterval === 0 ? this.plugin.settings.longBreakMinutes * 60 : this.plugin.settings.pomodoroBreakMinutes * 60; const breakDuration = this.plugin.pomodoroCount % this.plugin.settings.longBreakInterval === 0 ? this.plugin.settings.longBreakMinutes * 60 : this.plugin.settings.pomodoroBreakMinutes * 60;
@@ -341,44 +402,81 @@ var FocusTaskView = class extends import_obsidian2.ItemView {
} }
this.progressBarEl.style.width = `${Math.min(Math.max(progressPercent, 0), 100)}%`; this.progressBarEl.style.width = `${Math.min(Math.max(progressPercent, 0), 100)}%`;
if (progressPercent >= 100) if (progressPercent >= 100)
this.progressBarEl.addClass("focus-task-overtime"); this.progressBarEl.addClass("immerse-overtime");
if (!this.plugin.isBreakMode) { if (!this.plugin.isBreakMode) {
const timeInfo = activeCard.createEl("div", { cls: "focus-task-time-info" }); const timeInfo = activeCard.createEl("div", { cls: "immerse-time-info" });
timeInfo.createEl("span", { text: `Est: ${this.plugin.formatTimeHuman(task.estimatedMinutes)}` }); timeInfo.createEl("span", { text: `Est: ${this.plugin.formatTimeHuman(task.estimatedMinutes)}` });
this.actualTimeEl = timeInfo.createEl("span", { text: `Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}` }); this.actualTimeEl = timeInfo.createEl("span", { text: `Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}` });
} }
const controls = activeCard.createEl("div", { cls: "focus-task-active-controls" }); const controls = activeCard.createEl("div", { cls: "immerse-active-controls" });
this.pauseBtnEl = controls.createEl("button", { cls: "focus-task-btn focus-task-btn-secondary" }); if (this.plugin.isBreakMode) {
if (this.plugin.currentTimerSeconds > 0) {
this.pauseBtnEl = controls.createEl("button", { cls: "immerse-btn immerse-btn-secondary" });
this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? "\u23F8 Pause" : "\u25B6 Resume"; this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? "\u23F8 Pause" : "\u25B6 Resume";
this.pauseBtnEl.addEventListener("click", () => this.plugin.toggleTimer()); this.pauseBtnEl.addEventListener("click", () => this.plugin.toggleTimer());
if (!this.plugin.isBreakMode) { const skipBreakBtn = controls.createEl("button", { cls: "immerse-btn immerse-btn-primary" });
const completeBtn = controls.createEl("button", { cls: "focus-task-btn focus-task-btn-success" });
completeBtn.innerHTML = "\u2713 Complete";
completeBtn.addEventListener("click", () => this.plugin.completeTask(task.id));
}
const stopBtn = controls.createEl("button", { cls: "focus-task-btn focus-task-btn-danger" });
stopBtn.innerHTML = "\u2715 Stop";
stopBtn.addEventListener("click", () => this.plugin.stopTimer());
if (this.plugin.isBreakMode) {
const skipBreakBtn = controls.createEl("button", { cls: "focus-task-btn" });
skipBreakBtn.innerHTML = "\u23ED Skip Break"; skipBreakBtn.innerHTML = "\u23ED Skip Break";
skipBreakBtn.addEventListener("click", () => { skipBreakBtn.addEventListener("click", () => {
this.plugin.isBreakMode = false; this.plugin.isBreakMode = false;
this.plugin.stopTimer(); this.plugin.startPomodoro(task.id);
this.refresh();
}); });
const stopBtn = controls.createEl("button", { cls: "immerse-btn immerse-btn-danger" });
stopBtn.innerHTML = "\u2715 Stop";
stopBtn.addEventListener("click", () => {
this.plugin.isBreakMode = false;
this.plugin.stopTimer();
});
} else {
const resumeWorkBtn = controls.createEl("button", { cls: "immerse-btn immerse-btn-success" });
resumeWorkBtn.innerHTML = "\u25B6 Resume Work";
resumeWorkBtn.addEventListener("click", () => {
this.plugin.isBreakMode = false;
this.plugin.startPomodoro(task.id);
});
const stopBtn = controls.createEl("button", { cls: "immerse-btn immerse-btn-danger" });
stopBtn.innerHTML = "\u2715 Stop";
stopBtn.addEventListener("click", () => {
this.plugin.isBreakMode = false;
this.plugin.stopTimer();
});
}
} else {
if (this.plugin.currentTimerSeconds > 0) {
this.pauseBtnEl = controls.createEl("button", { cls: "immerse-btn immerse-btn-secondary" });
this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? "\u23F8 Pause" : "\u25B6 Resume";
this.pauseBtnEl.addEventListener("click", () => this.plugin.toggleTimer());
const completeBtn = controls.createEl("button", { cls: "immerse-btn immerse-btn-success" });
completeBtn.innerHTML = "\u2713 Complete";
completeBtn.addEventListener("click", () => this.plugin.completeTask(task.id));
const stopBtn = controls.createEl("button", { cls: "immerse-btn immerse-btn-danger" });
stopBtn.innerHTML = "\u2715 Stop";
stopBtn.addEventListener("click", () => this.plugin.stopTimer());
} else {
const startBreakBtn = controls.createEl("button", { cls: "immerse-btn immerse-btn-secondary" });
startBreakBtn.innerHTML = "\u2615 Start Break";
startBreakBtn.addEventListener("click", () => this.plugin.startBreak());
const continueBtn = controls.createEl("button", { cls: "immerse-btn immerse-btn-primary" });
continueBtn.innerHTML = "\u25B6 Continue Working";
continueBtn.addEventListener("click", () => this.plugin.startPomodoro(task.id));
const completeBtn = controls.createEl("button", { cls: "immerse-btn immerse-btn-success" });
completeBtn.innerHTML = "\u2713 Complete";
completeBtn.addEventListener("click", () => this.plugin.completeTask(task.id));
const stopBtn = controls.createEl("button", { cls: "immerse-btn immerse-btn-danger" });
stopBtn.innerHTML = "\u2715 Stop";
stopBtn.addEventListener("click", () => this.plugin.stopTimer());
}
} }
} }
} else { } else {
const startPrompt = activeSection.createEl("div", { cls: "focus-task-start-prompt" }); const startPrompt = activeSection.createEl("div", { cls: "immerse-start-prompt" });
startPrompt.createEl("div", { cls: "focus-task-prompt-icon", text: "\u26A1" }); startPrompt.createEl("div", { cls: "immerse-prompt-icon", text: "\u26A1" });
startPrompt.createEl("div", { cls: "focus-task-prompt-text", text: "Ready to focus?" }); startPrompt.createEl("div", { cls: "immerse-prompt-text", text: "Ready to focus?" });
startPrompt.createEl("div", { cls: "focus-task-prompt-hint", text: "Click \u25B6 on a task to start a Pomodoro session" }); startPrompt.createEl("div", { cls: "immerse-prompt-hint", text: "Click \u25B6 on a task to start a Pomodoro session" });
} }
} }
renderTaskList(container) { renderTaskList(container) {
const listSection = container.createEl("div", { cls: "focus-task-list-section" }); const listSection = container.createEl("div", { cls: "immerse-list-section" });
const filters = listSection.createEl("div", { cls: "focus-task-filters" }); const filters = listSection.createEl("div", { cls: "immerse-filters" });
const filterOptions = [ const filterOptions = [
{ id: "all", label: "All Tasks" }, { id: "all", label: "All Tasks" },
{ id: "today", label: "Today" }, { id: "today", label: "Today" },
@@ -387,7 +485,7 @@ var FocusTaskView = class extends import_obsidian2.ItemView {
]; ];
filterOptions.forEach((opt) => { filterOptions.forEach((opt) => {
const btn = filters.createEl("button", { const btn = filters.createEl("button", {
cls: `focus-task-filter-btn ${this.currentFilter === opt.id ? "active" : ""}`, cls: `immerse-filter-btn ${this.currentFilter === opt.id ? "active" : ""}`,
text: opt.label text: opt.label
}); });
btn.addEventListener("click", () => { btn.addEventListener("click", () => {
@@ -395,7 +493,7 @@ var FocusTaskView = class extends import_obsidian2.ItemView {
this.refresh(); this.refresh();
}); });
}); });
const taskList = listSection.createEl("div", { cls: "focus-task-task-list" }); const taskList = listSection.createEl("div", { cls: "immerse-task-list" });
let tasks = this.plugin.data.tasks; let tasks = this.plugin.data.tasks;
if (this.currentFilter === "today") { if (this.currentFilter === "today") {
tasks = this.plugin.getTodaysTasks(); tasks = this.plugin.getTodaysTasks();
@@ -410,20 +508,21 @@ var FocusTaskView = class extends import_obsidian2.ItemView {
return b.createdAt - a.createdAt; return b.createdAt - a.createdAt;
}); });
if (tasks.length === 0) { if (tasks.length === 0) {
const emptyState = taskList.createEl("div", { cls: "focus-task-empty-state" }); const emptyState = taskList.createEl("div", { cls: "immerse-empty-state" });
emptyState.createEl("div", { cls: "focus-task-empty-icon", text: "\u{1F4DD}" }); emptyState.createEl("div", { cls: "immerse-empty-icon", text: "\u{1F4DD}" });
emptyState.createEl("div", { cls: "focus-task-empty-text", text: "No tasks yet" }); emptyState.createEl("div", { cls: "immerse-empty-text", text: "No tasks yet" });
emptyState.createEl("div", { cls: "focus-task-empty-hint", text: "Add a task to get started!" }); emptyState.createEl("div", { cls: "immerse-empty-hint", text: "Add a task to get started!" });
} else { } else {
tasks.forEach((task) => this.renderTaskItem(taskList, task)); tasks.forEach((task) => this.renderTaskItem(taskList, task));
} }
} }
renderTaskItem(container, task) { renderTaskItem(container, task) {
const list = this.plugin.settings.lists.find((l) => l.id === task.list); const list = this.plugin.settings.lists.find((l) => l.id === task.list);
const isOverdue = !task.completed && task.scheduledDate && task.scheduledTime && new Date(`${task.scheduledDate}T${task.scheduledTime}`).getTime() < Date.now();
const taskEl = container.createEl("div", { const taskEl = container.createEl("div", {
cls: `focus-task-task-item ${task.completed ? "completed" : ""} ${task.isActive ? "active" : ""}` cls: `immerse-task-item ${task.completed ? "completed" : ""} ${task.isActive ? "active" : ""} ${isOverdue ? "overdue" : ""}`
}); });
const checkbox = taskEl.createEl("div", { cls: "focus-task-checkbox" }); const checkbox = taskEl.createEl("div", { cls: "immerse-checkbox" });
checkbox.innerHTML = task.completed ? "\u2713" : ""; checkbox.innerHTML = task.completed ? "\u2713" : "";
checkbox.style.borderColor = (list == null ? void 0 : list.color) || "#6366f1"; checkbox.style.borderColor = (list == null ? void 0 : list.color) || "#6366f1";
if (task.completed) { if (task.completed) {
@@ -436,48 +535,63 @@ var FocusTaskView = class extends import_obsidian2.ItemView {
this.plugin.completeTask(task.id); this.plugin.completeTask(task.id);
} }
}); });
const content = taskEl.createEl("div", { cls: "focus-task-task-content" }); const content = taskEl.createEl("div", { cls: "immerse-task-content" });
const taskHeader = content.createEl("div", { cls: "focus-task-task-header" }); const taskHeader = content.createEl("div", { cls: "immerse-task-header" });
taskHeader.createEl("span", { cls: "focus-task-task-text", text: task.text }); taskHeader.createEl("span", { cls: "immerse-task-text", text: task.text });
if (list) { if (list) {
const listBadge = taskHeader.createEl("span", { const listBadge = taskHeader.createEl("span", {
cls: "focus-task-list-badge", cls: "immerse-list-badge",
text: `${list.icon} ${list.name}` text: `${list.icon} ${list.name}`
}); });
listBadge.style.backgroundColor = list.color + "20"; listBadge.style.backgroundColor = list.color + "20";
listBadge.style.color = list.color; listBadge.style.color = list.color;
} }
const taskMeta = content.createEl("div", { cls: "focus-task-task-meta" }); const taskMeta = content.createEl("div", { cls: "immerse-task-meta" });
taskMeta.createEl("span", { text: `Est: ${this.plugin.formatTimeHuman(task.estimatedMinutes)}` }); taskMeta.createEl("span", { text: `Est: ${this.plugin.formatTimeHuman(task.estimatedMinutes)}` });
if (task.actualMinutes > 0) { if (task.actualMinutes > 0) {
const actualSpan = taskMeta.createEl("span"); const actualSpan = taskMeta.createEl("span");
actualSpan.setText(`Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}`); actualSpan.setText(`Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}`);
if (task.actualMinutes > task.estimatedMinutes) { if (task.actualMinutes > task.estimatedMinutes) {
actualSpan.addClass("focus-task-overtime-text"); actualSpan.addClass("immerse-overtime-text");
} }
} }
const actions = taskEl.createEl("div", { cls: "focus-task-task-actions" }); if (task.scheduledDate) {
const scheduleSpan = taskMeta.createEl("span", {
cls: `immerse-schedule-badge ${isOverdue ? "overdue" : ""}`
});
const dateStr = task.scheduledDate;
const timeStr = task.scheduledTime || "";
if (isOverdue) {
scheduleSpan.setText(`\u26A0\uFE0F OVERDUE: ${dateStr}${timeStr ? " " + timeStr : ""}`);
} else {
scheduleSpan.setText(`\u{1F4C5} ${dateStr}${timeStr ? " " + timeStr : ""}`);
}
if (task.reminderMinutes) {
scheduleSpan.title = `Reminder set for ${task.reminderMinutes} min before`;
}
}
const actions = taskEl.createEl("div", { cls: "immerse-task-actions" });
if (!task.completed) { if (!task.completed) {
const startBtn = actions.createEl("button", { cls: "focus-task-task-btn", attr: { "aria-label": "Start Pomodoro" } }); const startBtn = actions.createEl("button", { cls: "immerse-task-btn", attr: { "aria-label": "Start Pomodoro" } });
startBtn.innerHTML = "\u25B6"; startBtn.innerHTML = "\u25B6";
startBtn.addEventListener("click", (e) => { startBtn.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
this.plugin.startPomodoro(task.id); this.plugin.startPomodoro(task.id);
}); });
const stopwatchBtn = actions.createEl("button", { cls: "focus-task-task-btn", attr: { "aria-label": "Start Stopwatch" } }); const stopwatchBtn = actions.createEl("button", { cls: "immerse-task-btn", attr: { "aria-label": "Start Stopwatch" } });
stopwatchBtn.innerHTML = "\u23F1"; stopwatchBtn.innerHTML = "\u23F1";
stopwatchBtn.addEventListener("click", (e) => { stopwatchBtn.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
this.plugin.startTimer(task.id); this.plugin.startTimer(task.id);
}); });
} }
const editBtn = actions.createEl("button", { cls: "focus-task-task-btn", attr: { "aria-label": "Edit" } }); const editBtn = actions.createEl("button", { cls: "immerse-task-btn", attr: { "aria-label": "Edit" } });
editBtn.innerHTML = "\u270F\uFE0F"; editBtn.innerHTML = "\u270F\uFE0F";
editBtn.addEventListener("click", (e) => { editBtn.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
new EditTaskModal(this.app, this.plugin, task).open(); new EditTaskModal(this.app, this.plugin, task).open();
}); });
const deleteBtn = actions.createEl("button", { cls: "focus-task-task-btn focus-task-delete-btn", attr: { "aria-label": "Delete" } }); const deleteBtn = actions.createEl("button", { cls: "immerse-task-btn immerse-delete-btn", attr: { "aria-label": "Delete" } });
deleteBtn.innerHTML = "\u{1F5D1}"; deleteBtn.innerHTML = "\u{1F5D1}";
deleteBtn.addEventListener("click", (e) => { deleteBtn.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -487,7 +601,7 @@ var FocusTaskView = class extends import_obsidian2.ItemView {
}; };
// src/main.ts // src/main.ts
var FocusTaskPlugin = class extends import_obsidian3.Plugin { var ImmersePlugin = class extends import_obsidian3.Plugin {
constructor() { constructor() {
super(...arguments); super(...arguments);
// Timer state // Timer state
@@ -497,24 +611,37 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
this.isBreakMode = false; this.isBreakMode = false;
this.activeTaskId = null; this.activeTaskId = null;
this.pomodoroCount = 0; this.pomodoroCount = 0;
// Timestamp-based tracking for reliable background timing
this.timerStartTimestamp = 0;
this.pausedTimeRemaining = 0;
// Focus time tracking (in seconds for accuracy) // Focus time tracking (in seconds for accuracy)
this.focusSecondsToday = 0; this.focusSecondsToday = 0;
this.secondsWorkedOnCurrentTask = 0;
// Status bar element // Status bar element
this.statusBarEl = null; this.statusBarEl = null;
// Reminder system
this.reminderCheckInterval = null;
this.notifiedReminders = /* @__PURE__ */ new Set();
} }
// Track which reminders have been shown
async onload() { async onload() {
await this.loadAllData(); await this.loadAllData();
this.checkDailyReset(); this.checkDailyReset();
document.addEventListener("visibilitychange", () => {
if (!document.hidden && this.isTimerRunning) {
this.syncTimerFromTimestamp();
}
});
this.registerView( this.registerView(
VIEW_TYPE_FOCUS_TASK, VIEW_TYPE_IMMERSE,
(leaf) => new FocusTaskView(leaf, this) (leaf) => new ImmerseView(leaf, this)
); );
this.addRibbonIcon("zap", "Open Focus Task", () => { this.addRibbonIcon("zap", "Open Immerse", () => {
this.activateView(); this.activateView();
}); });
this.addCommand({ this.addCommand({
id: "open-focus-task", id: "open-immerse",
name: "Open Focus Task Panel", name: "Open Immerse Panel",
callback: () => this.activateView() callback: () => this.activateView()
}); });
this.addCommand({ this.addCommand({
@@ -537,16 +664,23 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
name: "Complete Current Task", name: "Complete Current Task",
callback: () => this.completeActiveTask() callback: () => this.completeActiveTask()
}); });
this.addSettingTab(new FocusTaskSettingTab(this.app, this)); this.addSettingTab(new ImmerseSettingTab(this.app, this));
this.createStatusBar(); this.createStatusBar();
if (this.settings.enableReminders) {
this.startReminderSystem();
}
} }
onunload() { onunload() {
this.stopTimer(); this.stopTimer();
this.stopReminderSystem();
} }
async loadAllData() { async loadAllData() {
const loaded = await this.loadData(); const loaded = await this.loadData();
this.data = Object.assign({}, DEFAULT_DATA, (loaded == null ? void 0 : loaded.data) || {}); this.data = Object.assign({}, DEFAULT_DATA, (loaded == null ? void 0 : loaded.data) || {});
this.settings = Object.assign({}, DEFAULT_SETTINGS, (loaded == null ? void 0 : loaded.settings) || {}); this.settings = Object.assign({}, DEFAULT_SETTINGS, (loaded == null ? void 0 : loaded.settings) || {});
if (!this.settings.lists || this.settings.lists.length === 0) {
this.settings.lists = DEFAULT_SETTINGS.lists;
}
this.focusSecondsToday = (this.data.totalFocusMinutesToday || 0) * 60; this.focusSecondsToday = (this.data.totalFocusMinutesToday || 0) * 60;
} }
async saveAllData() { async saveAllData() {
@@ -576,13 +710,13 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
async activateView() { async activateView() {
const { workspace } = this.app; const { workspace } = this.app;
let leaf = null; let leaf = null;
const leaves = workspace.getLeavesOfType(VIEW_TYPE_FOCUS_TASK); const leaves = workspace.getLeavesOfType(VIEW_TYPE_IMMERSE);
if (leaves.length > 0) { if (leaves.length > 0) {
leaf = leaves[0]; leaf = leaves[0];
} else { } else {
leaf = workspace.getRightLeaf(false); leaf = workspace.getRightLeaf(false);
if (leaf) { if (leaf) {
await leaf.setViewState({ type: VIEW_TYPE_FOCUS_TASK, active: true }); await leaf.setViewState({ type: VIEW_TYPE_IMMERSE, active: true });
} }
} }
if (leaf) { if (leaf) {
@@ -661,6 +795,13 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
} }
} }
// ============ Timer Management ============ // ============ Timer Management ============
// Sync timer based on timestamp when app returns from background
syncTimerFromTimestamp() {
if (!this.isTimerRunning)
return;
this.updateStatusBar();
this.updateTimerDisplay();
}
startTimer(taskId) { startTimer(taskId) {
const task = this.data.tasks.find((t) => t.id === taskId); const task = this.data.tasks.find((t) => t.id === taskId);
if (!task) if (!task)
@@ -671,15 +812,26 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
this.isBreakMode = false; this.isBreakMode = false;
this.currentTimerSeconds = 0; this.currentTimerSeconds = 0;
this.isTimerRunning = true; this.isTimerRunning = true;
this.secondsWorkedOnCurrentTask = task.actualMinutes * 60;
this.timerStartTimestamp = Date.now();
this.pausedTimeRemaining = 0;
const initialSecondsWorked = this.secondsWorkedOnCurrentTask;
let alertShown = false;
this.refreshView(); this.refreshView();
this.updateStatusBar(); this.updateStatusBar();
this.timerInterval = window.setInterval(() => { this.timerInterval = window.setInterval(() => {
this.currentTimerSeconds++; const now = Date.now();
task.actualMinutes = Math.floor(this.currentTimerSeconds / 60); const elapsedMs = now - this.timerStartTimestamp;
this.focusSecondsToday++; const elapsedSeconds = Math.floor(elapsedMs / 1e3);
this.currentTimerSeconds = elapsedSeconds;
this.secondsWorkedOnCurrentTask = initialSecondsWorked + elapsedSeconds;
task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60);
const newFocusSeconds = Math.floor((this.data.totalFocusMinutesToday || 0) * 60) + elapsedSeconds;
this.focusSecondsToday = newFocusSeconds;
this.updateStatusBar(); this.updateStatusBar();
this.updateTimerDisplay(); this.updateTimerDisplay();
if (this.currentTimerSeconds === task.estimatedMinutes * 60) { if (!alertShown && this.currentTimerSeconds >= task.estimatedMinutes * 60) {
alertShown = true;
if (this.settings.enableSounds) { if (this.settings.enableSounds) {
this.playAlertSound(); this.playAlertSound();
} }
@@ -698,15 +850,25 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
this.isBreakMode = false; this.isBreakMode = false;
this.currentTimerSeconds = this.settings.pomodoroWorkMinutes * 60; this.currentTimerSeconds = this.settings.pomodoroWorkMinutes * 60;
this.isTimerRunning = true; this.isTimerRunning = true;
this.secondsWorkedOnCurrentTask = Math.floor(task.actualMinutes * 60);
this.timerStartTimestamp = Date.now();
this.pausedTimeRemaining = this.currentTimerSeconds;
const initialSecondsWorked = this.secondsWorkedOnCurrentTask;
this.refreshView(); this.refreshView();
this.updateStatusBar(); this.updateStatusBar();
let secondsWorked = 0;
this.timerInterval = window.setInterval(() => { this.timerInterval = window.setInterval(() => {
this.currentTimerSeconds--; const now = Date.now();
const elapsedMs = now - this.timerStartTimestamp;
const elapsedSeconds = Math.floor(elapsedMs / 1e3);
this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds);
if (!this.isBreakMode) { if (!this.isBreakMode) {
secondsWorked++; this.secondsWorkedOnCurrentTask = initialSecondsWorked + elapsedSeconds;
task.actualMinutes = Math.floor(secondsWorked / 60); const actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60);
this.focusSecondsToday++; if (task.actualMinutes !== actualMinutes) {
task.actualMinutes = actualMinutes;
}
const newFocusSeconds = Math.floor((this.data.totalFocusMinutesToday || 0) * 60) + elapsedSeconds;
this.focusSecondsToday = newFocusSeconds;
} }
this.updateStatusBar(); this.updateStatusBar();
this.updateTimerDisplay(); this.updateTimerDisplay();
@@ -716,6 +878,14 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
}, 1e3); }, 1e3);
} }
handlePomodoroEnd() { handlePomodoroEnd() {
if (this.timerInterval) {
window.clearInterval(this.timerInterval);
this.timerInterval = null;
}
this.currentTimerSeconds = 0;
this.isTimerRunning = false;
this.updateStatusBar();
this.updateTimerDisplay();
if (!this.isBreakMode) { if (!this.isBreakMode) {
this.pomodoroCount++; this.pomodoroCount++;
this.data.pomodorosCompleted++; this.data.pomodorosCompleted++;
@@ -726,27 +896,32 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
if (this.settings.autoStartBreak) { if (this.settings.autoStartBreak) {
this.startBreak(); this.startBreak();
} else { } else {
this.stopTimer(); this.refreshView();
} }
} else { } else {
if (this.settings.enableSounds) { if (this.settings.enableSounds) {
this.playAlertSound(); this.playAlertSound();
} }
new import_obsidian3.Notice("\u26A1 Break over! Ready to focus?"); new import_obsidian3.Notice("\u26A1 Break over! Ready to focus?");
this.isBreakMode = false; this.refreshView();
this.stopTimer();
} }
this.saveAllData(); this.saveAllData();
} }
startBreak() { startBreak() {
this.isBreakMode = true; this.isBreakMode = true;
this.isTimerRunning = true;
const isLongBreak = this.pomodoroCount % this.settings.longBreakInterval === 0; const isLongBreak = this.pomodoroCount % this.settings.longBreakInterval === 0;
this.currentTimerSeconds = (isLongBreak ? this.settings.longBreakMinutes : this.settings.pomodoroBreakMinutes) * 60; this.currentTimerSeconds = (isLongBreak ? this.settings.longBreakMinutes : this.settings.pomodoroBreakMinutes) * 60;
this.timerStartTimestamp = Date.now();
this.pausedTimeRemaining = this.currentTimerSeconds;
new import_obsidian3.Notice(isLongBreak ? "\u2615 Long break time!" : "\u2615 Short break time!"); new import_obsidian3.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(() => {
this.currentTimerSeconds--; const now = Date.now();
const elapsedMs = now - this.timerStartTimestamp;
const elapsedSeconds = Math.floor(elapsedMs / 1e3);
this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds);
this.updateStatusBar(); this.updateStatusBar();
this.updateTimerDisplay(); this.updateTimerDisplay();
if (this.currentTimerSeconds <= 0) { if (this.currentTimerSeconds <= 0) {
@@ -761,14 +936,22 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
window.clearInterval(this.timerInterval); window.clearInterval(this.timerInterval);
this.timerInterval = null; this.timerInterval = null;
this.isTimerRunning = false; this.isTimerRunning = false;
this.pausedTimeRemaining = this.currentTimerSeconds;
} else if (this.activeTaskId) { } else if (this.activeTaskId) {
this.isTimerRunning = true; this.isTimerRunning = true;
this.timerStartTimestamp = Date.now();
const task = this.data.tasks.find((t) => t.id === this.activeTaskId); const task = this.data.tasks.find((t) => t.id === this.activeTaskId);
const initialSecondsWorked = this.secondsWorkedOnCurrentTask;
this.timerInterval = window.setInterval(() => { this.timerInterval = window.setInterval(() => {
this.currentTimerSeconds--; const now = Date.now();
const elapsedMs = now - this.timerStartTimestamp;
const elapsedSeconds = Math.floor(elapsedMs / 1e3);
this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds);
if (task && !this.isBreakMode) { if (task && !this.isBreakMode) {
task.actualMinutes = Math.floor((this.settings.pomodoroWorkMinutes * 60 - this.currentTimerSeconds) / 60); this.secondsWorkedOnCurrentTask = initialSecondsWorked + elapsedSeconds;
this.focusSecondsToday++; task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60);
const newFocusSeconds = Math.floor((this.data.totalFocusMinutesToday || 0) * 60) + elapsedSeconds;
this.focusSecondsToday = newFocusSeconds;
} }
this.updateStatusBar(); this.updateStatusBar();
this.updateTimerDisplay(); this.updateTimerDisplay();
@@ -791,10 +974,14 @@ var FocusTaskPlugin = 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;
task.actualMinutes = 0;
} }
} }
this.isTimerRunning = false; this.isTimerRunning = false;
this.activeTaskId = null; this.activeTaskId = null;
this.secondsWorkedOnCurrentTask = 0;
this.timerStartTimestamp = 0;
this.pausedTimeRemaining = 0;
this.updateStatusBar(); this.updateStatusBar();
this.saveAllData(); this.saveAllData();
this.refreshView(); this.refreshView();
@@ -810,7 +997,7 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
// ============ Status Bar Timer ============ // ============ Status Bar Timer ============
createStatusBar() { createStatusBar() {
this.statusBarEl = this.addStatusBarItem(); this.statusBarEl = this.addStatusBarItem();
this.statusBarEl.addClass("focus-task-status-bar"); this.statusBarEl.addClass("immerse-status-bar");
this.updateStatusBar(); this.updateStatusBar();
this.statusBarEl.addEventListener("click", () => { this.statusBarEl.addEventListener("click", () => {
this.activateView(); this.activateView();
@@ -825,10 +1012,63 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
const timeStr = this.formatTime(this.currentTimerSeconds); const timeStr = this.formatTime(this.currentTimerSeconds);
const icon = this.isTimerRunning ? "\u25B6" : "\u23F8"; const icon = this.isTimerRunning ? "\u25B6" : "\u23F8";
this.statusBarEl.setText(`\u26A1 ${icon} ${timeStr} - ${taskName}${task && task.text.length > 20 ? "..." : ""}`); this.statusBarEl.setText(`\u26A1 ${icon} ${timeStr} - ${taskName}${task && task.text.length > 20 ? "..." : ""}`);
this.statusBarEl.addClass("focus-task-status-active"); this.statusBarEl.addClass("immerse-status-active");
} else { } else {
this.statusBarEl.setText("\u26A1 Focus Task"); this.statusBarEl.setText("\u26A1 Immerse");
this.statusBarEl.removeClass("focus-task-status-active"); this.statusBarEl.removeClass("immerse-status-active");
}
}
// ============ Reminder System ============
startReminderSystem() {
this.reminderCheckInterval = window.setInterval(() => {
this.checkReminders();
}, 3e4);
this.checkReminders();
}
stopReminderSystem() {
if (this.reminderCheckInterval) {
window.clearInterval(this.reminderCheckInterval);
this.reminderCheckInterval = null;
}
}
checkReminders() {
if (!this.settings.enableReminders)
return;
const now = new Date();
const currentTime = now.getTime();
this.data.tasks.filter((task) => !task.completed && task.scheduledDate && task.scheduledTime).forEach((task) => {
const reminderKey = `${task.id}-${task.scheduledDate}-${task.scheduledTime}`;
if (this.notifiedReminders.has(reminderKey))
return;
const scheduledDateTime = new Date(`${task.scheduledDate}T${task.scheduledTime}`);
const scheduledTime = scheduledDateTime.getTime();
if (currentTime > scheduledTime) {
this.showOverdueNotice(task);
this.notifiedReminders.add(reminderKey);
return;
}
if (task.reminderMinutes) {
const reminderTime = scheduledTime - task.reminderMinutes * 60 * 1e3;
if (currentTime >= reminderTime) {
this.showReminder(task);
this.notifiedReminders.add(reminderKey);
}
}
});
}
showReminder(task) {
const timeStr = task.scheduledTime;
new import_obsidian3.Notice(`\u{1F514} Reminder: "${task.text}" is scheduled for ${timeStr}`, 8e3);
if (this.settings.enableSounds) {
this.playAlertSound();
}
}
showOverdueNotice(task) {
const dateStr = task.scheduledDate;
const timeStr = task.scheduledTime;
new import_obsidian3.Notice(`\u26A0\uFE0F Overdue: "${task.text}" was scheduled for ${dateStr} ${timeStr}`, 1e4);
if (this.settings.enableSounds) {
this.playAlertSound();
} }
} }
// ============ Sounds & Celebrations ============ // ============ Sounds & Celebrations ============
@@ -1005,18 +1245,18 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
return mins > 0 ? `${hours}hr ${mins}min` : `${hours}hr`; return mins > 0 ? `${hours}hr ${mins}min` : `${hours}hr`;
} }
refreshView() { refreshView() {
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_FOCUS_TASK); const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_IMMERSE);
leaves.forEach((leaf) => { leaves.forEach((leaf) => {
if (leaf.view instanceof FocusTaskView) { if (leaf.view instanceof ImmerseView) {
leaf.view.refresh(); leaf.view.refresh();
} }
}); });
} }
// Light refresh - only updates timer display without rebuilding DOM // Light refresh - only updates timer display without rebuilding DOM
updateTimerDisplay() { updateTimerDisplay() {
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_FOCUS_TASK); const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_IMMERSE);
leaves.forEach((leaf) => { leaves.forEach((leaf) => {
if (leaf.view instanceof FocusTaskView) { if (leaf.view instanceof ImmerseView) {
leaf.view.updateTimerDisplay(); leaf.view.updateTimerDisplay();
} }
}); });
@@ -1053,7 +1293,7 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
}; };
} }
}; };
var FocusTaskSettingTab = class extends import_obsidian3.PluginSettingTab { var ImmerseSettingTab = class extends import_obsidian3.PluginSettingTab {
constructor(app, plugin) { constructor(app, plugin) {
super(app, plugin); super(app, plugin);
this.plugin = plugin; this.plugin = plugin;
@@ -1061,7 +1301,7 @@ var FocusTaskSettingTab = class extends import_obsidian3.PluginSettingTab {
display() { display() {
const { containerEl } = this; const { containerEl } = this;
containerEl.empty(); containerEl.empty();
containerEl.createEl("h1", { text: "\u26A1 Focus Task 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_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) => {
this.plugin.settings.pomodoroWorkMinutes = value; this.plugin.settings.pomodoroWorkMinutes = value;
@@ -1139,14 +1379,14 @@ var FocusTaskSettingTab = class extends import_obsidian3.PluginSettingTab {
this.display(); this.display();
})); }));
containerEl.createEl("h2", { text: "\u{1F4D6} About" }); containerEl.createEl("h2", { text: "\u{1F4D6} About" });
const aboutDiv = containerEl.createDiv({ cls: "focus-task-about" }); const aboutDiv = containerEl.createDiv({ cls: "immerse-about" });
aboutDiv.innerHTML = ` aboutDiv.innerHTML = `
<p><strong>Focus Task</strong> is heavily inspired by <a href="https://www.blitzit.app/">Blitzit</a>, <p><strong>Immerse</strong> is heavily inspired by <a href="https://www.blitzit.app/">Blitzit</a>,
a fantastic productivity app that combines task management with focused time tracking.</p> a fantastic productivity app that combines task management with focused time tracking.</p>
<p>This plugin brings similar functionality directly into Obsidian, allowing you to manage tasks, <p>This plugin brings similar functionality directly into Obsidian, allowing you to manage tasks,
use the Pomodoro technique, and track your productivity without leaving your notes.</p> use the Pomodoro technique, and track your productivity without leaving your notes.</p>
<p> <p>
<a href="https://git.cribdev.com/crib/focus-task">Source Code</a> <a href="https://git.cribdev.com/crib/immerse">Source Code</a>
</p> </p>
`; `;
} }

View File

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

62
package-release.mjs Normal file
View File

@@ -0,0 +1,62 @@
import { promises as fs } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const RELEASE_DIR = 'release';
const FILES_TO_INCLUDE = [
'main.js',
'manifest.json',
'styles.css'
];
async function packageRelease() {
try {
console.log('📦 Packaging release files...\n');
// Create release directory if it doesn't exist
try {
await fs.access(RELEASE_DIR);
console.log(`✓ Release directory exists: ${RELEASE_DIR}`);
} catch {
await fs.mkdir(RELEASE_DIR);
console.log(`✓ Created release directory: ${RELEASE_DIR}`);
}
// Copy each file
for (const file of FILES_TO_INCLUDE) {
const sourcePath = path.join(__dirname, file);
const destPath = path.join(__dirname, RELEASE_DIR, file);
try {
await fs.copyFile(sourcePath, destPath);
console.log(`✓ Copied ${file}`);
} catch (error) {
console.error(`✗ Failed to copy ${file}:`, error.message);
process.exit(1);
}
}
// Read manifest to get version
const manifestPath = path.join(__dirname, 'manifest.json');
const manifestContent = await fs.readFile(manifestPath, 'utf8');
const manifest = JSON.parse(manifestContent);
console.log(`\n✅ Release package created successfully!`);
console.log(`📁 Location: ./${RELEASE_DIR}/`);
console.log(`📌 Version: ${manifest.version}`);
console.log(`\nFiles included:`);
FILES_TO_INCLUDE.forEach(file => console.log(` - ${file}`));
console.log(`\n💡 Tip: You can now upload these files from the '${RELEASE_DIR}' directory to your Gitea release.`);
console.log(`💡 Tip: To create a zip, run: cd ${RELEASE_DIR} && zip -r ../immerse-${manifest.version}.zip *`);
} catch (error) {
console.error('❌ Error packaging release:', error);
process.exit(1);
}
}
packageRelease();

View File

@@ -1,11 +1,12 @@
{ {
"name": "focus-task", "name": "immerse",
"version": "1.0.4", "version": "1.0.9",
"description": "A Blitzit-inspired task management and focus timer plugin for Obsidian", "description": "A Blitzit-inspired task management and focus timer plugin for Obsidian",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"dev": "node esbuild.config.mjs", "dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"package": "npm run build && node package-release.mjs",
"version": "node version-bump.mjs && git add manifest.json versions.json" "version": "node version-bump.mjs && git add manifest.json versions.json"
}, },
"keywords": [ "keywords": [
@@ -21,7 +22,7 @@
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.cribdev.com/crib/focus-task" "url": "https://git.cribdev.com/crib/immerse"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^16.11.6", "@types/node": "^16.11.6",

View File

@@ -9,25 +9,25 @@ import {
} from 'obsidian'; } from 'obsidian';
import { import {
FocusTask, ImmerseTask,
FocusTaskSettings, ImmerseSettings,
FocusTaskData, ImmerseData,
DEFAULT_SETTINGS, DEFAULT_SETTINGS,
DEFAULT_DATA, DEFAULT_DATA,
VIEW_TYPE_FOCUS_TASK, VIEW_TYPE_IMMERSE,
CELEBRATION_MESSAGES, CELEBRATION_MESSAGES,
EARLY_FINISH_MESSAGES, EARLY_FINISH_MESSAGES,
OVERTIME_MESSAGES, OVERTIME_MESSAGES,
} from './types'; } from './types';
import { FocusTaskView } from './view'; import { ImmerseView } from './view';
import { QuickAddTaskModal } from './modals'; import { QuickAddTaskModal } from './modals';
// ============ Main Plugin Class ============ // ============ Main Plugin Class ============
export default class FocusTaskPlugin extends Plugin { export default class ImmersePlugin extends Plugin {
settings: FocusTaskSettings; settings: ImmerseSettings;
data: FocusTaskData; data: ImmerseData;
// Timer state // Timer state
timerInterval: number | null = null; timerInterval: number | null = null;
@@ -37,33 +37,49 @@ export default class FocusTaskPlugin extends Plugin {
activeTaskId: string | null = null; activeTaskId: string | null = null;
pomodoroCount: number = 0; pomodoroCount: number = 0;
// Timestamp-based tracking for reliable background timing
private timerStartTimestamp: number = 0;
private pausedTimeRemaining: number = 0;
// Focus time tracking (in seconds for accuracy) // Focus time tracking (in seconds for accuracy)
private focusSecondsToday: number = 0; private focusSecondsToday: number = 0;
private secondsWorkedOnCurrentTask: number = 0;
// Status bar element // Status bar element
statusBarEl: HTMLElement | null = null; statusBarEl: HTMLElement | null = null;
// Reminder system
private reminderCheckInterval: number | null = null;
private notifiedReminders: Set<string> = new Set(); // Track which reminders have been shown
async onload() { async onload() {
await this.loadAllData(); await this.loadAllData();
// Check and reset daily stats // Check and reset daily stats
this.checkDailyReset(); this.checkDailyReset();
// Handle visibility changes to sync timer when app comes back to foreground
document.addEventListener('visibilitychange', () => {
if (!document.hidden && this.isTimerRunning) {
this.syncTimerFromTimestamp();
}
});
// Register the main view // Register the main view
this.registerView( this.registerView(
VIEW_TYPE_FOCUS_TASK, VIEW_TYPE_IMMERSE,
(leaf) => new FocusTaskView(leaf, this) (leaf) => new ImmerseView(leaf, this)
); );
// Add ribbon icon // Add ribbon icon
this.addRibbonIcon('zap', 'Open Focus Task', () => { this.addRibbonIcon('zap', 'Open Immerse', () => {
this.activateView(); this.activateView();
}); });
// Add commands // Add commands
this.addCommand({ this.addCommand({
id: 'open-focus-task', id: 'open-immerse',
name: 'Open Focus Task Panel', name: 'Open Immerse Panel',
callback: () => this.activateView(), callback: () => this.activateView(),
}); });
@@ -92,20 +108,35 @@ export default class FocusTaskPlugin extends Plugin {
}); });
// Add settings tab // Add settings tab
this.addSettingTab(new FocusTaskSettingTab(this.app, this)); this.addSettingTab(new ImmerseSettingTab(this.app, this));
// Create status bar timer // Create status bar timer
this.createStatusBar(); this.createStatusBar();
// Start reminder checking system
if (this.settings.enableReminders) {
this.startReminderSystem();
}
} }
onunload() { onunload() {
this.stopTimer(); this.stopTimer();
this.stopReminderSystem();
} }
async loadAllData() { async loadAllData() {
const loaded = await this.loadData(); const loaded = await this.loadData();
// Merge loaded data with defaults (defaults first, then override with loaded)
// This ensures new fields get default values even for existing installs
this.data = Object.assign({}, DEFAULT_DATA, loaded?.data || {}); this.data = Object.assign({}, DEFAULT_DATA, loaded?.data || {});
this.settings = Object.assign({}, DEFAULT_SETTINGS, loaded?.settings || {}); this.settings = Object.assign({}, DEFAULT_SETTINGS, loaded?.settings || {});
// Ensure lists array exists and has at least the default lists
if (!this.settings.lists || this.settings.lists.length === 0) {
this.settings.lists = DEFAULT_SETTINGS.lists;
}
// Initialize seconds from stored minutes // Initialize seconds from stored minutes
this.focusSecondsToday = (this.data.totalFocusMinutesToday || 0) * 60; this.focusSecondsToday = (this.data.totalFocusMinutesToday || 0) * 60;
} }
@@ -144,14 +175,14 @@ export default class FocusTaskPlugin extends Plugin {
const { workspace } = this.app; const { workspace } = this.app;
let leaf: WorkspaceLeaf | null = null; let leaf: WorkspaceLeaf | null = null;
const leaves = workspace.getLeavesOfType(VIEW_TYPE_FOCUS_TASK); const leaves = workspace.getLeavesOfType(VIEW_TYPE_IMMERSE);
if (leaves.length > 0) { if (leaves.length > 0) {
leaf = leaves[0]; leaf = leaves[0];
} else { } else {
leaf = workspace.getRightLeaf(false); leaf = workspace.getRightLeaf(false);
if (leaf) { if (leaf) {
await leaf.setViewState({ type: VIEW_TYPE_FOCUS_TASK, active: true }); await leaf.setViewState({ type: VIEW_TYPE_IMMERSE, active: true });
} }
} }
@@ -162,7 +193,7 @@ export default class FocusTaskPlugin extends Plugin {
// ============ Task Management ============ // ============ Task Management ============
createTask(text: string, estimatedMinutes: number = this.settings.defaultEstimateMinutes, list: string = 'work'): FocusTask { createTask(text: string, estimatedMinutes: number = this.settings.defaultEstimateMinutes, list: string = 'work'): ImmerseTask {
return { return {
id: this.generateId(), id: this.generateId(),
text, text,
@@ -180,13 +211,13 @@ export default class FocusTaskPlugin extends Plugin {
return Date.now().toString(36) + Math.random().toString(36).substr(2); return Date.now().toString(36) + Math.random().toString(36).substr(2);
} }
addTask(task: FocusTask) { addTask(task: ImmerseTask) {
this.data.tasks.push(task); this.data.tasks.push(task);
this.saveAllData(); this.saveAllData();
this.refreshView(); this.refreshView();
} }
updateTask(taskId: string, updates: Partial<FocusTask>) { updateTask(taskId: string, updates: Partial<ImmerseTask>) {
const task = this.data.tasks.find(t => t.id === taskId); const task = this.data.tasks.find(t => t.id === taskId);
if (task) { if (task) {
Object.assign(task, updates); Object.assign(task, updates);
@@ -250,6 +281,18 @@ export default class FocusTaskPlugin extends Plugin {
// ============ Timer Management ============ // ============ Timer Management ============
// Sync timer based on timestamp when app returns from background
syncTimerFromTimestamp() {
// Since all intervals now calculate from timestamps directly,
// we just need to trigger an update when coming back to foreground
if (!this.isTimerRunning) return;
// The interval will automatically calculate correct values on next tick
// Just update the display immediately to show current state
this.updateStatusBar();
this.updateTimerDisplay();
}
startTimer(taskId: string) { startTimer(taskId: string) {
const task = this.data.tasks.find(t => t.id === taskId); const task = this.data.tasks.find(t => t.id === taskId);
if (!task) return; if (!task) return;
@@ -263,6 +306,15 @@ export default class FocusTaskPlugin extends Plugin {
this.isBreakMode = false; this.isBreakMode = false;
this.currentTimerSeconds = 0; this.currentTimerSeconds = 0;
this.isTimerRunning = true; this.isTimerRunning = true;
this.secondsWorkedOnCurrentTask = task.actualMinutes * 60;
// Set timestamp for background tracking (stopwatch mode)
this.timerStartTimestamp = Date.now();
this.pausedTimeRemaining = 0; // 0 indicates stopwatch mode
// Store initial values
const initialSecondsWorked = this.secondsWorkedOnCurrentTask;
let alertShown = false;
// Full refresh to show the active task card // Full refresh to show the active task card
this.refreshView(); this.refreshView();
@@ -270,18 +322,29 @@ export default class FocusTaskPlugin extends Plugin {
// Start interval (count up mode - stopwatch) // Start interval (count up mode - stopwatch)
this.timerInterval = window.setInterval(() => { this.timerInterval = window.setInterval(() => {
this.currentTimerSeconds++; // Calculate elapsed time from timestamp
task.actualMinutes = Math.floor(this.currentTimerSeconds / 60); const now = Date.now();
const elapsedMs = now - this.timerStartTimestamp;
const elapsedSeconds = Math.floor(elapsedMs / 1000);
// Track focus time // Update timer (count up)
this.focusSecondsToday++; this.currentTimerSeconds = elapsedSeconds;
// Update actual time worked
this.secondsWorkedOnCurrentTask = initialSecondsWorked + elapsedSeconds;
task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60);
// Update focus time
const newFocusSeconds = Math.floor((this.data.totalFocusMinutesToday || 0) * 60) + elapsedSeconds;
this.focusSecondsToday = newFocusSeconds;
// Light update - only timer display, no full refresh // Light update - only timer display, no full refresh
this.updateStatusBar(); this.updateStatusBar();
this.updateTimerDisplay(); this.updateTimerDisplay();
// Check if over estimate // Check if over estimate (only alert once)
if (this.currentTimerSeconds === task.estimatedMinutes * 60) { if (!alertShown && this.currentTimerSeconds >= task.estimatedMinutes * 60) {
alertShown = true;
if (this.settings.enableSounds) { if (this.settings.enableSounds) {
this.playAlertSound(); this.playAlertSound();
} }
@@ -303,21 +366,42 @@ export default class FocusTaskPlugin extends Plugin {
this.currentTimerSeconds = this.settings.pomodoroWorkMinutes * 60; this.currentTimerSeconds = this.settings.pomodoroWorkMinutes * 60;
this.isTimerRunning = true; this.isTimerRunning = true;
// Initialize from existing actual time to preserve progress across breaks
// Store as seconds for precision
this.secondsWorkedOnCurrentTask = Math.floor(task.actualMinutes * 60);
// Set timestamp for background tracking (pomodoro countdown mode)
this.timerStartTimestamp = Date.now();
this.pausedTimeRemaining = this.currentTimerSeconds;
// Store the initial seconds worked to calculate delta
const initialSecondsWorked = this.secondsWorkedOnCurrentTask;
// Full refresh to show the active task card // Full refresh to show the active task card
this.refreshView(); this.refreshView();
this.updateStatusBar(); this.updateStatusBar();
// Track seconds worked for accurate focus time
let secondsWorked = 0;
this.timerInterval = window.setInterval(() => { this.timerInterval = window.setInterval(() => {
this.currentTimerSeconds--; // Calculate elapsed time from timestamp (more accurate than counting ticks)
const now = Date.now();
const elapsedMs = now - this.timerStartTimestamp;
const elapsedSeconds = Math.floor(elapsedMs / 1000);
// Update countdown timer
this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds);
if (!this.isBreakMode) { if (!this.isBreakMode) {
secondsWorked++; // Update actual time worked based on real elapsed time
task.actualMinutes = Math.floor(secondsWorked / 60); this.secondsWorkedOnCurrentTask = initialSecondsWorked + elapsedSeconds;
// Increment focus time by 1 second const actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60);
this.focusSecondsToday++;
if (task.actualMinutes !== actualMinutes) {
task.actualMinutes = actualMinutes;
}
// Update focus time based on elapsed seconds
const newFocusSeconds = Math.floor((this.data.totalFocusMinutesToday || 0) * 60) + elapsedSeconds;
this.focusSecondsToday = newFocusSeconds;
} }
// Light update - only timer display, no full refresh // Light update - only timer display, no full refresh
@@ -331,6 +415,20 @@ export default class FocusTaskPlugin extends Plugin {
} }
handlePomodoroEnd() { handlePomodoroEnd() {
// Stop the timer interval to prevent going into negative
if (this.timerInterval) {
window.clearInterval(this.timerInterval);
this.timerInterval = null;
}
// Set timer to 0 to ensure it doesn't show negative
this.currentTimerSeconds = 0;
this.isTimerRunning = false;
// Update displays immediately
this.updateStatusBar();
this.updateTimerDisplay();
if (!this.isBreakMode) { if (!this.isBreakMode) {
// Work session ended // Work session ended
this.pomodoroCount++; this.pomodoroCount++;
@@ -345,16 +443,17 @@ export default class FocusTaskPlugin extends Plugin {
if (this.settings.autoStartBreak) { if (this.settings.autoStartBreak) {
this.startBreak(); this.startBreak();
} else { } else {
this.stopTimer(); this.refreshView();
} }
} else { } else {
// Break ended // Break ended - keep timer at 0 until user resumes
if (this.settings.enableSounds) { if (this.settings.enableSounds) {
this.playAlertSound(); this.playAlertSound();
} }
new Notice('⚡ Break over! Ready to focus?'); new Notice('⚡ Break over! Ready to focus?');
this.isBreakMode = false;
this.stopTimer(); // Keep the break card visible with timer at 0:00
this.refreshView();
} }
this.saveAllData(); this.saveAllData();
@@ -362,9 +461,14 @@ export default class FocusTaskPlugin extends Plugin {
startBreak() { startBreak() {
this.isBreakMode = true; this.isBreakMode = true;
this.isTimerRunning = true;
const isLongBreak = this.pomodoroCount % this.settings.longBreakInterval === 0; const isLongBreak = this.pomodoroCount % this.settings.longBreakInterval === 0;
this.currentTimerSeconds = (isLongBreak ? this.settings.longBreakMinutes : this.settings.pomodoroBreakMinutes) * 60; this.currentTimerSeconds = (isLongBreak ? this.settings.longBreakMinutes : this.settings.pomodoroBreakMinutes) * 60;
// Set timestamp for background tracking (break countdown mode)
this.timerStartTimestamp = Date.now();
this.pausedTimeRemaining = this.currentTimerSeconds;
new Notice(isLongBreak ? '☕ Long break time!' : '☕ Short break time!'); new Notice(isLongBreak ? '☕ Long break time!' : '☕ Short break time!');
// Full refresh to show break state // Full refresh to show break state
@@ -372,7 +476,14 @@ export default class FocusTaskPlugin extends Plugin {
if (!this.timerInterval) { if (!this.timerInterval) {
this.timerInterval = window.setInterval(() => { this.timerInterval = window.setInterval(() => {
this.currentTimerSeconds--; // Calculate elapsed time from timestamp
const now = Date.now();
const elapsedMs = now - this.timerStartTimestamp;
const elapsedSeconds = Math.floor(elapsedMs / 1000);
// Update countdown timer
this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds);
// Light update - only timer display // Light update - only timer display
this.updateStatusBar(); this.updateStatusBar();
this.updateTimerDisplay(); this.updateTimerDisplay();
@@ -388,22 +499,39 @@ export default class FocusTaskPlugin extends Plugin {
toggleTimer() { toggleTimer() {
if (this.isTimerRunning && this.timerInterval) { if (this.isTimerRunning && this.timerInterval) {
// Pause // Pause - save current state
window.clearInterval(this.timerInterval); window.clearInterval(this.timerInterval);
this.timerInterval = null; this.timerInterval = null;
this.isTimerRunning = false; this.isTimerRunning = false;
this.pausedTimeRemaining = this.currentTimerSeconds;
} else if (this.activeTaskId) { } else if (this.activeTaskId) {
// Resume // Resume - restart with new timestamp
this.isTimerRunning = true; this.isTimerRunning = true;
this.timerStartTimestamp = Date.now();
const task = this.data.tasks.find(t => t.id === this.activeTaskId); const task = this.data.tasks.find(t => t.id === this.activeTaskId);
// Store initial values for resume
const initialSecondsWorked = this.secondsWorkedOnCurrentTask;
this.timerInterval = window.setInterval(() => { this.timerInterval = window.setInterval(() => {
this.currentTimerSeconds--; // Calculate elapsed time from timestamp
const now = Date.now();
const elapsedMs = now - this.timerStartTimestamp;
const elapsedSeconds = Math.floor(elapsedMs / 1000);
// Update timer (countdown from paused position)
this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds);
if (task && !this.isBreakMode) { if (task && !this.isBreakMode) {
task.actualMinutes = Math.floor((this.settings.pomodoroWorkMinutes * 60 - this.currentTimerSeconds) / 60); // Update actual time worked
// Track focus time this.secondsWorkedOnCurrentTask = initialSecondsWorked + elapsedSeconds;
this.focusSecondsToday++; task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60);
// Update focus time
const newFocusSeconds = Math.floor((this.data.totalFocusMinutesToday || 0) * 60) + elapsedSeconds;
this.focusSecondsToday = newFocusSeconds;
} }
// Light update - only timer display // Light update - only timer display
this.updateStatusBar(); this.updateStatusBar();
this.updateTimerDisplay(); this.updateTimerDisplay();
@@ -431,11 +559,17 @@ export default class FocusTaskPlugin extends Plugin {
const task = this.data.tasks.find(t => t.id === this.activeTaskId); const task = this.data.tasks.find(t => t.id === this.activeTaskId);
if (task) { if (task) {
task.isActive = false; task.isActive = false;
// Reset actual time when manually stopping (not after a break)
// This allows starting fresh next time
task.actualMinutes = 0;
} }
} }
this.isTimerRunning = false; this.isTimerRunning = false;
this.activeTaskId = null; this.activeTaskId = null;
this.secondsWorkedOnCurrentTask = 0;
this.timerStartTimestamp = 0;
this.pausedTimeRemaining = 0;
this.updateStatusBar(); this.updateStatusBar();
this.saveAllData(); this.saveAllData();
this.refreshView(); this.refreshView();
@@ -454,7 +588,7 @@ export default class FocusTaskPlugin extends Plugin {
createStatusBar() { createStatusBar() {
this.statusBarEl = this.addStatusBarItem(); this.statusBarEl = this.addStatusBarItem();
this.statusBarEl.addClass('focus-task-status-bar'); this.statusBarEl.addClass('immerse-status-bar');
this.updateStatusBar(); this.updateStatusBar();
// Click to open panel // Click to open panel
@@ -473,16 +607,94 @@ export default class FocusTaskPlugin extends Plugin {
const icon = this.isTimerRunning ? '▶' : '⏸'; const icon = this.isTimerRunning ? '▶' : '⏸';
this.statusBarEl.setText(`${icon} ${timeStr} - ${taskName}${task && task.text.length > 20 ? '...' : ''}`); this.statusBarEl.setText(`${icon} ${timeStr} - ${taskName}${task && task.text.length > 20 ? '...' : ''}`);
this.statusBarEl.addClass('focus-task-status-active'); this.statusBarEl.addClass('immerse-status-active');
} else { } else {
this.statusBarEl.setText('⚡ Focus Task'); this.statusBarEl.setText('⚡ Immerse');
this.statusBarEl.removeClass('focus-task-status-active'); this.statusBarEl.removeClass('immerse-status-active');
}
}
// ============ Reminder System ============
startReminderSystem() {
// Check for reminders every 30 seconds
this.reminderCheckInterval = window.setInterval(() => {
this.checkReminders();
}, 30000);
// Also check immediately
this.checkReminders();
}
stopReminderSystem() {
if (this.reminderCheckInterval) {
window.clearInterval(this.reminderCheckInterval);
this.reminderCheckInterval = null;
}
}
checkReminders() {
if (!this.settings.enableReminders) return;
const now = new Date();
const currentTime = now.getTime();
// Check each incomplete task with scheduling
this.data.tasks
.filter(task => !task.completed && task.scheduledDate && task.scheduledTime)
.forEach(task => {
const reminderKey = `${task.id}-${task.scheduledDate}-${task.scheduledTime}`;
// Skip if we already notified for this reminder
if (this.notifiedReminders.has(reminderKey)) return;
// Parse the scheduled date and time
const scheduledDateTime = new Date(`${task.scheduledDate}T${task.scheduledTime}`);
const scheduledTime = scheduledDateTime.getTime();
// Check for overdue tasks (scheduled time has passed)
if (currentTime > scheduledTime) {
this.showOverdueNotice(task);
this.notifiedReminders.add(reminderKey);
return;
}
// Check for tasks with reminders set
if (task.reminderMinutes) {
const reminderTime = scheduledTime - (task.reminderMinutes * 60 * 1000);
// Show reminder if it's past the reminder time but before the scheduled time
if (currentTime >= reminderTime) {
this.showReminder(task);
this.notifiedReminders.add(reminderKey);
}
}
});
}
showReminder(task: ImmerseTask) {
const timeStr = task.scheduledTime;
new Notice(`🔔 Reminder: "${task.text}" is scheduled for ${timeStr}`, 8000);
// Play alert sound if enabled
if (this.settings.enableSounds) {
this.playAlertSound();
}
}
showOverdueNotice(task: ImmerseTask) {
const dateStr = task.scheduledDate;
const timeStr = task.scheduledTime;
new Notice(`⚠️ Overdue: "${task.text}" was scheduled for ${dateStr} ${timeStr}`, 10000);
// Play alert sound if enabled
if (this.settings.enableSounds) {
this.playAlertSound();
} }
} }
// ============ Sounds & Celebrations ============ // ============ Sounds & Celebrations ============
showCelebration(task: FocusTask) { showCelebration(task: ImmerseTask) {
let messages = CELEBRATION_MESSAGES; let messages = CELEBRATION_MESSAGES;
let extraMessage = ''; let extraMessage = '';
@@ -549,7 +761,7 @@ export default class FocusTaskPlugin extends Plugin {
// ============ Daily Note Logging ============ // ============ Daily Note Logging ============
async logTaskToDailyNote(task: FocusTask) { async logTaskToDailyNote(task: ImmerseTask) {
try { try {
const list = this.settings.lists.find(l => l.id === task.list); const list = this.settings.lists.find(l => l.id === task.list);
@@ -720,9 +932,9 @@ export default class FocusTaskPlugin extends Plugin {
} }
refreshView() { refreshView() {
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_FOCUS_TASK); const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_IMMERSE);
leaves.forEach(leaf => { leaves.forEach(leaf => {
if (leaf.view instanceof FocusTaskView) { if (leaf.view instanceof ImmerseView) {
leaf.view.refresh(); leaf.view.refresh();
} }
}); });
@@ -730,23 +942,23 @@ export default class FocusTaskPlugin extends Plugin {
// Light refresh - only updates timer display without rebuilding DOM // Light refresh - only updates timer display without rebuilding DOM
updateTimerDisplay() { updateTimerDisplay() {
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_FOCUS_TASK); const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_IMMERSE);
leaves.forEach(leaf => { leaves.forEach(leaf => {
if (leaf.view instanceof FocusTaskView) { if (leaf.view instanceof ImmerseView) {
leaf.view.updateTimerDisplay(); leaf.view.updateTimerDisplay();
} }
}); });
} }
getTasksByList(listId: string): FocusTask[] { getTasksByList(listId: string): ImmerseTask[] {
return this.data.tasks.filter(t => t.list === listId); return this.data.tasks.filter(t => t.list === listId);
} }
getPendingTasks(): FocusTask[] { getPendingTasks(): ImmerseTask[] {
return this.data.tasks.filter(t => !t.completed); return this.data.tasks.filter(t => !t.completed);
} }
getTodaysTasks(): FocusTask[] { getTodaysTasks(): ImmerseTask[] {
const today = new Date().toDateString(); const today = new Date().toDateString();
return this.data.tasks.filter(t => { return this.data.tasks.filter(t => {
if (t.scheduledDate === today) return true; if (t.scheduledDate === today) return true;
@@ -777,10 +989,10 @@ export default class FocusTaskPlugin extends Plugin {
// ============ Settings Tab ============ // ============ Settings Tab ============
class FocusTaskSettingTab extends PluginSettingTab { class ImmerseSettingTab extends PluginSettingTab {
plugin: FocusTaskPlugin; plugin: ImmersePlugin;
constructor(app: App, plugin: FocusTaskPlugin) { constructor(app: App, plugin: ImmersePlugin) {
super(app, plugin); super(app, plugin);
this.plugin = plugin; this.plugin = plugin;
} }
@@ -789,7 +1001,7 @@ class FocusTaskSettingTab extends PluginSettingTab {
const { containerEl } = this; const { containerEl } = this;
containerEl.empty(); containerEl.empty();
containerEl.createEl('h1', { text: '⚡ Focus Task Settings' }); containerEl.createEl('h1', { text: '⚡ Immerse Settings' });
// Pomodoro Settings // Pomodoro Settings
containerEl.createEl('h2', { text: '🍅 Pomodoro Timer' }); containerEl.createEl('h2', { text: '🍅 Pomodoro Timer' });
@@ -965,14 +1177,14 @@ class FocusTaskSettingTab extends PluginSettingTab {
// About section // About section
containerEl.createEl('h2', { text: '📖 About' }); containerEl.createEl('h2', { text: '📖 About' });
const aboutDiv = containerEl.createDiv({ cls: 'focus-task-about' }); const aboutDiv = containerEl.createDiv({ cls: 'immerse-about' });
aboutDiv.innerHTML = ` aboutDiv.innerHTML = `
<p><strong>Focus Task</strong> is heavily inspired by <a href="https://www.blitzit.app/">Blitzit</a>, <p><strong>Immerse</strong> is heavily inspired by <a href="https://www.blitzit.app/">Blitzit</a>,
a fantastic productivity app that combines task management with focused time tracking.</p> a fantastic productivity app that combines task management with focused time tracking.</p>
<p>This plugin brings similar functionality directly into Obsidian, allowing you to manage tasks, <p>This plugin brings similar functionality directly into Obsidian, allowing you to manage tasks,
use the Pomodoro technique, and track your productivity without leaving your notes.</p> use the Pomodoro technique, and track your productivity without leaving your notes.</p>
<p> <p>
<a href="https://git.cribdev.com/crib/focus-task">Source Code</a> <a href="https://git.cribdev.com/crib/immerse">Source Code</a>
</p> </p>
`; `;
} }

View File

@@ -5,21 +5,25 @@ import {
Setting, Setting,
} from 'obsidian'; } from 'obsidian';
import { FocusTask } from './types'; import { ImmerseTask } from './types';
import FocusTaskPlugin from './main'; import ImmersePlugin from './main';
// ============ Quick Add Task Modal ============ // ============ Quick Add Task Modal ============
export class QuickAddTaskModal extends Modal { export class QuickAddTaskModal extends Modal {
plugin: FocusTaskPlugin; plugin: ImmersePlugin;
taskText: string = ''; taskText: string = '';
estimatedMinutes: number; estimatedMinutes: number;
selectedList: string = 'work'; selectedList: string = 'work';
scheduledDate: string = '';
scheduledTime: string = '';
reminderMinutes: number = 0;
constructor(app: App, plugin: FocusTaskPlugin) { constructor(app: App, plugin: ImmersePlugin) {
super(app); super(app);
this.plugin = plugin; this.plugin = plugin;
this.estimatedMinutes = plugin.settings.defaultEstimateMinutes; this.estimatedMinutes = plugin.settings.defaultEstimateMinutes;
this.reminderMinutes = plugin.settings.defaultReminderMinutes;
if (plugin.settings.lists.length > 0) { if (plugin.settings.lists.length > 0) {
this.selectedList = plugin.settings.lists[0].id; this.selectedList = plugin.settings.lists[0].id;
} }
@@ -27,7 +31,7 @@ export class QuickAddTaskModal extends Modal {
onOpen() { onOpen() {
const { contentEl } = this; const { contentEl } = this;
contentEl.addClass('focus-task-modal'); contentEl.addClass('immerse-modal');
contentEl.createEl('h2', { text: '⚡ Add New Task' }); contentEl.createEl('h2', { text: '⚡ Add New Task' });
@@ -82,19 +86,68 @@ export class QuickAddTaskModal extends Modal {
dropdown.onChange(value => this.selectedList = value); dropdown.onChange(value => this.selectedList = value);
}); });
// Buttons // Scheduled date
const buttonContainer = contentEl.createEl('div', { cls: 'focus-task-modal-buttons' }); new Setting(contentEl)
.setName('📅 Scheduled Date')
.setDesc('Optional: When do you plan to work on this?')
.addText(text => {
text.setPlaceholder('YYYY-MM-DD')
.setValue(this.scheduledDate)
.onChange(value => this.scheduledDate = value);
text.inputEl.type = 'date';
});
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel', cls: 'focus-task-btn' }); // Scheduled time
new Setting(contentEl)
.setName('⏰ Scheduled Time')
.setDesc('Optional: What time?')
.addText(text => {
text.setPlaceholder('HH:mm')
.setValue(this.scheduledTime)
.onChange(value => this.scheduledTime = value);
text.inputEl.type = 'time';
});
// Reminder
if (this.plugin.settings.enableReminders) {
new Setting(contentEl)
.setName('🔔 Reminder')
.setDesc('Remind me before the scheduled time')
.addDropdown(dropdown => {
dropdown.addOption('0', 'No reminder');
dropdown.addOption('5', '5 minutes before');
dropdown.addOption('10', '10 minutes before');
dropdown.addOption('15', '15 minutes before');
dropdown.addOption('30', '30 minutes before');
dropdown.addOption('60', '1 hour before');
dropdown.setValue(this.reminderMinutes.toString());
dropdown.onChange(value => this.reminderMinutes = parseInt(value));
});
}
// Buttons
const buttonContainer = contentEl.createEl('div', { cls: 'immerse-modal-buttons' });
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel', cls: 'immerse-btn' });
cancelBtn.addEventListener('click', () => this.close()); cancelBtn.addEventListener('click', () => this.close());
const addBtn = buttonContainer.createEl('button', { text: 'Add Task', cls: 'focus-task-btn focus-task-btn-primary' }); const addBtn = buttonContainer.createEl('button', { text: 'Add Task', cls: 'immerse-btn immerse-btn-primary' });
addBtn.addEventListener('click', () => this.submitTask()); addBtn.addEventListener('click', () => this.submitTask());
} }
submitTask() { submitTask() {
if (this.taskText.trim()) { if (this.taskText.trim()) {
const task = this.plugin.createTask(this.taskText, this.estimatedMinutes, this.selectedList); const task = this.plugin.createTask(this.taskText, this.estimatedMinutes, this.selectedList);
// Add scheduling data if provided
if (this.scheduledDate) {
task.scheduledDate = this.scheduledDate;
}
if (this.scheduledTime) {
task.scheduledTime = this.scheduledTime;
}
if (this.reminderMinutes > 0 && this.scheduledDate && this.scheduledTime) {
task.reminderMinutes = this.reminderMinutes;
}
this.plugin.addTask(task); this.plugin.addTask(task);
new Notice('✅ Task added!'); new Notice('✅ Task added!');
this.close(); this.close();
@@ -112,10 +165,10 @@ export class QuickAddTaskModal extends Modal {
// ============ Edit Task Modal ============ // ============ Edit Task Modal ============
export class EditTaskModal extends Modal { export class EditTaskModal extends Modal {
plugin: FocusTaskPlugin; plugin: ImmersePlugin;
task: FocusTask; task: ImmerseTask;
constructor(app: App, plugin: FocusTaskPlugin, task: FocusTask) { constructor(app: App, plugin: ImmersePlugin, task: ImmerseTask) {
super(app); super(app);
this.plugin = plugin; this.plugin = plugin;
this.task = { ...task }; this.task = { ...task };
@@ -123,7 +176,7 @@ export class EditTaskModal extends Modal {
onOpen() { onOpen() {
const { contentEl } = this; const { contentEl } = this;
contentEl.addClass('focus-task-modal'); contentEl.addClass('immerse-modal');
contentEl.createEl('h2', { text: '✏️ Edit Task' }); contentEl.createEl('h2', { text: '✏️ Edit Task' });
@@ -177,6 +230,48 @@ export class EditTaskModal extends Modal {
textarea.inputEl.rows = 4; textarea.inputEl.rows = 4;
}); });
// Scheduled date
new Setting(contentEl)
.setName('📅 Scheduled Date')
.setDesc('Optional: When do you plan to work on this?')
.addText(text => {
text.setPlaceholder('YYYY-MM-DD')
.setValue(this.task.scheduledDate || '')
.onChange(value => this.task.scheduledDate = value || undefined);
text.inputEl.type = 'date';
});
// Scheduled time
new Setting(contentEl)
.setName('⏰ Scheduled Time')
.setDesc('Optional: What time?')
.addText(text => {
text.setPlaceholder('HH:mm')
.setValue(this.task.scheduledTime || '')
.onChange(value => this.task.scheduledTime = value || undefined);
text.inputEl.type = 'time';
});
// Reminder
if (this.plugin.settings.enableReminders) {
new Setting(contentEl)
.setName('🔔 Reminder')
.setDesc('Remind me before the scheduled time')
.addDropdown(dropdown => {
dropdown.addOption('0', 'No reminder');
dropdown.addOption('5', '5 minutes before');
dropdown.addOption('10', '10 minutes before');
dropdown.addOption('15', '15 minutes before');
dropdown.addOption('30', '30 minutes before');
dropdown.addOption('60', '1 hour before');
dropdown.setValue((this.task.reminderMinutes || 0).toString());
dropdown.onChange(value => {
const minutes = parseInt(value);
this.task.reminderMinutes = minutes > 0 ? minutes : undefined;
});
});
}
// Show actual time if task has been worked on // Show actual time if task has been worked on
if (this.task.actualMinutes > 0) { if (this.task.actualMinutes > 0) {
new Setting(contentEl) new Setting(contentEl)
@@ -184,12 +279,12 @@ export class EditTaskModal extends Modal {
.setDesc(`You've worked on this task for ${this.plugin.formatTimeHuman(this.task.actualMinutes)}`); .setDesc(`You've worked on this task for ${this.plugin.formatTimeHuman(this.task.actualMinutes)}`);
} }
const buttonContainer = contentEl.createEl('div', { cls: 'focus-task-modal-buttons' }); const buttonContainer = contentEl.createEl('div', { cls: 'immerse-modal-buttons' });
const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel', cls: 'focus-task-btn' }); const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel', cls: 'immerse-btn' });
cancelBtn.addEventListener('click', () => this.close()); cancelBtn.addEventListener('click', () => this.close());
const saveBtn = buttonContainer.createEl('button', { text: 'Save', cls: 'focus-task-btn focus-task-btn-primary' }); const saveBtn = buttonContainer.createEl('button', { text: 'Save', cls: 'immerse-btn immerse-btn-primary' });
saveBtn.addEventListener('click', () => { saveBtn.addEventListener('click', () => {
this.plugin.updateTask(this.task.id, this.task); this.plugin.updateTask(this.task.id, this.task);
new Notice('✅ Task updated!'); new Notice('✅ Task updated!');

View File

@@ -1,6 +1,6 @@
// ============ Types & Interfaces ============ // ============ Types & Interfaces ============
export interface FocusTask { export interface ImmerseTask {
id: string; id: string;
text: string; text: string;
completed: boolean; completed: boolean;
@@ -10,7 +10,9 @@ export interface FocusTask {
completedAt?: number; completedAt?: number;
list: string; list: string;
notes: string; notes: string;
scheduledDate?: string; scheduledDate?: string; // Date in YYYY-MM-DD format
scheduledTime?: string; // Time in HH:mm format (24-hour)
reminderMinutes?: number; // Minutes before scheduled time to remind (0 = no reminder)
isActive: boolean; isActive: boolean;
} }
@@ -21,7 +23,7 @@ export interface TaskList {
icon: string; icon: string;
} }
export interface FocusTaskSettings { export interface ImmerseSettings {
pomodoroWorkMinutes: number; pomodoroWorkMinutes: number;
pomodoroBreakMinutes: number; pomodoroBreakMinutes: number;
longBreakMinutes: number; longBreakMinutes: number;
@@ -34,10 +36,13 @@ export interface FocusTaskSettings {
tickSoundEnabled: boolean; tickSoundEnabled: boolean;
// Daily note logging // Daily note logging
logToDaily: boolean; logToDaily: boolean;
// Task reminders
enableReminders: boolean;
defaultReminderMinutes: number; // Default minutes before task to remind
} }
export interface FocusTaskData { export interface ImmerseData {
tasks: FocusTask[]; tasks: ImmerseTask[];
completedToday: number; completedToday: number;
totalFocusMinutesToday: number; totalFocusMinutesToday: number;
streak: number; streak: number;
@@ -45,7 +50,7 @@ export interface FocusTaskData {
pomodorosCompleted: number; pomodorosCompleted: number;
} }
export const DEFAULT_SETTINGS: FocusTaskSettings = { export const DEFAULT_SETTINGS: ImmerseSettings = {
pomodoroWorkMinutes: 25, pomodoroWorkMinutes: 25,
pomodoroBreakMinutes: 5, pomodoroBreakMinutes: 5,
longBreakMinutes: 15, longBreakMinutes: 15,
@@ -58,13 +63,16 @@ export const DEFAULT_SETTINGS: FocusTaskSettings = {
{ id: 'personal', name: 'Personal', color: '#22c55e', icon: '🏠' }, { id: 'personal', name: 'Personal', color: '#22c55e', icon: '🏠' },
{ id: 'learning', name: 'Learning', color: '#f59e0b', icon: '📚' }, { id: 'learning', name: 'Learning', color: '#f59e0b', icon: '📚' },
], ],
autoStartBreak: false, autoStartBreak: true,
tickSoundEnabled: false, tickSoundEnabled: false,
// Daily note logging // Daily note logging
logToDaily: false, logToDaily: false,
// Task reminders
enableReminders: true,
defaultReminderMinutes: 15,
}; };
export const DEFAULT_DATA: FocusTaskData = { export const DEFAULT_DATA: ImmerseData = {
tasks: [], tasks: [],
completedToday: 0, completedToday: 0,
totalFocusMinutesToday: 0, totalFocusMinutesToday: 0,
@@ -73,7 +81,7 @@ export const DEFAULT_DATA: FocusTaskData = {
pomodorosCompleted: 0, pomodorosCompleted: 0,
}; };
export const VIEW_TYPE_FOCUS_TASK = 'focus-task-view'; export const VIEW_TYPE_IMMERSE = 'immerse-view';
// ============ Celebration Messages ============ // ============ Celebration Messages ============

View File

@@ -3,14 +3,14 @@ import {
WorkspaceLeaf, WorkspaceLeaf,
} from 'obsidian'; } from 'obsidian';
import { VIEW_TYPE_FOCUS_TASK, FocusTask } from './types'; import { VIEW_TYPE_IMMERSE, ImmerseTask } from './types';
import { QuickAddTaskModal, EditTaskModal } from './modals'; import { QuickAddTaskModal, EditTaskModal } from './modals';
import FocusTaskPlugin from './main'; import ImmersePlugin from './main';
// ============ Main View ============ // ============ Main View ============
export class FocusTaskView extends ItemView { export class ImmerseView extends ItemView {
plugin: FocusTaskPlugin; plugin: ImmersePlugin;
currentFilter: string = 'all'; currentFilter: string = 'all';
// References to elements that need frequent updates // References to elements that need frequent updates
@@ -19,17 +19,17 @@ export class FocusTaskView extends ItemView {
private actualTimeEl: HTMLElement | null = null; private actualTimeEl: HTMLElement | null = null;
private pauseBtnEl: HTMLElement | null = null; private pauseBtnEl: HTMLElement | null = null;
constructor(leaf: WorkspaceLeaf, plugin: FocusTaskPlugin) { constructor(leaf: WorkspaceLeaf, plugin: ImmersePlugin) {
super(leaf); super(leaf);
this.plugin = plugin; this.plugin = plugin;
} }
getViewType(): string { getViewType(): string {
return VIEW_TYPE_FOCUS_TASK; return VIEW_TYPE_IMMERSE;
} }
getDisplayText(): string { getDisplayText(): string {
return 'Focus Task'; return 'Immerse';
} }
getIcon(): string { getIcon(): string {
@@ -74,7 +74,7 @@ export class FocusTaskView extends ItemView {
refresh() { refresh() {
const container = this.containerEl.children[1]; const container = this.containerEl.children[1];
container.empty(); container.empty();
container.addClass('focus-task-container'); container.addClass('immerse-container');
// Reset element references // Reset element references
this.timerTimeEl = null; this.timerTimeEl = null;
@@ -96,18 +96,18 @@ export class FocusTaskView extends ItemView {
} }
renderHeader(container: Element) { renderHeader(container: Element) {
const header = container.createEl('div', { cls: 'focus-task-header' }); const header = container.createEl('div', { cls: 'immerse-header' });
const titleSection = header.createEl('div', { cls: 'focus-task-title-section' }); const titleSection = header.createEl('div', { cls: 'immerse-title-section' });
titleSection.createEl('h2', { text: '⚡ Focus Task', cls: 'focus-task-title' }); titleSection.createEl('h2', { text: '⚡ Immerse', cls: 'immerse-title' });
const today = new Date(); const today = new Date();
const dateStr = today.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' }); const dateStr = today.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' });
titleSection.createEl('div', { text: dateStr, cls: 'focus-task-date' }); titleSection.createEl('div', { text: dateStr, cls: 'immerse-date' });
const actions = header.createEl('div', { cls: 'focus-task-header-actions' }); const actions = header.createEl('div', { cls: 'immerse-header-actions' });
const addBtn = actions.createEl('button', { cls: 'focus-task-btn focus-task-btn-primary' }); const addBtn = actions.createEl('button', { cls: 'immerse-btn immerse-btn-primary' });
addBtn.innerHTML = '+ Add Task'; addBtn.innerHTML = '+ Add Task';
addBtn.addEventListener('click', () => { addBtn.addEventListener('click', () => {
new QuickAddTaskModal(this.app, this.plugin).open(); new QuickAddTaskModal(this.app, this.plugin).open();
@@ -116,7 +116,7 @@ export class FocusTaskView extends ItemView {
renderStatsBar(container: Element) { renderStatsBar(container: Element) {
const stats = this.plugin.getStats(); const stats = this.plugin.getStats();
const statsBar = container.createEl('div', { cls: 'focus-task-stats-bar' }); const statsBar = container.createEl('div', { cls: 'immerse-stats-bar' });
const statItems = [ const statItems = [
{ label: 'Pending', value: stats.pendingCount.toString(), icon: '📋' }, { label: 'Pending', value: stats.pendingCount.toString(), icon: '📋' },
@@ -126,42 +126,44 @@ export class FocusTaskView extends ItemView {
]; ];
statItems.forEach(stat => { statItems.forEach(stat => {
const item = statsBar.createEl('div', { cls: 'focus-task-stat-item' }); const item = statsBar.createEl('div', { cls: 'immerse-stat-item' });
item.createEl('div', { cls: 'focus-task-stat-icon', text: stat.icon }); item.createEl('div', { cls: 'immerse-stat-icon', text: stat.icon });
item.createEl('div', { cls: 'focus-task-stat-value', text: stat.value }); item.createEl('div', { cls: 'immerse-stat-value', text: stat.value });
item.createEl('div', { cls: 'focus-task-stat-label', text: stat.label }); item.createEl('div', { cls: 'immerse-stat-label', text: stat.label });
}); });
} }
renderActiveTask(container: Element) { renderActiveTask(container: Element) {
const activeSection = container.createEl('div', { cls: 'focus-task-active-section' }); const activeSection = container.createEl('div', { cls: 'immerse-active-section' });
if (this.plugin.activeTaskId) { if (this.plugin.activeTaskId) {
const task = this.plugin.data.tasks.find(t => t.id === this.plugin.activeTaskId); const task = this.plugin.data.tasks.find(t => t.id === this.plugin.activeTaskId);
if (task) { if (task) {
activeSection.addClass('focus-task-has-active'); activeSection.addClass('immerse-has-active');
const activeCard = activeSection.createEl('div', { cls: 'focus-task-active-card' }); const activeCard = activeSection.createEl('div', { cls: 'immerse-active-card' });
if (this.plugin.isBreakMode) { if (this.plugin.isBreakMode) {
activeCard.addClass('focus-task-break-card'); activeCard.addClass('immerse-break-card');
activeCard.createEl('div', { cls: 'focus-task-active-label', text: '☕ BREAK TIME' }); const breakLabel = this.plugin.currentTimerSeconds > 0 ? '☕ BREAK TIME' : '✨ BREAK COMPLETE';
activeCard.createEl('div', { cls: 'immerse-active-label', text: breakLabel });
} else { } else {
activeCard.createEl('div', { cls: 'focus-task-active-label', text: '🎯 FOCUSING ON' }); const workLabel = this.plugin.currentTimerSeconds > 0 ? '🎯 FOCUSING ON' : '🍅 POMODORO COMPLETE';
activeCard.createEl('div', { cls: 'immerse-active-label', text: workLabel });
} }
activeCard.createEl('div', { cls: 'focus-task-active-task-name', text: task.text }); activeCard.createEl('div', { cls: 'immerse-active-task-name', text: task.text });
// Timer display - store reference for updates // Timer display - store reference for updates
const timerDisplay = activeCard.createEl('div', { cls: 'focus-task-timer-display' }); const timerDisplay = activeCard.createEl('div', { cls: 'immerse-timer-display' });
this.timerTimeEl = timerDisplay.createEl('span', { this.timerTimeEl = timerDisplay.createEl('span', {
cls: 'focus-task-timer-time', cls: 'immerse-timer-time',
text: this.plugin.formatTime(this.plugin.currentTimerSeconds) text: this.plugin.formatTime(this.plugin.currentTimerSeconds)
}); });
// Progress bar - store reference for updates // Progress bar - store reference for updates
const progressWrap = activeCard.createEl('div', { cls: 'focus-task-progress-wrap' }); const progressWrap = activeCard.createEl('div', { cls: 'immerse-progress-wrap' });
this.progressBarEl = progressWrap.createEl('div', { cls: 'focus-task-progress-bar' }); this.progressBarEl = progressWrap.createEl('div', { cls: 'immerse-progress-bar' });
let progressPercent = 0; let progressPercent = 0;
if (this.plugin.isBreakMode) { if (this.plugin.isBreakMode) {
@@ -175,56 +177,104 @@ export class FocusTaskView extends ItemView {
} }
this.progressBarEl.style.width = `${Math.min(Math.max(progressPercent, 0), 100)}%`; this.progressBarEl.style.width = `${Math.min(Math.max(progressPercent, 0), 100)}%`;
if (progressPercent >= 100) this.progressBarEl.addClass('focus-task-overtime'); if (progressPercent >= 100) this.progressBarEl.addClass('immerse-overtime');
// Time info - store reference for actual time updates // Time info - store reference for actual time updates
if (!this.plugin.isBreakMode) { if (!this.plugin.isBreakMode) {
const timeInfo = activeCard.createEl('div', { cls: 'focus-task-time-info' }); const timeInfo = activeCard.createEl('div', { cls: 'immerse-time-info' });
timeInfo.createEl('span', { text: `Est: ${this.plugin.formatTimeHuman(task.estimatedMinutes)}` }); timeInfo.createEl('span', { text: `Est: ${this.plugin.formatTimeHuman(task.estimatedMinutes)}` });
this.actualTimeEl = timeInfo.createEl('span', { text: `Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}` }); this.actualTimeEl = timeInfo.createEl('span', { text: `Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}` });
} }
// Controls // Controls
const controls = activeCard.createEl('div', { cls: 'focus-task-active-controls' }); const controls = activeCard.createEl('div', { cls: 'immerse-active-controls' });
this.pauseBtnEl = controls.createEl('button', { cls: 'focus-task-btn focus-task-btn-secondary' }); if (this.plugin.isBreakMode) {
// Break mode controls
if (this.plugin.currentTimerSeconds > 0) {
// Break is still counting down
this.pauseBtnEl = controls.createEl('button', { cls: 'immerse-btn immerse-btn-secondary' });
this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? '⏸ Pause' : '▶ Resume'; this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? '⏸ Pause' : '▶ Resume';
this.pauseBtnEl.addEventListener('click', () => this.plugin.toggleTimer()); this.pauseBtnEl.addEventListener('click', () => this.plugin.toggleTimer());
if (!this.plugin.isBreakMode) { const skipBreakBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-primary' });
const completeBtn = controls.createEl('button', { cls: 'focus-task-btn focus-task-btn-success' });
completeBtn.innerHTML = '✓ Complete';
completeBtn.addEventListener('click', () => this.plugin.completeTask(task.id));
}
const stopBtn = controls.createEl('button', { cls: 'focus-task-btn focus-task-btn-danger' });
stopBtn.innerHTML = '✕ Stop';
stopBtn.addEventListener('click', () => this.plugin.stopTimer());
if (this.plugin.isBreakMode) {
const skipBreakBtn = controls.createEl('button', { cls: 'focus-task-btn' });
skipBreakBtn.innerHTML = '⏭ Skip Break'; skipBreakBtn.innerHTML = '⏭ Skip Break';
skipBreakBtn.addEventListener('click', () => { skipBreakBtn.addEventListener('click', () => {
this.plugin.isBreakMode = false; this.plugin.isBreakMode = false;
this.plugin.stopTimer(); this.plugin.startPomodoro(task.id);
this.refresh();
}); });
const stopBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-danger' });
stopBtn.innerHTML = '✕ Stop';
stopBtn.addEventListener('click', () => {
this.plugin.isBreakMode = false;
this.plugin.stopTimer();
});
} else {
// Break timer finished - show resume button
const resumeWorkBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-success' });
resumeWorkBtn.innerHTML = '▶ Resume Work';
resumeWorkBtn.addEventListener('click', () => {
this.plugin.isBreakMode = false;
this.plugin.startPomodoro(task.id);
});
const stopBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-danger' });
stopBtn.innerHTML = '✕ Stop';
stopBtn.addEventListener('click', () => {
this.plugin.isBreakMode = false;
this.plugin.stopTimer();
});
}
} else {
// Work mode controls
if (this.plugin.currentTimerSeconds > 0) {
// Work session still running
this.pauseBtnEl = controls.createEl('button', { cls: 'immerse-btn immerse-btn-secondary' });
this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? '⏸ Pause' : '▶ Resume';
this.pauseBtnEl.addEventListener('click', () => this.plugin.toggleTimer());
const completeBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-success' });
completeBtn.innerHTML = '✓ Complete';
completeBtn.addEventListener('click', () => this.plugin.completeTask(task.id));
const stopBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-danger' });
stopBtn.innerHTML = '✕ Stop';
stopBtn.addEventListener('click', () => this.plugin.stopTimer());
} else {
// Work session finished - show break and completion options
const startBreakBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-secondary' });
startBreakBtn.innerHTML = '☕ Start Break';
startBreakBtn.addEventListener('click', () => this.plugin.startBreak());
const continueBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-primary' });
continueBtn.innerHTML = '▶ Continue Working';
continueBtn.addEventListener('click', () => this.plugin.startPomodoro(task.id));
const completeBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-success' });
completeBtn.innerHTML = '✓ Complete';
completeBtn.addEventListener('click', () => this.plugin.completeTask(task.id));
const stopBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-danger' });
stopBtn.innerHTML = '✕ Stop';
stopBtn.addEventListener('click', () => this.plugin.stopTimer());
}
} }
} }
} else { } else {
// No active task - show start focus prompt // No active task - show start focus prompt
const startPrompt = activeSection.createEl('div', { cls: 'focus-task-start-prompt' }); const startPrompt = activeSection.createEl('div', { cls: 'immerse-start-prompt' });
startPrompt.createEl('div', { cls: 'focus-task-prompt-icon', text: '⚡' }); startPrompt.createEl('div', { cls: 'immerse-prompt-icon', text: '⚡' });
startPrompt.createEl('div', { cls: 'focus-task-prompt-text', text: 'Ready to focus?' }); startPrompt.createEl('div', { cls: 'immerse-prompt-text', text: 'Ready to focus?' });
startPrompt.createEl('div', { cls: 'focus-task-prompt-hint', text: 'Click ▶ on a task to start a Pomodoro session' }); startPrompt.createEl('div', { cls: 'immerse-prompt-hint', text: 'Click ▶ on a task to start a Pomodoro session' });
} }
} }
renderTaskList(container: Element) { renderTaskList(container: Element) {
const listSection = container.createEl('div', { cls: 'focus-task-list-section' }); const listSection = container.createEl('div', { cls: 'immerse-list-section' });
// Filters // Filters
const filters = listSection.createEl('div', { cls: 'focus-task-filters' }); const filters = listSection.createEl('div', { cls: 'immerse-filters' });
const filterOptions = [ const filterOptions = [
{ id: 'all', label: 'All Tasks' }, { id: 'all', label: 'All Tasks' },
@@ -235,7 +285,7 @@ export class FocusTaskView extends ItemView {
filterOptions.forEach(opt => { filterOptions.forEach(opt => {
const btn = filters.createEl('button', { const btn = filters.createEl('button', {
cls: `focus-task-filter-btn ${this.currentFilter === opt.id ? 'active' : ''}`, cls: `immerse-filter-btn ${this.currentFilter === opt.id ? 'active' : ''}`,
text: opt.label, text: opt.label,
}); });
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
@@ -245,7 +295,7 @@ export class FocusTaskView extends ItemView {
}); });
// Task items // Task items
const taskList = listSection.createEl('div', { cls: 'focus-task-task-list' }); const taskList = listSection.createEl('div', { cls: 'immerse-task-list' });
let tasks = this.plugin.data.tasks; let tasks = this.plugin.data.tasks;
@@ -265,24 +315,28 @@ export class FocusTaskView extends ItemView {
}); });
if (tasks.length === 0) { if (tasks.length === 0) {
const emptyState = taskList.createEl('div', { cls: 'focus-task-empty-state' }); const emptyState = taskList.createEl('div', { cls: 'immerse-empty-state' });
emptyState.createEl('div', { cls: 'focus-task-empty-icon', text: '📝' }); emptyState.createEl('div', { cls: 'immerse-empty-icon', text: '📝' });
emptyState.createEl('div', { cls: 'focus-task-empty-text', text: 'No tasks yet' }); emptyState.createEl('div', { cls: 'immerse-empty-text', text: 'No tasks yet' });
emptyState.createEl('div', { cls: 'focus-task-empty-hint', text: 'Add a task to get started!' }); emptyState.createEl('div', { cls: 'immerse-empty-hint', text: 'Add a task to get started!' });
} else { } else {
tasks.forEach(task => this.renderTaskItem(taskList, task)); tasks.forEach(task => this.renderTaskItem(taskList, task));
} }
} }
renderTaskItem(container: Element, task: FocusTask) { renderTaskItem(container: Element, task: ImmerseTask) {
const list = this.plugin.settings.lists.find(l => l.id === task.list); const list = this.plugin.settings.lists.find(l => l.id === task.list);
// Check if task is overdue
const isOverdue = !task.completed && task.scheduledDate && task.scheduledTime &&
new Date(`${task.scheduledDate}T${task.scheduledTime}`).getTime() < Date.now();
const taskEl = container.createEl('div', { const taskEl = container.createEl('div', {
cls: `focus-task-task-item ${task.completed ? 'completed' : ''} ${task.isActive ? 'active' : ''}` cls: `immerse-task-item ${task.completed ? 'completed' : ''} ${task.isActive ? 'active' : ''} ${isOverdue ? 'overdue' : ''}`
}); });
// Checkbox // Checkbox
const checkbox = taskEl.createEl('div', { cls: 'focus-task-checkbox' }); const checkbox = taskEl.createEl('div', { cls: 'immerse-checkbox' });
checkbox.innerHTML = task.completed ? '✓' : ''; checkbox.innerHTML = task.completed ? '✓' : '';
checkbox.style.borderColor = list?.color || '#6366f1'; checkbox.style.borderColor = list?.color || '#6366f1';
if (task.completed) { if (task.completed) {
@@ -297,37 +351,54 @@ export class FocusTaskView extends ItemView {
}); });
// Task content // Task content
const content = taskEl.createEl('div', { cls: 'focus-task-task-content' }); const content = taskEl.createEl('div', { cls: 'immerse-task-content' });
const taskHeader = content.createEl('div', { cls: 'focus-task-task-header' }); const taskHeader = content.createEl('div', { cls: 'immerse-task-header' });
taskHeader.createEl('span', { cls: 'focus-task-task-text', text: task.text }); taskHeader.createEl('span', { cls: 'immerse-task-text', text: task.text });
if (list) { if (list) {
const listBadge = taskHeader.createEl('span', { const listBadge = taskHeader.createEl('span', {
cls: 'focus-task-list-badge', cls: 'immerse-list-badge',
text: `${list.icon} ${list.name}`, text: `${list.icon} ${list.name}`,
}); });
listBadge.style.backgroundColor = list.color + '20'; listBadge.style.backgroundColor = list.color + '20';
listBadge.style.color = list.color; listBadge.style.color = list.color;
} }
const taskMeta = content.createEl('div', { cls: 'focus-task-task-meta' }); const taskMeta = content.createEl('div', { cls: 'immerse-task-meta' });
taskMeta.createEl('span', { text: `Est: ${this.plugin.formatTimeHuman(task.estimatedMinutes)}` }); taskMeta.createEl('span', { text: `Est: ${this.plugin.formatTimeHuman(task.estimatedMinutes)}` });
if (task.actualMinutes > 0) { if (task.actualMinutes > 0) {
const actualSpan = taskMeta.createEl('span'); const actualSpan = taskMeta.createEl('span');
actualSpan.setText(`Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}`); actualSpan.setText(`Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}`);
if (task.actualMinutes > task.estimatedMinutes) { if (task.actualMinutes > task.estimatedMinutes) {
actualSpan.addClass('focus-task-overtime-text'); actualSpan.addClass('immerse-overtime-text');
}
}
// Show scheduled date/time if set
if (task.scheduledDate) {
const scheduleSpan = taskMeta.createEl('span', {
cls: `immerse-schedule-badge ${isOverdue ? 'overdue' : ''}`
});
const dateStr = task.scheduledDate;
const timeStr = task.scheduledTime || '';
if (isOverdue) {
scheduleSpan.setText(`⚠️ OVERDUE: ${dateStr}${timeStr ? ' ' + timeStr : ''}`);
} else {
scheduleSpan.setText(`📅 ${dateStr}${timeStr ? ' ' + timeStr : ''}`);
}
if (task.reminderMinutes) {
scheduleSpan.title = `Reminder set for ${task.reminderMinutes} min before`;
} }
} }
// Actions // Actions
const actions = taskEl.createEl('div', { cls: 'focus-task-task-actions' }); const actions = taskEl.createEl('div', { cls: 'immerse-task-actions' });
if (!task.completed) { if (!task.completed) {
// Start pomodoro button // Start pomodoro button
const startBtn = actions.createEl('button', { cls: 'focus-task-task-btn', attr: { 'aria-label': 'Start Pomodoro' } }); const startBtn = actions.createEl('button', { cls: 'immerse-task-btn', attr: { 'aria-label': 'Start Pomodoro' } });
startBtn.innerHTML = '▶'; startBtn.innerHTML = '▶';
startBtn.addEventListener('click', (e) => { startBtn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -335,7 +406,7 @@ export class FocusTaskView extends ItemView {
}); });
// Stopwatch mode button // Stopwatch mode button
const stopwatchBtn = actions.createEl('button', { cls: 'focus-task-task-btn', attr: { 'aria-label': 'Start Stopwatch' } }); const stopwatchBtn = actions.createEl('button', { cls: 'immerse-task-btn', attr: { 'aria-label': 'Start Stopwatch' } });
stopwatchBtn.innerHTML = '⏱'; stopwatchBtn.innerHTML = '⏱';
stopwatchBtn.addEventListener('click', (e) => { stopwatchBtn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -344,7 +415,7 @@ export class FocusTaskView extends ItemView {
} }
// Edit button // Edit button
const editBtn = actions.createEl('button', { cls: 'focus-task-task-btn', attr: { 'aria-label': 'Edit' } }); const editBtn = actions.createEl('button', { cls: 'immerse-task-btn', attr: { 'aria-label': 'Edit' } });
editBtn.innerHTML = '✏️'; editBtn.innerHTML = '✏️';
editBtn.addEventListener('click', (e) => { editBtn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -352,7 +423,7 @@ export class FocusTaskView extends ItemView {
}); });
// Delete button // Delete button
const deleteBtn = actions.createEl('button', { cls: 'focus-task-task-btn focus-task-delete-btn', attr: { 'aria-label': 'Delete' } }); const deleteBtn = actions.createEl('button', { cls: 'immerse-task-btn immerse-delete-btn', attr: { 'aria-label': 'Delete' } });
deleteBtn.innerHTML = '🗑'; deleteBtn.innerHTML = '🗑';
deleteBtn.addEventListener('click', (e) => { deleteBtn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();

View File

@@ -1,10 +1,10 @@
/* ============================================ /* ============================================
FOCUS TASK - Obsidian Plugin Styles IMMERSE - Obsidian Plugin Styles
Inspired by https://www.blitzit.app/ Inspired by https://www.blitzit.app/
============================================ */ ============================================ */
/* ============ CSS Variables ============ */ /* ============ CSS Variables ============ */
.focus-task-container { .immerse-container {
--ft-primary: #6366f1; --ft-primary: #6366f1;
--ft-primary-light: #818cf8; --ft-primary-light: #818cf8;
--ft-primary-dark: #4f46e5; --ft-primary-dark: #4f46e5;
@@ -27,7 +27,7 @@
} }
/* ============ Container ============ */ /* ============ Container ============ */
.focus-task-container { .immerse-container {
padding: 16px; padding: 16px;
font-family: var(--font-interface); font-family: var(--font-interface);
overflow-y: auto; overflow-y: auto;
@@ -35,7 +35,7 @@
} }
/* ============ Header ============ */ /* ============ Header ============ */
.focus-task-header { .immerse-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@@ -44,13 +44,13 @@
border-bottom: 1px solid var(--ft-border); border-bottom: 1px solid var(--ft-border);
} }
.focus-task-title-section { .immerse-title-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
} }
.focus-task-title { .immerse-title {
margin: 0; margin: 0;
font-size: 24px; font-size: 24px;
font-weight: 700; font-weight: 700;
@@ -60,18 +60,18 @@
background-clip: text; background-clip: text;
} }
.focus-task-date { .immerse-date {
color: var(--ft-text-muted); color: var(--ft-text-muted);
font-size: 13px; font-size: 13px;
} }
.focus-task-header-actions { .immerse-header-actions {
display: flex; display: flex;
gap: 8px; gap: 8px;
} }
/* ============ Buttons ============ */ /* ============ Buttons ============ */
.focus-task-btn { .immerse-btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -87,48 +87,48 @@
color: var(--ft-text); color: var(--ft-text);
} }
.focus-task-btn:hover { .immerse-btn:hover {
background: var(--ft-border); background: var(--ft-border);
transform: translateY(-1px); transform: translateY(-1px);
} }
.focus-task-btn-primary { .immerse-btn-primary {
background: linear-gradient(135deg, var(--ft-primary) 0%, var(--ft-primary-dark) 100%); background: linear-gradient(135deg, var(--ft-primary) 0%, var(--ft-primary-dark) 100%);
color: white; color: white;
} }
.focus-task-btn-primary:hover { .immerse-btn-primary:hover {
background: linear-gradient(135deg, var(--ft-primary-light) 0%, var(--ft-primary) 100%); background: linear-gradient(135deg, var(--ft-primary-light) 0%, var(--ft-primary) 100%);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
} }
.focus-task-btn-success { .immerse-btn-success {
background: var(--ft-success); background: var(--ft-success);
color: white; color: white;
} }
.focus-task-btn-success:hover { .immerse-btn-success:hover {
background: #16a34a; background: #16a34a;
} }
.focus-task-btn-secondary { .immerse-btn-secondary {
background: var(--ft-bg-secondary); background: var(--ft-bg-secondary);
border: 1px solid var(--ft-border); border: 1px solid var(--ft-border);
} }
.focus-task-btn-danger { .immerse-btn-danger {
background: transparent; background: transparent;
color: var(--ft-danger); color: var(--ft-danger);
border: 1px solid var(--ft-danger); border: 1px solid var(--ft-danger);
} }
.focus-task-btn-danger:hover { .immerse-btn-danger:hover {
background: var(--ft-danger); background: var(--ft-danger);
color: white; color: white;
} }
/* ============ Stats Bar ============ */ /* ============ Stats Bar ============ */
.focus-task-stats-bar { .immerse-stats-bar {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 12px; gap: 12px;
@@ -136,12 +136,12 @@
} }
@media (max-width: 600px) { @media (max-width: 600px) {
.focus-task-stats-bar { .immerse-stats-bar {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
} }
.focus-task-stat-item { .immerse-stat-item {
background: var(--ft-bg-secondary); background: var(--ft-bg-secondary);
border-radius: var(--ft-radius); border-radius: var(--ft-radius);
padding: 12px; padding: 12px;
@@ -149,23 +149,23 @@
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.focus-task-stat-item:hover { .immerse-stat-item:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: var(--ft-shadow); box-shadow: var(--ft-shadow);
} }
.focus-task-stat-icon { .immerse-stat-icon {
font-size: 20px; font-size: 20px;
margin-bottom: 4px; margin-bottom: 4px;
} }
.focus-task-stat-value { .immerse-stat-value {
font-size: 18px; font-size: 18px;
font-weight: 700; font-weight: 700;
color: var(--ft-primary); color: var(--ft-primary);
} }
.focus-task-stat-label { .immerse-stat-label {
font-size: 11px; font-size: 11px;
color: var(--ft-text-muted); color: var(--ft-text-muted);
text-transform: uppercase; text-transform: uppercase;
@@ -173,11 +173,11 @@
} }
/* ============ Active Task Section ============ */ /* ============ Active Task Section ============ */
.focus-task-active-section { .immerse-active-section {
margin-bottom: 24px; margin-bottom: 24px;
} }
.focus-task-start-prompt { .immerse-start-prompt {
background: linear-gradient(135deg, var(--ft-bg-secondary) 0%, var(--ft-bg-tertiary) 100%); background: linear-gradient(135deg, var(--ft-bg-secondary) 0%, var(--ft-bg-tertiary) 100%);
border: 2px dashed var(--ft-border); border: 2px dashed var(--ft-border);
border-radius: var(--ft-radius); border-radius: var(--ft-radius);
@@ -185,7 +185,7 @@
text-align: center; text-align: center;
} }
.focus-task-prompt-icon { .immerse-prompt-icon {
font-size: 48px; font-size: 48px;
margin-bottom: 12px; margin-bottom: 12px;
animation: ft-pulse 2s ease-in-out infinite; animation: ft-pulse 2s ease-in-out infinite;
@@ -196,20 +196,20 @@
50% { transform: scale(1.1); opacity: 0.8; } 50% { transform: scale(1.1); opacity: 0.8; }
} }
.focus-task-prompt-text { .immerse-prompt-text {
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: var(--ft-text); color: var(--ft-text);
margin-bottom: 4px; margin-bottom: 4px;
} }
.focus-task-prompt-hint { .immerse-prompt-hint {
font-size: 14px; font-size: 14px;
color: var(--ft-text-muted); color: var(--ft-text-muted);
} }
/* ============ Active Task Card ============ */ /* ============ Active Task Card ============ */
.focus-task-active-card { .immerse-active-card {
background: linear-gradient(135deg, var(--ft-primary) 0%, var(--ft-primary-dark) 100%); background: linear-gradient(135deg, var(--ft-primary) 0%, var(--ft-primary-dark) 100%);
border-radius: var(--ft-radius); border-radius: var(--ft-radius);
padding: 24px; padding: 24px;
@@ -217,11 +217,11 @@
box-shadow: var(--ft-shadow-lg); box-shadow: var(--ft-shadow-lg);
} }
.focus-task-break-card { .immerse-break-card {
background: linear-gradient(135deg, var(--ft-break) 0%, #0891b2 100%); background: linear-gradient(135deg, var(--ft-break) 0%, #0891b2 100%);
} }
.focus-task-active-label { .immerse-active-label {
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
@@ -230,31 +230,31 @@
margin-bottom: 8px; margin-bottom: 8px;
} }
.focus-task-active-task-name { .immerse-active-task-name {
font-size: 20px; font-size: 20px;
font-weight: 700; font-weight: 700;
margin-bottom: 16px; margin-bottom: 16px;
word-break: break-word; word-break: break-word;
} }
.focus-task-timer-display { .immerse-timer-display {
text-align: center; text-align: center;
margin-bottom: 16px; margin-bottom: 16px;
} }
.focus-task-timer-time { .immerse-timer-time {
font-size: 48px; font-size: 48px;
font-weight: 700; font-weight: 700;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
letter-spacing: 2px; letter-spacing: 2px;
} }
.focus-task-timer-time.focus-task-overtime { .immerse-timer-time.immerse-overtime {
color: var(--ft-warning); color: var(--ft-warning);
} }
/* Progress Bar */ /* Progress Bar */
.focus-task-progress-wrap { .immerse-progress-wrap {
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
border-radius: 999px; border-radius: 999px;
height: 8px; height: 8px;
@@ -262,18 +262,18 @@
margin-bottom: 12px; margin-bottom: 12px;
} }
.focus-task-progress-bar { .immerse-progress-bar {
height: 100%; height: 100%;
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
border-radius: 999px; border-radius: 999px;
transition: width 0.3s ease; transition: width 0.3s ease;
} }
.focus-task-progress-bar.focus-task-overtime { .immerse-progress-bar.immerse-overtime {
background: var(--ft-warning); background: var(--ft-warning);
} }
.focus-task-time-info { .immerse-time-info {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
font-size: 13px; font-size: 13px;
@@ -281,13 +281,13 @@
margin-bottom: 16px; margin-bottom: 16px;
} }
.focus-task-active-controls { .immerse-active-controls {
display: flex; display: flex;
gap: 8px; gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
} }
.focus-task-active-controls .focus-task-btn { .immerse-active-controls .immerse-btn {
flex: 1; flex: 1;
min-width: 80px; min-width: 80px;
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
@@ -295,33 +295,33 @@
border: none; border: none;
} }
.focus-task-active-controls .focus-task-btn:hover { .immerse-active-controls .immerse-btn:hover {
background: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.3);
} }
.focus-task-active-controls .focus-task-btn-success { .immerse-active-controls .immerse-btn-success {
background: var(--ft-success); background: var(--ft-success);
} }
.focus-task-active-controls .focus-task-btn-danger { .immerse-active-controls .immerse-btn-danger {
background: transparent; background: transparent;
border: 1px solid rgba(255, 255, 255, 0.4); border: 1px solid rgba(255, 255, 255, 0.4);
} }
/* ============ Task List Section ============ */ /* ============ Task List Section ============ */
.focus-task-list-section { .immerse-list-section {
flex: 1; flex: 1;
} }
/* Filters */ /* Filters */
.focus-task-filters { .immerse-filters {
display: flex; display: flex;
gap: 8px; gap: 8px;
margin-bottom: 16px; margin-bottom: 16px;
flex-wrap: wrap; flex-wrap: wrap;
} }
.focus-task-filter-btn { .immerse-filter-btn {
padding: 6px 12px; padding: 6px 12px;
border-radius: 999px; border-radius: 999px;
font-size: 13px; font-size: 13px;
@@ -332,49 +332,49 @@
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.focus-task-filter-btn:hover { .immerse-filter-btn:hover {
background: var(--ft-bg-tertiary); background: var(--ft-bg-tertiary);
color: var(--ft-text); color: var(--ft-text);
} }
.focus-task-filter-btn.active { .immerse-filter-btn.active {
background: var(--ft-primary); background: var(--ft-primary);
border-color: var(--ft-primary); border-color: var(--ft-primary);
color: white; color: white;
} }
/* Task List */ /* Task List */
.focus-task-task-list { .immerse-task-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
} }
/* Empty State */ /* Empty State */
.focus-task-empty-state { .immerse-empty-state {
text-align: center; text-align: center;
padding: 40px 20px; padding: 40px 20px;
color: var(--ft-text-muted); color: var(--ft-text-muted);
} }
.focus-task-empty-icon { .immerse-empty-icon {
font-size: 48px; font-size: 48px;
margin-bottom: 12px; margin-bottom: 12px;
opacity: 0.5; opacity: 0.5;
} }
.focus-task-empty-text { .immerse-empty-text {
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
margin-bottom: 4px; margin-bottom: 4px;
} }
.focus-task-empty-hint { .immerse-empty-hint {
font-size: 14px; font-size: 14px;
} }
/* ============ Task Item ============ */ /* ============ Task Item ============ */
.focus-task-task-item { .immerse-task-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
@@ -385,27 +385,27 @@
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.focus-task-task-item:hover { .immerse-task-item:hover {
border-color: var(--ft-primary); border-color: var(--ft-primary);
box-shadow: var(--ft-shadow); box-shadow: var(--ft-shadow);
transform: translateX(4px); transform: translateX(4px);
} }
.focus-task-task-item.active { .immerse-task-item.active {
border-color: var(--ft-primary); border-color: var(--ft-primary);
background: linear-gradient(90deg, rgba(99, 102, 241, 0.1) 0%, var(--ft-bg-secondary) 100%); background: linear-gradient(90deg, rgba(99, 102, 241, 0.1) 0%, var(--ft-bg-secondary) 100%);
} }
.focus-task-task-item.completed { .immerse-task-item.completed {
opacity: 0.6; opacity: 0.6;
} }
.focus-task-task-item.completed .focus-task-task-text { .immerse-task-item.completed .immerse-task-text {
text-decoration: line-through; text-decoration: line-through;
} }
/* Checkbox */ /* Checkbox */
.focus-task-checkbox { .immerse-checkbox {
width: 24px; width: 24px;
height: 24px; height: 24px;
min-width: 24px; min-width: 24px;
@@ -420,18 +420,18 @@
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.focus-task-checkbox:hover { .immerse-checkbox:hover {
transform: scale(1.1); transform: scale(1.1);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2); box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
} }
/* Task Content */ /* Task Content */
.focus-task-task-content { .immerse-task-content {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.focus-task-task-header { .immerse-task-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
@@ -439,44 +439,71 @@
margin-bottom: 4px; margin-bottom: 4px;
} }
.focus-task-task-text { .immerse-task-text {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: var(--ft-text); color: var(--ft-text);
word-break: break-word; word-break: break-word;
} }
.focus-task-list-badge { .immerse-list-badge {
font-size: 11px; font-size: 11px;
padding: 2px 8px; padding: 2px 8px;
border-radius: 999px; border-radius: 999px;
font-weight: 500; font-weight: 500;
} }
.focus-task-task-meta { .immerse-task-meta {
display: flex; display: flex;
gap: 12px; gap: 12px;
font-size: 12px; font-size: 12px;
color: var(--ft-text-muted); color: var(--ft-text-muted);
} }
.focus-task-overtime-text { .immerse-overtime-text {
color: var(--ft-warning); color: var(--ft-warning);
} }
.immerse-schedule-badge {
padding: 2px 8px;
background: var(--ft-primary);
color: white;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.immerse-schedule-badge.overdue {
background: var(--ft-danger);
animation: pulse-overdue 2s ease-in-out infinite;
}
@keyframes pulse-overdue {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
.immerse-task-item.overdue {
border-left: 3px solid var(--ft-danger);
}
/* Task Actions */ /* Task Actions */
.focus-task-task-actions { .immerse-task-actions {
display: flex; display: flex;
gap: 4px; gap: 4px;
opacity: 0; opacity: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
} }
.focus-task-task-item:hover .focus-task-task-actions { .immerse-task-item:hover .immerse-task-actions {
opacity: 1; opacity: 1;
} }
.focus-task-task-btn { .immerse-task-btn {
width: 32px; width: 32px;
height: 32px; height: 32px;
border-radius: 6px; border-radius: 6px;
@@ -490,18 +517,18 @@
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.focus-task-task-btn:hover { .immerse-task-btn:hover {
background: var(--ft-primary); background: var(--ft-primary);
color: white; color: white;
transform: scale(1.1); transform: scale(1.1);
} }
.focus-task-delete-btn:hover { .immerse-delete-btn:hover {
background: var(--ft-danger); background: var(--ft-danger);
} }
/* ============ Status Bar Timer ============ */ /* ============ Status Bar Timer ============ */
.focus-task-status-bar { .immerse-status-bar {
cursor: pointer; cursor: pointer;
padding: 0 8px; padding: 0 8px;
display: flex; display: flex;
@@ -511,26 +538,26 @@
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.focus-task-status-bar:hover { .immerse-status-bar:hover {
color: var(--text-accent); color: var(--text-accent);
} }
.focus-task-status-bar.focus-task-status-active { .immerse-status-bar.immerse-status-active {
color: var(--text-accent); color: var(--text-accent);
font-weight: 500; font-weight: 500;
} }
/* ============ Modal Styles ============ */ /* ============ Modal Styles ============ */
.focus-task-modal { .immerse-modal {
padding: 20px; padding: 20px;
} }
.focus-task-modal h2 { .immerse-modal h2 {
margin-top: 0; margin-top: 0;
margin-bottom: 20px; margin-bottom: 20px;
} }
.focus-task-modal-buttons { .immerse-modal-buttons {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 8px; gap: 8px;
@@ -540,41 +567,41 @@
} }
/* About section in settings */ /* About section in settings */
.focus-task-about { .immerse-about {
background: var(--ft-bg-secondary); background: var(--ft-bg-secondary);
border-radius: var(--ft-radius); border-radius: var(--ft-radius);
padding: 16px; padding: 16px;
margin-top: 8px; margin-top: 8px;
} }
.focus-task-about p { .immerse-about p {
margin: 0 0 12px 0; margin: 0 0 12px 0;
line-height: 1.6; line-height: 1.6;
} }
.focus-task-about p:last-child { .immerse-about p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.focus-task-about a { .immerse-about a {
color: var(--ft-primary); color: var(--ft-primary);
} }
/* ============ Scrollbar Styling ============ */ /* ============ Scrollbar Styling ============ */
.focus-task-container::-webkit-scrollbar { .immerse-container::-webkit-scrollbar {
width: 8px; width: 8px;
} }
.focus-task-container::-webkit-scrollbar-track { .immerse-container::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
.focus-task-container::-webkit-scrollbar-thumb { .immerse-container::-webkit-scrollbar-thumb {
background: var(--ft-border); background: var(--ft-border);
border-radius: 4px; border-radius: 4px;
} }
.focus-task-container::-webkit-scrollbar-thumb:hover { .immerse-container::-webkit-scrollbar-thumb:hover {
background: var(--ft-text-muted); background: var(--ft-text-muted);
} }
@@ -585,37 +612,37 @@
100% { transform: scale(1); } 100% { transform: scale(1); }
} }
.focus-task-checkbox.completing { .immerse-checkbox.completing {
animation: ft-checkComplete 0.3s ease; animation: ft-checkComplete 0.3s ease;
} }
/* ============ Dark mode adjustments ============ */ /* ============ Dark mode adjustments ============ */
.theme-dark .focus-task-status-bar { .theme-dark .immerse-status-bar {
background: var(--background-secondary); background: var(--background-secondary);
} }
/* ============ Mobile Responsive ============ */ /* ============ Mobile Responsive ============ */
@media (max-width: 400px) { @media (max-width: 400px) {
.focus-task-header { .immerse-header {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 12px; gap: 12px;
} }
.focus-task-active-controls { .immerse-active-controls {
flex-direction: column; flex-direction: column;
} }
.focus-task-active-controls .focus-task-btn { .immerse-active-controls .immerse-btn {
flex: none; flex: none;
width: 100%; width: 100%;
} }
.focus-task-task-item { .immerse-task-item {
flex-wrap: wrap; flex-wrap: wrap;
} }
.focus-task-task-actions { .immerse-task-actions {
opacity: 1; opacity: 1;
width: 100%; width: 100%;
justify-content: flex-end; justify-content: flex-end;

View File

@@ -1,3 +1,8 @@
{ {
"1.0.4": "0.15.0" "1.0.4": "0.15.0",
"1.0.5": "0.15.0",
"1.0.6": "0.15.0",
"1.0.7": "0.15.0",
"1.0.8": "0.15.0",
"1.0.9": "0.15.0"
} }