From a652a6ac04023e7df8699559453f645a455c8fbc Mon Sep 17 00:00:00 2001 From: Sayuop Date: Sat, 22 Nov 2025 13:34:07 +0100 Subject: [PATCH] Initial commit --- .gitignore | 19 + LICENSE | 21 ++ README.md | 254 ++++++++++++++ esbuild.config.mjs | 48 +++ manifest.json | 11 + package.json | 36 ++ src/main.ts | 843 +++++++++++++++++++++++++++++++++++++++++++++ src/modals.ts | 204 +++++++++++ src/types.ts | 101 ++++++ src/view.ts | 319 +++++++++++++++++ styles.css | 698 +++++++++++++++++++++++++++++++++++++ tsconfig.json | 24 ++ version-bump.mjs | 14 + versions.json | 3 + 14 files changed, 2595 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 esbuild.config.mjs create mode 100644 manifest.json create mode 100644 package.json create mode 100644 src/main.ts create mode 100644 src/modals.ts create mode 100644 src/types.ts create mode 100644 src/view.ts create mode 100644 styles.css create mode 100644 tsconfig.json create mode 100644 version-bump.mjs create mode 100644 versions.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d492a75 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Build output +main.js +*.js.map + +# npm +node_modules/ +package-lock.json + +# IDE +.idea/ +.vscode/ +*.sublime-* + +# OS files +.DS_Store +Thumbs.db + +# Obsidian +data.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..927659e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Crib + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b364524 --- /dev/null +++ b/README.md @@ -0,0 +1,254 @@ +# ⚡ Focus Task + +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) +![License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge) +![Version](https://img.shields.io/badge/Version-1.0.0-blue?style=for-the-badge) + +## 🎯 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. + +### Why Focus Task? + +- **Stay in Flow**: No need to switch between apps - manage tasks where you take notes +- **Time Awareness**: Know exactly how long tasks take vs. your estimates +- **Pomodoro Built-in**: Work in focused sprints with automatic break reminders +- **Satisfying Feedback**: Celebratory messages and sounds when you complete tasks +- **Visual Progress**: See your daily progress with stats and streaks + +## ✨ Features + +### 📋 Task Management +- Create tasks with time estimates +- Organize tasks into customizable lists (Work, Personal, Learning, etc.) +- Add notes and details to tasks +- Filter tasks by list, today's tasks, or completed items +- Drag-and-drop task reordering + +### ⏱️ Dual Timer Modes + +#### Pomodoro Timer +- Configurable work sessions (default: 25 minutes) +- Short breaks (default: 5 minutes) +- Long breaks after a set number of pomodoros (default: 15 minutes every 4 pomodoros) +- Auto-start break option +- Visual countdown with progress bar + +#### Stopwatch Mode +- Free-form time tracking +- Alerts when you exceed your estimate +- Track actual time vs. estimated time + +### 📊 Progress Tracking +- **Daily Stats**: See tasks completed, focus time, and more +- **Streak Counter**: Build momentum with consecutive productive days +- **Time Comparison**: Compare estimated vs. actual time to improve planning +- **Pomodoro Count**: Track total pomodoros completed + +### 🎨 User Experience +- **Floating Timer Widget**: Draggable timer that stays visible while you work +- **Celebration Messages**: Fun, randomized messages when you complete tasks +- **Sound Notifications**: Audio alerts for timer completion and task completion +- **Keyboard Shortcuts**: Quick access to common actions +- **Responsive Design**: Works great in any panel size + +## 🚀 Installation + +### From Obsidian Community Plugins (Coming Soon) +1. Open Obsidian Settings +2. Go to Community Plugins +3. Search for "Focus Task" +4. Click Install, then Enable + +### Manual Installation +1. Download the latest release from the [releases page](https://git.cribdev.com/crib/focus-task/releases) +2. Extract the files to your vault's `.obsidian/plugins/focus-task/` folder +3. Reload Obsidian +4. Enable the plugin in Settings → Community Plugins + +### Building from Source +```bash +# Clone the repository +git clone https://git.cribdev.com/crib/focus-task.git +cd focus-task + +# Install dependencies +npm install + +# Build the plugin +npm run build + +# Copy to your vault +cp main.js manifest.json styles.css /path/to/your/vault/.obsidian/plugins/focus-task/ +``` + +## 📖 Usage + +### Getting Started + +1. **Open Focus Task**: Click the ⚡ icon in the ribbon or use the command palette (`Ctrl/Cmd + P` → "Open Focus Task Panel") + +2. **Add a Task**: Click "+ Add Task" and fill in: + - Task description + - Time estimate + - List category + +3. **Start Focusing**: Click ▶ on any task to start a Pomodoro session, or ⏱ for stopwatch mode + +4. **Complete Tasks**: Check off tasks when done and enjoy the celebration! + +### Keyboard Shortcuts + +| Action | Command | +|--------|---------| +| Open Panel | `Ctrl/Cmd + P` → "Open Focus Task Panel" | +| Quick Add Task | `Ctrl/Cmd + P` → "Quick Add Task" | +| Toggle Timer | `Ctrl/Cmd + P` → "Toggle Timer" | +| Complete Task | `Ctrl/Cmd + P` → "Complete Current Task" | +| Start Focus | `Ctrl/Cmd + P` → "Start Focus Mode on Next Task" | + +### Timer Modes Explained + +#### 🍅 Pomodoro Mode (▶ button) +Best for: Maintaining focus on challenging tasks + +1. Timer counts down from your configured work duration +2. When time's up, you'll get a notification +3. Take a break (short or long, based on your settings) +4. Repeat until the task is complete + +#### ⏱️ Stopwatch Mode (⏱ button) +Best for: Tracking time on open-ended tasks + +1. Timer counts up from zero +2. Get alerted when you exceed your estimate +3. Stop whenever the task is complete +4. See exactly how long the task took + +## ⚙️ Settings + +### Pomodoro Timer +| Setting | Description | Default | +|---------|-------------|---------| +| Work Duration | Length of each work session | 25 min | +| Short Break | Length of short breaks | 5 min | +| Long Break | Length of long breaks | 15 min | +| Long Break Interval | Pomodoros before a long break | 4 | +| Auto-start Breaks | Automatically start break timer | Off | + +### General +| Setting | Description | Default | +|---------|-------------|---------| +| Default Time Estimate | Default estimate for new tasks | 30 min | +| Enable Sounds | Play completion/alert sounds | On | +| Enable Celebrations | Show celebration messages | On | +| Show Floating Timer | Display draggable timer widget | On | + +### Lists +Customize your task lists with: +- Custom names +- Emoji icons +- Color coding + +Default lists: Work 💼, Personal 🏠, Learning 📚 + +## 🎨 Customization + +### Adding Custom Lists +1. Go to Settings → Focus Task → Lists +2. Click "+ Add List" +3. Set the name, emoji, and color +4. Click Save + +### Theming +Focus Task respects your Obsidian theme and adapts to both light and dark modes automatically. + +## 📁 Project Structure + +``` +focus-task/ +├── src/ +│ ├── main.ts # Main plugin class +│ ├── types.ts # TypeScript interfaces +│ ├── view.ts # Main UI view +│ └── modals.ts # Task modals +├── styles.css # Plugin styles +├── manifest.json # Obsidian plugin manifest +├── package.json # npm configuration +├── tsconfig.json # TypeScript config +├── esbuild.config.mjs # Build configuration +└── README.md # This file +``` + +## 🙏 Inspiration & Credits + +This plugin is **heavily inspired by [Blitzit](https://www.blitzit.app/)**, a fantastic standalone productivity app that combines task management with focused time tracking. + +Blitzit's approach to productivity resonated with me: +- Simple, focused interface +- Time estimation and tracking +- Pomodoro technique integration +- Satisfying task completion experience +- Progress insights + +I wanted to bring this experience directly into Obsidian, where I already manage my notes and knowledge. Focus Task is my attempt to capture the essence of what makes Blitzit great while leveraging Obsidian's powerful ecosystem. + +**If you're looking for a dedicated productivity app, I highly recommend checking out [Blitzit](https://www.blitzit.app/)!** + +## 🤝 Contributing + +Contributions are welcome! Feel free to: + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +### Development Setup + +```bash +# Clone the repo +git clone https://git.cribdev.com/crib/focus-task.git +cd focus-task + +# Install dependencies +npm install + +# Start development build (watches for changes) +npm run dev + +# Create symlink to your test vault +ln -s $(pwd) /path/to/vault/.obsidian/plugins/focus-task +``` + +## 📝 Roadmap + +- [ ] Integration with Obsidian Tasks plugin +- [ ] Calendar view for scheduled tasks +- [ ] Weekly/Monthly reports +- [ ] Task templates +- [ ] Sync with external task managers +- [ ] Mobile optimizations +- [ ] Task dependencies +- [ ] Time blocking in daily notes + +## 📜 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## 🔗 Links + +- **Repository**: [https://git.cribdev.com/crib/focus-task](https://git.cribdev.com/crib/focus-task) +- **Issues**: [https://git.cribdev.com/crib/focus-task/issues](https://git.cribdev.com/crib/focus-task/issues) +- **Inspiration**: [Blitzit App](https://www.blitzit.app/) + +--- + +

+ Made with ❤️ for the Obsidian community +
+ Inspired by Blitzit ⚡ +

diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..27f4fc4 --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,48 @@ +import esbuild from "esbuild"; +import process from "process"; +import builtins from "builtin-modules"; + +const banner = +`/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ +`; + +const prod = (process.argv[2] === 'production'); + +const context = await esbuild.context({ + banner: { + js: banner, + }, + entryPoints: ['src/main.ts'], + bundle: true, + external: [ + 'obsidian', + 'electron', + '@codemirror/autocomplete', + '@codemirror/collab', + '@codemirror/commands', + '@codemirror/language', + '@codemirror/lint', + '@codemirror/search', + '@codemirror/state', + '@codemirror/view', + '@lezer/common', + '@lezer/highlight', + '@lezer/lr', + ...builtins], + format: 'cjs', + target: 'es2018', + logLevel: "info", + sourcemap: prod ? false : 'inline', + treeShaking: true, + outfile: 'main.js', +}); + +if (prod) { + await context.rebuild(); + process.exit(0); +} else { + await context.watch(); +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..7fe3bdf --- /dev/null +++ b/manifest.json @@ -0,0 +1,11 @@ +{ + "id": "focus-task", + "name": "Focus Task", + "version": "1.0.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.", + "author": "Crib", + "authorUrl": "https://git.cribdev.com/crib", + "fundingUrl": "", + "isDesktopOnly": false +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0681790 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "focus-task", + "version": "1.0.0", + "description": "A Blitzit-inspired task management and focus timer plugin for Obsidian", + "main": "main.js", + "scripts": { + "dev": "node esbuild.config.mjs", + "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", + "version": "node version-bump.mjs && git add manifest.json versions.json" + }, + "keywords": [ + "obsidian", + "plugin", + "productivity", + "pomodoro", + "tasks", + "timer", + "focus" + ], + "author": "Crib", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://git.cribdev.com/crib/focus-task" + }, + "devDependencies": { + "@types/node": "^16.11.6", + "@typescript-eslint/eslint-plugin": "^5.29.0", + "@typescript-eslint/parser": "^5.29.0", + "builtin-modules": "^3.3.0", + "esbuild": "0.17.3", + "obsidian": "latest", + "tslib": "2.4.0", + "typescript": "4.7.4" + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..dd7d2b1 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,843 @@ +import { + App, + Notice, + Plugin, + PluginSettingTab, + Setting, + WorkspaceLeaf, +} from 'obsidian'; + +import { + FocusTask, + FocusTaskSettings, + FocusTaskData, + DEFAULT_SETTINGS, + DEFAULT_DATA, + VIEW_TYPE_FOCUS_TASK, + CELEBRATION_MESSAGES, + EARLY_FINISH_MESSAGES, + OVERTIME_MESSAGES, +} from './types'; + +import { FocusTaskView } from './view'; +import { QuickAddTaskModal } from './modals'; + +// ============ Main Plugin Class ============ + +export default class FocusTaskPlugin extends Plugin { + settings: FocusTaskSettings; + data: FocusTaskData; + + // Timer state + timerInterval: number | null = null; + currentTimerSeconds: number = 0; + isTimerRunning: boolean = false; + isBreakMode: boolean = false; + activeTaskId: string | null = null; + pomodoroCount: number = 0; + + // Floating timer element + floatingTimerEl: HTMLElement | null = null; + + async onload() { + await this.loadAllData(); + + // Check and reset daily stats + this.checkDailyReset(); + + // Register the main view + this.registerView( + VIEW_TYPE_FOCUS_TASK, + (leaf) => new FocusTaskView(leaf, this) + ); + + // Add ribbon icon + this.addRibbonIcon('zap', 'Open Focus Task', () => { + this.activateView(); + }); + + // Add commands + this.addCommand({ + id: 'open-focus-task', + name: 'Open Focus Task Panel', + callback: () => this.activateView(), + }); + + this.addCommand({ + id: 'quick-add-task', + name: 'Quick Add Task', + callback: () => new QuickAddTaskModal(this.app, this).open(), + }); + + this.addCommand({ + id: 'start-focus-mode', + name: 'Start Focus Mode on Next Task', + callback: () => this.startFocusOnNextTask(), + }); + + this.addCommand({ + id: 'toggle-timer', + name: 'Toggle Timer (Play/Pause)', + callback: () => this.toggleTimer(), + }); + + this.addCommand({ + id: 'complete-current-task', + name: 'Complete Current Task', + callback: () => this.completeActiveTask(), + }); + + // Add settings tab + this.addSettingTab(new FocusTaskSettingTab(this.app, this)); + + // Create floating timer if enabled + if (this.settings.showFloatingTimer) { + this.createFloatingTimer(); + } + } + + onunload() { + this.stopTimer(); + this.removeFloatingTimer(); + } + + async loadAllData() { + const loaded = await this.loadData(); + this.data = Object.assign({}, DEFAULT_DATA, loaded?.data || {}); + this.settings = Object.assign({}, DEFAULT_SETTINGS, loaded?.settings || {}); + } + + async saveAllData() { + await this.saveData({ + settings: this.settings, + data: this.data, + }); + } + + checkDailyReset() { + const today = new Date().toDateString(); + if (this.data.lastActiveDate !== today) { + // Check streak + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + if (this.data.lastActiveDate === yesterday.toDateString()) { + this.data.streak++; + } else if (this.data.lastActiveDate !== today) { + this.data.streak = 0; + } + + // Reset daily stats + this.data.completedToday = 0; + this.data.totalFocusMinutesToday = 0; + this.data.lastActiveDate = today; + this.saveAllData(); + } + } + + async activateView() { + const { workspace } = this.app; + + let leaf: WorkspaceLeaf | null = null; + const leaves = workspace.getLeavesOfType(VIEW_TYPE_FOCUS_TASK); + + if (leaves.length > 0) { + leaf = leaves[0]; + } else { + leaf = workspace.getRightLeaf(false); + if (leaf) { + await leaf.setViewState({ type: VIEW_TYPE_FOCUS_TASK, active: true }); + } + } + + if (leaf) { + workspace.revealLeaf(leaf); + } + } + + // ============ Task Management ============ + + createTask(text: string, estimatedMinutes: number = this.settings.defaultEstimateMinutes, list: string = 'work'): FocusTask { + return { + id: this.generateId(), + text, + completed: false, + estimatedMinutes, + actualMinutes: 0, + createdAt: Date.now(), + list, + notes: '', + isActive: false, + }; + } + + generateId(): string { + return Date.now().toString(36) + Math.random().toString(36).substr(2); + } + + addTask(task: FocusTask) { + this.data.tasks.push(task); + this.saveAllData(); + this.refreshView(); + } + + updateTask(taskId: string, updates: Partial) { + const task = this.data.tasks.find(t => t.id === taskId); + if (task) { + Object.assign(task, updates); + this.saveAllData(); + this.refreshView(); + } + } + + deleteTask(taskId: string) { + this.data.tasks = this.data.tasks.filter(t => t.id !== taskId); + if (this.activeTaskId === taskId) { + this.stopTimer(); + this.activeTaskId = null; + } + this.saveAllData(); + this.refreshView(); + } + + completeTask(taskId: string) { + const task = this.data.tasks.find(t => t.id === taskId); + if (task && !task.completed) { + task.completed = true; + task.completedAt = Date.now(); + task.isActive = false; + + this.data.completedToday++; + this.data.lastActiveDate = new Date().toDateString(); + + // Show celebration + if (this.settings.enableCelebrations) { + this.showCelebration(task); + } + + // Play sound + if (this.settings.enableSounds) { + this.playCompletionSound(); + } + + if (this.activeTaskId === taskId) { + this.stopTimer(); + this.activeTaskId = null; + } + + this.saveAllData(); + this.refreshView(); + } + } + + completeActiveTask() { + if (this.activeTaskId) { + this.completeTask(this.activeTaskId); + } else { + new Notice('No active task to complete'); + } + } + + // ============ Timer Management ============ + + startTimer(taskId: string) { + const task = this.data.tasks.find(t => t.id === taskId); + if (!task) return; + + // Stop any existing timer + this.stopTimer(); + + // Set active task + this.activeTaskId = taskId; + task.isActive = true; + this.isBreakMode = false; + this.currentTimerSeconds = 0; + this.isTimerRunning = true; + + // Start interval (count up mode - stopwatch) + this.timerInterval = window.setInterval(() => { + this.currentTimerSeconds++; + task.actualMinutes = Math.floor(this.currentTimerSeconds / 60); + + // Update floating timer + this.updateFloatingTimer(); + this.refreshView(); + + // Check if over estimate + if (this.currentTimerSeconds === task.estimatedMinutes * 60) { + if (this.settings.enableSounds) { + this.playAlertSound(); + } + new Notice(`⏰ Time's up for: ${task.text}`); + } + }, 1000); + + this.saveAllData(); + this.refreshView(); + this.updateFloatingTimer(); + } + + startPomodoro(taskId: string) { + const task = this.data.tasks.find(t => t.id === taskId); + if (!task) return; + + this.stopTimer(); + this.activeTaskId = taskId; + task.isActive = true; + this.isBreakMode = false; + this.currentTimerSeconds = this.settings.pomodoroWorkMinutes * 60; + this.isTimerRunning = true; + + this.timerInterval = window.setInterval(() => { + this.currentTimerSeconds--; + + if (!this.isBreakMode) { + task.actualMinutes = Math.floor((this.settings.pomodoroWorkMinutes * 60 - this.currentTimerSeconds) / 60); + this.data.totalFocusMinutesToday = Math.floor(this.data.totalFocusMinutesToday + 1/60); + } + + this.updateFloatingTimer(); + this.refreshView(); + + if (this.currentTimerSeconds <= 0) { + this.handlePomodoroEnd(); + } + }, 1000); + + this.updateFloatingTimer(); + this.refreshView(); + } + + handlePomodoroEnd() { + if (!this.isBreakMode) { + // Work session ended + this.pomodoroCount++; + this.data.pomodorosCompleted++; + + if (this.settings.enableSounds) { + this.playAlertSound(); + } + + new Notice('🍅 Pomodoro complete! Time for a break.'); + + if (this.settings.autoStartBreak) { + this.startBreak(); + } else { + this.stopTimer(); + } + } else { + // Break ended + if (this.settings.enableSounds) { + this.playAlertSound(); + } + new Notice('⚡ Break over! Ready to focus?'); + this.isBreakMode = false; + this.stopTimer(); + } + + this.saveAllData(); + } + + startBreak() { + this.isBreakMode = true; + const isLongBreak = this.pomodoroCount % this.settings.longBreakInterval === 0; + this.currentTimerSeconds = (isLongBreak ? this.settings.longBreakMinutes : this.settings.pomodoroBreakMinutes) * 60; + + new Notice(isLongBreak ? '☕ Long break time!' : '☕ Short break time!'); + + if (!this.timerInterval) { + this.timerInterval = window.setInterval(() => { + this.currentTimerSeconds--; + this.updateFloatingTimer(); + this.refreshView(); + + if (this.currentTimerSeconds <= 0) { + this.handlePomodoroEnd(); + } + }, 1000); + } + + this.updateFloatingTimer(); + this.refreshView(); + } + + toggleTimer() { + if (this.isTimerRunning && this.timerInterval) { + // Pause + window.clearInterval(this.timerInterval); + this.timerInterval = null; + this.isTimerRunning = false; + } else if (this.activeTaskId) { + // Resume + this.isTimerRunning = true; + const task = this.data.tasks.find(t => t.id === this.activeTaskId); + + this.timerInterval = window.setInterval(() => { + this.currentTimerSeconds--; + if (task && !this.isBreakMode) { + task.actualMinutes = Math.floor((this.settings.pomodoroWorkMinutes * 60 - this.currentTimerSeconds) / 60); + } + this.updateFloatingTimer(); + this.refreshView(); + + if (this.currentTimerSeconds <= 0) { + this.handlePomodoroEnd(); + } + }, 1000); + } else { + new Notice('No active task. Select a task first.'); + } + + this.updateFloatingTimer(); + this.refreshView(); + } + + stopTimer() { + if (this.timerInterval) { + window.clearInterval(this.timerInterval); + this.timerInterval = null; + } + + if (this.activeTaskId) { + const task = this.data.tasks.find(t => t.id === this.activeTaskId); + if (task) { + task.isActive = false; + } + } + + this.isTimerRunning = false; + this.activeTaskId = null; + this.updateFloatingTimer(); + this.saveAllData(); + } + + startFocusOnNextTask() { + const pendingTasks = this.data.tasks.filter(t => !t.completed); + if (pendingTasks.length > 0) { + this.startPomodoro(pendingTasks[0].id); + } else { + new Notice('No pending tasks. Add a task first!'); + } + } + + // ============ Floating Timer ============ + + createFloatingTimer() { + if (this.floatingTimerEl) return; + + this.floatingTimerEl = document.body.createEl('div', { + cls: 'focus-task-floating-timer', + }); + + this.floatingTimerEl.innerHTML = ` +
+
No active task
+
00:00
+
+ + +
+
+ `; + + // Make draggable + this.makeDraggable(this.floatingTimerEl); + + // Add event listeners + const playPauseBtn = this.floatingTimerEl.querySelector('.play-pause'); + const completeBtn = this.floatingTimerEl.querySelector('.complete'); + + playPauseBtn?.addEventListener('click', () => this.toggleTimer()); + completeBtn?.addEventListener('click', () => this.completeActiveTask()); + } + + removeFloatingTimer() { + if (this.floatingTimerEl) { + this.floatingTimerEl.remove(); + this.floatingTimerEl = null; + } + } + + updateFloatingTimer() { + if (!this.floatingTimerEl) return; + + const taskEl = this.floatingTimerEl.querySelector('.focus-task-floating-task'); + const timeEl = this.floatingTimerEl.querySelector('.focus-task-floating-time'); + const playPauseBtn = this.floatingTimerEl.querySelector('.play-pause'); + + if (this.activeTaskId) { + const task = this.data.tasks.find(t => t.id === this.activeTaskId); + if (task && taskEl) { + taskEl.textContent = this.isBreakMode ? '☕ Break Time' : task.text; + } + } else if (taskEl) { + taskEl.textContent = 'No active task'; + } + + if (timeEl) { + timeEl.textContent = this.formatTime(this.currentTimerSeconds); + timeEl.classList.toggle('focus-task-overtime', this.currentTimerSeconds < 0); + } + + if (playPauseBtn) { + playPauseBtn.textContent = this.isTimerRunning ? '⏸' : '▶'; + } + + // Update color based on state + this.floatingTimerEl.classList.toggle('focus-task-break-mode', this.isBreakMode); + this.floatingTimerEl.classList.toggle('focus-task-active', this.isTimerRunning); + } + + makeDraggable(el: HTMLElement) { + let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; + + el.onmousedown = dragMouseDown; + + function dragMouseDown(e: MouseEvent) { + if ((e.target as HTMLElement).tagName === 'BUTTON') return; + e.preventDefault(); + pos3 = e.clientX; + pos4 = e.clientY; + document.onmouseup = closeDragElement; + document.onmousemove = elementDrag; + } + + function elementDrag(e: MouseEvent) { + e.preventDefault(); + pos1 = pos3 - e.clientX; + pos2 = pos4 - e.clientY; + pos3 = e.clientX; + pos4 = e.clientY; + el.style.top = (el.offsetTop - pos2) + "px"; + el.style.left = (el.offsetLeft - pos1) + "px"; + el.style.right = 'auto'; + el.style.bottom = 'auto'; + } + + function closeDragElement() { + document.onmouseup = null; + document.onmousemove = null; + } + } + + // ============ Sounds & Celebrations ============ + + showCelebration(task: FocusTask) { + let messages = CELEBRATION_MESSAGES; + let extraMessage = ''; + + if (task.actualMinutes < task.estimatedMinutes) { + const saved = task.estimatedMinutes - task.actualMinutes; + messages = EARLY_FINISH_MESSAGES; + extraMessage = ` (${saved} min early!)`; + } else if (task.actualMinutes > task.estimatedMinutes * 1.5) { + messages = OVERTIME_MESSAGES; + } + + const celebration = messages[Math.floor(Math.random() * messages.length)]; + new Notice(`${celebration.emoji} ${celebration.message}${extraMessage}`); + } + + playCompletionSound() { + try { + const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.frequency.setValueAtTime(800, audioContext.currentTime); + oscillator.frequency.setValueAtTime(1000, audioContext.currentTime + 0.1); + oscillator.frequency.setValueAtTime(1200, audioContext.currentTime + 0.2); + + gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); + + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + 0.3); + } catch (e) { + console.log('Audio not available'); + } + } + + playAlertSound() { + try { + const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.frequency.setValueAtTime(440, audioContext.currentTime); + gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); + + for (let i = 0; i < 3; i++) { + oscillator.frequency.setValueAtTime(440, audioContext.currentTime + i * 0.3); + oscillator.frequency.setValueAtTime(550, audioContext.currentTime + i * 0.3 + 0.15); + } + + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.9); + + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + 0.9); + } catch (e) { + console.log('Audio not available'); + } + } + + // ============ Utilities ============ + + formatTime(seconds: number): string { + const absSeconds = Math.abs(seconds); + const mins = Math.floor(absSeconds / 60); + const secs = absSeconds % 60; + const sign = seconds < 0 ? '-' : ''; + return `${sign}${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } + + formatTimeHuman(minutes: number): string { + if (minutes < 60) return `${minutes}min`; + const hours = Math.floor(minutes / 60); + const mins = minutes % 60; + return mins > 0 ? `${hours}hr ${mins}min` : `${hours}hr`; + } + + refreshView() { + const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_FOCUS_TASK); + leaves.forEach(leaf => { + if (leaf.view instanceof FocusTaskView) { + leaf.view.refresh(); + } + }); + } + + getTasksByList(listId: string): FocusTask[] { + return this.data.tasks.filter(t => t.list === listId); + } + + getPendingTasks(): FocusTask[] { + return this.data.tasks.filter(t => !t.completed); + } + + getTodaysTasks(): FocusTask[] { + const today = new Date().toDateString(); + return this.data.tasks.filter(t => { + if (t.scheduledDate === today) return true; + if (!t.scheduledDate && !t.completed) return true; + return false; + }); + } + + getStats() { + const pending = this.getPendingTasks(); + const totalEstimate = pending.reduce((sum, t) => sum + t.estimatedMinutes, 0); + const completedTasks = this.data.tasks.filter(t => t.completed); + const avgAccuracy = completedTasks.length > 0 + ? completedTasks.reduce((sum, t) => sum + (t.estimatedMinutes / Math.max(t.actualMinutes, 1)), 0) / completedTasks.length + : 1; + + return { + pendingCount: pending.length, + completedToday: this.data.completedToday, + totalEstimatedMinutes: totalEstimate, + totalFocusMinutesToday: Math.floor(this.data.totalFocusMinutesToday), + streak: this.data.streak, + pomodorosCompleted: this.data.pomodorosCompleted, + avgAccuracy: Math.round(avgAccuracy * 100), + }; + } +} + +// ============ Settings Tab ============ + +class FocusTaskSettingTab extends PluginSettingTab { + plugin: FocusTaskPlugin; + + constructor(app: App, plugin: FocusTaskPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + const { containerEl } = this; + containerEl.empty(); + + containerEl.createEl('h1', { text: '⚡ Focus Task Settings' }); + + // Pomodoro Settings + containerEl.createEl('h2', { text: '🍅 Pomodoro Timer' }); + + new 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; + await this.plugin.saveAllData(); + })); + + new Setting(containerEl) + .setName('Short Break Duration') + .setDesc('Length of short breaks in minutes') + .addSlider(slider => slider + .setLimits(1, 15, 1) + .setValue(this.plugin.settings.pomodoroBreakMinutes) + .setDynamicTooltip() + .onChange(async value => { + this.plugin.settings.pomodoroBreakMinutes = value; + await this.plugin.saveAllData(); + })); + + new Setting(containerEl) + .setName('Long Break Duration') + .setDesc('Length of long breaks in minutes') + .addSlider(slider => slider + .setLimits(5, 30, 5) + .setValue(this.plugin.settings.longBreakMinutes) + .setDynamicTooltip() + .onChange(async value => { + this.plugin.settings.longBreakMinutes = value; + await this.plugin.saveAllData(); + })); + + new Setting(containerEl) + .setName('Long Break Interval') + .setDesc('Number of pomodoros before a long break') + .addSlider(slider => slider + .setLimits(2, 6, 1) + .setValue(this.plugin.settings.longBreakInterval) + .setDynamicTooltip() + .onChange(async value => { + this.plugin.settings.longBreakInterval = value; + await this.plugin.saveAllData(); + })); + + new Setting(containerEl) + .setName('Auto-start Breaks') + .setDesc('Automatically start break timer after work session') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.autoStartBreak) + .onChange(async value => { + this.plugin.settings.autoStartBreak = value; + await this.plugin.saveAllData(); + })); + + // General Settings + containerEl.createEl('h2', { text: '⚙️ General' }); + + new Setting(containerEl) + .setName('Default Time Estimate') + .setDesc('Default estimated time for new tasks in minutes') + .addSlider(slider => slider + .setLimits(5, 120, 5) + .setValue(this.plugin.settings.defaultEstimateMinutes) + .setDynamicTooltip() + .onChange(async value => { + this.plugin.settings.defaultEstimateMinutes = value; + await this.plugin.saveAllData(); + })); + + new Setting(containerEl) + .setName('Enable Sounds') + .setDesc('Play sounds for timer completion and task completion') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.enableSounds) + .onChange(async value => { + this.plugin.settings.enableSounds = value; + await this.plugin.saveAllData(); + })); + + new Setting(containerEl) + .setName('Enable Celebrations') + .setDesc('Show celebration messages when completing tasks') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.enableCelebrations) + .onChange(async value => { + this.plugin.settings.enableCelebrations = value; + await this.plugin.saveAllData(); + })); + + new Setting(containerEl) + .setName('Show Floating Timer') + .setDesc('Display a draggable floating timer widget') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.showFloatingTimer) + .onChange(async value => { + this.plugin.settings.showFloatingTimer = value; + if (value) { + this.plugin.createFloatingTimer(); + } else { + this.plugin.removeFloatingTimer(); + } + await this.plugin.saveAllData(); + })); + + // Lists Management + containerEl.createEl('h2', { text: '📋 Lists' }); + + this.plugin.settings.lists.forEach((list, index) => { + new Setting(containerEl) + .setName(`${list.icon} ${list.name}`) + .addText(text => text + .setValue(list.name) + .setPlaceholder('List name') + .onChange(async value => { + this.plugin.settings.lists[index].name = value; + await this.plugin.saveAllData(); + })) + .addText(text => text + .setValue(list.icon) + .setPlaceholder('Emoji') + .onChange(async value => { + this.plugin.settings.lists[index].icon = value; + await this.plugin.saveAllData(); + })) + .addColorPicker(picker => picker + .setValue(list.color) + .onChange(async value => { + this.plugin.settings.lists[index].color = value; + await this.plugin.saveAllData(); + })) + .addButton(btn => btn + .setIcon('trash') + .setTooltip('Delete list') + .onClick(async () => { + this.plugin.settings.lists.splice(index, 1); + await this.plugin.saveAllData(); + this.display(); + })); + }); + + new Setting(containerEl) + .addButton(btn => btn + .setButtonText('+ Add List') + .onClick(async () => { + this.plugin.settings.lists.push({ + id: this.plugin.generateId(), + name: 'New List', + color: '#6366f1', + icon: '📁', + }); + await this.plugin.saveAllData(); + this.display(); + })); + + // About section + containerEl.createEl('h2', { text: '📖 About' }); + + const aboutDiv = containerEl.createDiv({ cls: 'focus-task-about' }); + aboutDiv.innerHTML = ` +

Focus Task is heavily inspired by Blitzit, + a fantastic productivity app that combines task management with focused time tracking.

+

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.

+

+ Source Code +

+ `; + } +} diff --git a/src/modals.ts b/src/modals.ts new file mode 100644 index 0000000..40e5b3e --- /dev/null +++ b/src/modals.ts @@ -0,0 +1,204 @@ +import { + App, + Modal, + Notice, + Setting, +} from 'obsidian'; + +import { FocusTask } from './types'; +import FocusTaskPlugin from './main'; + +// ============ Quick Add Task Modal ============ + +export class QuickAddTaskModal extends Modal { + plugin: FocusTaskPlugin; + taskText: string = ''; + estimatedMinutes: number; + selectedList: string = 'work'; + + constructor(app: App, plugin: FocusTaskPlugin) { + super(app); + this.plugin = plugin; + this.estimatedMinutes = plugin.settings.defaultEstimateMinutes; + if (plugin.settings.lists.length > 0) { + this.selectedList = plugin.settings.lists[0].id; + } + } + + onOpen() { + const { contentEl } = this; + contentEl.addClass('focus-task-modal'); + + contentEl.createEl('h2', { text: '⚡ Add New Task' }); + + // Task text input + new Setting(contentEl) + .setName('Task') + .addText(text => { + text.setPlaceholder('What do you need to do?') + .onChange(value => this.taskText = value); + text.inputEl.focus(); + text.inputEl.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && this.taskText.trim()) { + this.submitTask(); + } + }); + }); + + // Time estimate + new Setting(contentEl) + .setName('Estimated Time') + .setDesc('How long do you think this will take?') + .addDropdown(dropdown => { + const options: Record = { + '5': '5 min', + '10': '10 min', + '15': '15 min', + '20': '20 min', + '25': '25 min (1 pomodoro)', + '30': '30 min', + '45': '45 min', + '50': '50 min (2 pomodoros)', + '60': '1 hour', + '90': '1.5 hours', + '120': '2 hours', + '180': '3 hours', + }; + Object.entries(options).forEach(([value, label]) => { + dropdown.addOption(value, label); + }); + dropdown.setValue(this.estimatedMinutes.toString()); + dropdown.onChange(value => this.estimatedMinutes = parseInt(value)); + }); + + // List selection + new Setting(contentEl) + .setName('List') + .addDropdown(dropdown => { + this.plugin.settings.lists.forEach(list => { + dropdown.addOption(list.id, `${list.icon} ${list.name}`); + }); + dropdown.setValue(this.selectedList); + dropdown.onChange(value => this.selectedList = value); + }); + + // Buttons + const buttonContainer = contentEl.createEl('div', { cls: 'focus-task-modal-buttons' }); + + const cancelBtn = buttonContainer.createEl('button', { text: 'Cancel', cls: 'focus-task-btn' }); + cancelBtn.addEventListener('click', () => this.close()); + + const addBtn = buttonContainer.createEl('button', { text: 'Add Task', cls: 'focus-task-btn focus-task-btn-primary' }); + addBtn.addEventListener('click', () => this.submitTask()); + } + + submitTask() { + if (this.taskText.trim()) { + const task = this.plugin.createTask(this.taskText, this.estimatedMinutes, this.selectedList); + this.plugin.addTask(task); + new Notice('✅ Task added!'); + this.close(); + } else { + new Notice('Please enter a task description'); + } + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} + +// ============ Edit Task Modal ============ + +export class EditTaskModal extends Modal { + plugin: FocusTaskPlugin; + task: FocusTask; + + constructor(app: App, plugin: FocusTaskPlugin, task: FocusTask) { + super(app); + this.plugin = plugin; + this.task = { ...task }; + } + + onOpen() { + const { contentEl } = this; + contentEl.addClass('focus-task-modal'); + + contentEl.createEl('h2', { text: '✏️ Edit Task' }); + + new Setting(contentEl) + .setName('Task') + .addText(text => text + .setValue(this.task.text) + .onChange(value => this.task.text = value)); + + new Setting(contentEl) + .setName('Estimated Time') + .addDropdown(dropdown => { + const options: Record = { + '5': '5 min', + '10': '10 min', + '15': '15 min', + '20': '20 min', + '25': '25 min', + '30': '30 min', + '45': '45 min', + '50': '50 min', + '60': '1 hour', + '90': '1.5 hours', + '120': '2 hours', + '180': '3 hours', + }; + Object.entries(options).forEach(([value, label]) => { + dropdown.addOption(value, label); + }); + dropdown.setValue(this.task.estimatedMinutes.toString()); + dropdown.onChange(value => this.task.estimatedMinutes = parseInt(value)); + }); + + new Setting(contentEl) + .setName('List') + .addDropdown(dropdown => { + this.plugin.settings.lists.forEach(list => { + dropdown.addOption(list.id, `${list.icon} ${list.name}`); + }); + dropdown.setValue(this.task.list); + dropdown.onChange(value => this.task.list = value); + }); + + new Setting(contentEl) + .setName('Notes') + .setDesc('Add any additional details or links') + .addTextArea(textarea => { + textarea + .setValue(this.task.notes) + .onChange(value => this.task.notes = value); + textarea.inputEl.rows = 4; + }); + + // Show actual time if task has been worked on + if (this.task.actualMinutes > 0) { + new 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 cancelBtn = buttonContainer.createEl('button', { text: 'Cancel', cls: 'focus-task-btn' }); + cancelBtn.addEventListener('click', () => this.close()); + + const saveBtn = buttonContainer.createEl('button', { text: 'Save', cls: 'focus-task-btn focus-task-btn-primary' }); + saveBtn.addEventListener('click', () => { + this.plugin.updateTask(this.task.id, this.task); + new Notice('✅ Task updated!'); + this.close(); + }); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..6a92e8d --- /dev/null +++ b/src/types.ts @@ -0,0 +1,101 @@ +// ============ Types & Interfaces ============ + +export interface FocusTask { + id: string; + text: string; + completed: boolean; + estimatedMinutes: number; + actualMinutes: number; + createdAt: number; + completedAt?: number; + list: string; + notes: string; + scheduledDate?: string; + isActive: boolean; +} + +export interface TaskList { + id: string; + name: string; + color: string; + icon: string; +} + +export interface FocusTaskSettings { + pomodoroWorkMinutes: number; + pomodoroBreakMinutes: number; + longBreakMinutes: number; + longBreakInterval: number; + enableSounds: boolean; + enableCelebrations: boolean; + defaultEstimateMinutes: number; + lists: TaskList[]; + showFloatingTimer: boolean; + autoStartBreak: boolean; + tickSoundEnabled: boolean; +} + +export interface FocusTaskData { + tasks: FocusTask[]; + completedToday: number; + totalFocusMinutesToday: number; + streak: number; + lastActiveDate: string; + pomodorosCompleted: number; +} + +export const DEFAULT_SETTINGS: FocusTaskSettings = { + pomodoroWorkMinutes: 25, + pomodoroBreakMinutes: 5, + longBreakMinutes: 15, + longBreakInterval: 4, + enableSounds: true, + enableCelebrations: true, + defaultEstimateMinutes: 30, + lists: [ + { id: 'work', name: 'Work', color: '#6366f1', icon: '💼' }, + { id: 'personal', name: 'Personal', color: '#22c55e', icon: '🏠' }, + { id: 'learning', name: 'Learning', color: '#f59e0b', icon: '📚' }, + ], + showFloatingTimer: true, + autoStartBreak: false, + tickSoundEnabled: false, +}; + +export const DEFAULT_DATA: FocusTaskData = { + tasks: [], + completedToday: 0, + totalFocusMinutesToday: 0, + streak: 0, + lastActiveDate: '', + pomodorosCompleted: 0, +}; + +export const VIEW_TYPE_FOCUS_TASK = 'focus-task-view'; + +// ============ Celebration Messages ============ + +export const CELEBRATION_MESSAGES = [ + { emoji: '💥', message: 'Crushed it!' }, + { emoji: '🔥', message: 'On fire!' }, + { emoji: '⚡', message: 'Lightning fast!' }, + { emoji: '🎯', message: 'Bullseye!' }, + { emoji: '🚀', message: 'Blasting through!' }, + { emoji: '💪', message: 'Strong work!' }, + { emoji: '🌟', message: 'Stellar!' }, + { emoji: '✨', message: 'Brilliant!' }, + { emoji: '🏆', message: 'Champion!' }, + { emoji: '🎉', message: 'Well done!' }, +]; + +export const EARLY_FINISH_MESSAGES = [ + { emoji: '⚡', message: 'Speed demon! Finished early!' }, + { emoji: '🏎️', message: 'Faster than expected!' }, + { emoji: '🎯', message: 'Under budget! Nice work!' }, +]; + +export const OVERTIME_MESSAGES = [ + { emoji: '💪', message: 'Persistence pays off!' }, + { emoji: '🏃', message: 'Marathon runner!' }, + { emoji: '🔥', message: 'The grind is real!' }, +]; diff --git a/src/view.ts b/src/view.ts new file mode 100644 index 0000000..00278f5 --- /dev/null +++ b/src/view.ts @@ -0,0 +1,319 @@ +import { + ItemView, + WorkspaceLeaf, +} from 'obsidian'; + +import { VIEW_TYPE_FOCUS_TASK, FocusTask } from './types'; +import { QuickAddTaskModal, EditTaskModal } from './modals'; +import FocusTaskPlugin from './main'; + +// ============ Main View ============ + +export class FocusTaskView extends ItemView { + plugin: FocusTaskPlugin; + currentFilter: string = 'all'; + + constructor(leaf: WorkspaceLeaf, plugin: FocusTaskPlugin) { + super(leaf); + this.plugin = plugin; + } + + getViewType(): string { + return VIEW_TYPE_FOCUS_TASK; + } + + getDisplayText(): string { + return 'Focus Task'; + } + + getIcon(): string { + return 'zap'; + } + + async onOpen() { + this.refresh(); + } + + refresh() { + const container = this.containerEl.children[1]; + container.empty(); + container.addClass('focus-task-container'); + + // Header + this.renderHeader(container); + + // Stats bar + this.renderStatsBar(container); + + // Active task / Timer + this.renderActiveTask(container); + + // Task list + this.renderTaskList(container); + } + + renderHeader(container: Element) { + const header = container.createEl('div', { cls: 'focus-task-header' }); + + const titleSection = header.createEl('div', { cls: 'focus-task-title-section' }); + titleSection.createEl('h2', { text: '⚡ Focus Task', cls: 'focus-task-title' }); + + const today = new Date(); + const dateStr = today.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' }); + titleSection.createEl('div', { text: dateStr, cls: 'focus-task-date' }); + + const actions = header.createEl('div', { cls: 'focus-task-header-actions' }); + + const addBtn = actions.createEl('button', { cls: 'focus-task-btn focus-task-btn-primary' }); + addBtn.innerHTML = '+ Add Task'; + addBtn.addEventListener('click', () => { + new QuickAddTaskModal(this.app, this.plugin).open(); + }); + } + + renderStatsBar(container: Element) { + const stats = this.plugin.getStats(); + const statsBar = container.createEl('div', { cls: 'focus-task-stats-bar' }); + + const statItems = [ + { label: 'Pending', value: stats.pendingCount.toString(), icon: '📋' }, + { label: 'Done Today', value: stats.completedToday.toString(), icon: '✅' }, + { label: 'Focus Time', value: this.plugin.formatTimeHuman(stats.totalFocusMinutesToday), icon: '⏱️' }, + { label: 'Streak', value: `${stats.streak} days`, icon: '🔥' }, + ]; + + statItems.forEach(stat => { + const item = statsBar.createEl('div', { cls: 'focus-task-stat-item' }); + item.createEl('div', { cls: 'focus-task-stat-icon', text: stat.icon }); + item.createEl('div', { cls: 'focus-task-stat-value', text: stat.value }); + item.createEl('div', { cls: 'focus-task-stat-label', text: stat.label }); + }); + } + + renderActiveTask(container: Element) { + const activeSection = container.createEl('div', { cls: 'focus-task-active-section' }); + + if (this.plugin.activeTaskId) { + const task = this.plugin.data.tasks.find(t => t.id === this.plugin.activeTaskId); + if (task) { + activeSection.addClass('focus-task-has-active'); + + const activeCard = activeSection.createEl('div', { cls: 'focus-task-active-card' }); + + if (this.plugin.isBreakMode) { + activeCard.addClass('focus-task-break-card'); + activeCard.createEl('div', { cls: 'focus-task-active-label', text: '☕ BREAK TIME' }); + } else { + activeCard.createEl('div', { cls: 'focus-task-active-label', text: '🎯 FOCUSING ON' }); + } + + activeCard.createEl('div', { cls: 'focus-task-active-task-name', text: task.text }); + + // Timer display + const timerDisplay = activeCard.createEl('div', { cls: 'focus-task-timer-display' }); + timerDisplay.createEl('span', { + cls: 'focus-task-timer-time', + text: this.plugin.formatTime(this.plugin.currentTimerSeconds) + }); + + // Progress bar + const progressWrap = activeCard.createEl('div', { cls: 'focus-task-progress-wrap' }); + const progress = progressWrap.createEl('div', { cls: 'focus-task-progress-bar' }); + + let progressPercent = 0; + if (this.plugin.isBreakMode) { + const breakDuration = this.plugin.pomodoroCount % this.plugin.settings.longBreakInterval === 0 + ? this.plugin.settings.longBreakMinutes * 60 + : this.plugin.settings.pomodoroBreakMinutes * 60; + progressPercent = ((breakDuration - this.plugin.currentTimerSeconds) / breakDuration) * 100; + } else { + const workDuration = this.plugin.settings.pomodoroWorkMinutes * 60; + progressPercent = ((workDuration - this.plugin.currentTimerSeconds) / workDuration) * 100; + } + + progress.style.width = `${Math.min(Math.max(progressPercent, 0), 100)}%`; + if (progressPercent >= 100) progress.addClass('focus-task-overtime'); + + // Time info + if (!this.plugin.isBreakMode) { + const timeInfo = activeCard.createEl('div', { cls: 'focus-task-time-info' }); + timeInfo.createEl('span', { text: `Est: ${this.plugin.formatTimeHuman(task.estimatedMinutes)}` }); + timeInfo.createEl('span', { text: `Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}` }); + } + + // Controls + const controls = activeCard.createEl('div', { cls: 'focus-task-active-controls' }); + + const pauseBtn = controls.createEl('button', { cls: 'focus-task-btn focus-task-btn-secondary' }); + pauseBtn.innerHTML = this.plugin.isTimerRunning ? '⏸ Pause' : '▶ Resume'; + pauseBtn.addEventListener('click', () => this.plugin.toggleTimer()); + + if (!this.plugin.isBreakMode) { + const completeBtn = controls.createEl('button', { cls: 'focus-task-btn focus-task-btn-success' }); + completeBtn.innerHTML = '✓ Complete'; + completeBtn.addEventListener('click', () => this.plugin.completeTask(task.id)); + } + + const stopBtn = controls.createEl('button', { cls: 'focus-task-btn focus-task-btn-danger' }); + stopBtn.innerHTML = '✕ Stop'; + stopBtn.addEventListener('click', () => this.plugin.stopTimer()); + + if (this.plugin.isBreakMode) { + const skipBreakBtn = controls.createEl('button', { cls: 'focus-task-btn' }); + skipBreakBtn.innerHTML = '⏭ Skip Break'; + skipBreakBtn.addEventListener('click', () => { + this.plugin.isBreakMode = false; + this.plugin.stopTimer(); + this.refresh(); + }); + } + } + } else { + // No active task - show start focus prompt + const startPrompt = activeSection.createEl('div', { cls: 'focus-task-start-prompt' }); + startPrompt.createEl('div', { cls: 'focus-task-prompt-icon', text: '⚡' }); + startPrompt.createEl('div', { cls: 'focus-task-prompt-text', text: 'Ready to focus?' }); + startPrompt.createEl('div', { cls: 'focus-task-prompt-hint', text: 'Click ▶ on a task to start a Pomodoro session' }); + } + } + + renderTaskList(container: Element) { + const listSection = container.createEl('div', { cls: 'focus-task-list-section' }); + + // Filters + const filters = listSection.createEl('div', { cls: 'focus-task-filters' }); + + const filterOptions = [ + { id: 'all', label: 'All Tasks' }, + { id: 'today', label: 'Today' }, + { id: 'completed', label: 'Completed' }, + ...this.plugin.settings.lists.map(l => ({ id: l.id, label: `${l.icon} ${l.name}` })), + ]; + + filterOptions.forEach(opt => { + const btn = filters.createEl('button', { + cls: `focus-task-filter-btn ${this.currentFilter === opt.id ? 'active' : ''}`, + text: opt.label, + }); + btn.addEventListener('click', () => { + this.currentFilter = opt.id; + this.refresh(); + }); + }); + + // Task items + const taskList = listSection.createEl('div', { cls: 'focus-task-task-list' }); + + let tasks = this.plugin.data.tasks; + + // Filter tasks + if (this.currentFilter === 'today') { + tasks = this.plugin.getTodaysTasks(); + } else if (this.currentFilter === 'completed') { + tasks = this.plugin.data.tasks.filter(t => t.completed); + } else if (this.currentFilter !== 'all') { + tasks = this.plugin.getTasksByList(this.currentFilter); + } + + // Sort: incomplete first, then by creation date + tasks = [...tasks].sort((a, b) => { + if (a.completed !== b.completed) return a.completed ? 1 : -1; + return b.createdAt - a.createdAt; + }); + + if (tasks.length === 0) { + const emptyState = taskList.createEl('div', { cls: 'focus-task-empty-state' }); + emptyState.createEl('div', { cls: 'focus-task-empty-icon', text: '📝' }); + emptyState.createEl('div', { cls: 'focus-task-empty-text', text: 'No tasks yet' }); + emptyState.createEl('div', { cls: 'focus-task-empty-hint', text: 'Add a task to get started!' }); + } else { + tasks.forEach(task => this.renderTaskItem(taskList, task)); + } + } + + renderTaskItem(container: Element, task: FocusTask) { + const list = this.plugin.settings.lists.find(l => l.id === task.list); + + const taskEl = container.createEl('div', { + cls: `focus-task-task-item ${task.completed ? 'completed' : ''} ${task.isActive ? 'active' : ''}` + }); + + // Checkbox + const checkbox = taskEl.createEl('div', { cls: 'focus-task-checkbox' }); + checkbox.innerHTML = task.completed ? '✓' : ''; + checkbox.style.borderColor = list?.color || '#6366f1'; + if (task.completed) { + checkbox.style.backgroundColor = list?.color || '#6366f1'; + checkbox.style.color = 'white'; + } + checkbox.addEventListener('click', (e) => { + e.stopPropagation(); + if (!task.completed) { + this.plugin.completeTask(task.id); + } + }); + + // Task content + const content = taskEl.createEl('div', { cls: 'focus-task-task-content' }); + + const taskHeader = content.createEl('div', { cls: 'focus-task-task-header' }); + taskHeader.createEl('span', { cls: 'focus-task-task-text', text: task.text }); + + if (list) { + const listBadge = taskHeader.createEl('span', { + cls: 'focus-task-list-badge', + text: `${list.icon} ${list.name}`, + }); + listBadge.style.backgroundColor = list.color + '20'; + listBadge.style.color = list.color; + } + + const taskMeta = content.createEl('div', { cls: 'focus-task-task-meta' }); + taskMeta.createEl('span', { text: `Est: ${this.plugin.formatTimeHuman(task.estimatedMinutes)}` }); + + if (task.actualMinutes > 0) { + const actualSpan = taskMeta.createEl('span'); + actualSpan.setText(`Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}`); + if (task.actualMinutes > task.estimatedMinutes) { + actualSpan.addClass('focus-task-overtime-text'); + } + } + + // Actions + const actions = taskEl.createEl('div', { cls: 'focus-task-task-actions' }); + + if (!task.completed) { + // Start pomodoro button + const startBtn = actions.createEl('button', { cls: 'focus-task-task-btn', attr: { 'aria-label': 'Start Pomodoro' } }); + startBtn.innerHTML = '▶'; + startBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.plugin.startPomodoro(task.id); + }); + + // Stopwatch mode button + const stopwatchBtn = actions.createEl('button', { cls: 'focus-task-task-btn', attr: { 'aria-label': 'Start Stopwatch' } }); + stopwatchBtn.innerHTML = '⏱'; + stopwatchBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.plugin.startTimer(task.id); + }); + } + + // Edit button + const editBtn = actions.createEl('button', { cls: 'focus-task-task-btn', attr: { 'aria-label': 'Edit' } }); + editBtn.innerHTML = '✏️'; + editBtn.addEventListener('click', (e) => { + e.stopPropagation(); + new EditTaskModal(this.app, this.plugin, task).open(); + }); + + // Delete button + const deleteBtn = actions.createEl('button', { cls: 'focus-task-task-btn focus-task-delete-btn', attr: { 'aria-label': 'Delete' } }); + deleteBtn.innerHTML = '🗑'; + deleteBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.plugin.deleteTask(task.id); + }); + } +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..3785fe6 --- /dev/null +++ b/styles.css @@ -0,0 +1,698 @@ +/* ============================================ + FOCUS TASK - Obsidian Plugin Styles + Inspired by https://www.blitzit.app/ + ============================================ */ + +/* ============ CSS Variables ============ */ +.focus-task-container { + --ft-primary: #6366f1; + --ft-primary-light: #818cf8; + --ft-primary-dark: #4f46e5; + --ft-success: #22c55e; + --ft-warning: #f59e0b; + --ft-danger: #ef4444; + --ft-break: #06b6d4; + + --ft-bg: var(--background-primary); + --ft-bg-secondary: var(--background-secondary); + --ft-bg-tertiary: var(--background-modifier-hover); + --ft-text: var(--text-normal); + --ft-text-muted: var(--text-muted); + --ft-border: var(--background-modifier-border); + + --ft-radius: 12px; + --ft-radius-sm: 8px; + --ft-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --ft-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} + +/* ============ Container ============ */ +.focus-task-container { + padding: 16px; + font-family: var(--font-interface); + overflow-y: auto; + height: 100%; +} + +/* ============ Header ============ */ +.focus-task-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid var(--ft-border); +} + +.focus-task-title-section { + display: flex; + flex-direction: column; + gap: 4px; +} + +.focus-task-title { + margin: 0; + font-size: 24px; + font-weight: 700; + background: linear-gradient(135deg, var(--ft-primary) 0%, var(--ft-primary-light) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.focus-task-date { + color: var(--ft-text-muted); + font-size: 13px; +} + +.focus-task-header-actions { + display: flex; + gap: 8px; +} + +/* ============ Buttons ============ */ +.focus-task-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 16px; + border-radius: var(--ft-radius-sm); + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + border: none; + background: var(--ft-bg-tertiary); + color: var(--ft-text); +} + +.focus-task-btn:hover { + background: var(--ft-border); + transform: translateY(-1px); +} + +.focus-task-btn-primary { + background: linear-gradient(135deg, var(--ft-primary) 0%, var(--ft-primary-dark) 100%); + color: white; +} + +.focus-task-btn-primary:hover { + background: linear-gradient(135deg, var(--ft-primary-light) 0%, var(--ft-primary) 100%); + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4); +} + +.focus-task-btn-success { + background: var(--ft-success); + color: white; +} + +.focus-task-btn-success:hover { + background: #16a34a; +} + +.focus-task-btn-secondary { + background: var(--ft-bg-secondary); + border: 1px solid var(--ft-border); +} + +.focus-task-btn-danger { + background: transparent; + color: var(--ft-danger); + border: 1px solid var(--ft-danger); +} + +.focus-task-btn-danger:hover { + background: var(--ft-danger); + color: white; +} + +/* ============ Stats Bar ============ */ +.focus-task-stats-bar { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 12px; + margin-bottom: 20px; +} + +@media (max-width: 600px) { + .focus-task-stats-bar { + grid-template-columns: repeat(2, 1fr); + } +} + +.focus-task-stat-item { + background: var(--ft-bg-secondary); + border-radius: var(--ft-radius); + padding: 12px; + text-align: center; + transition: all 0.2s ease; +} + +.focus-task-stat-item:hover { + transform: translateY(-2px); + box-shadow: var(--ft-shadow); +} + +.focus-task-stat-icon { + font-size: 20px; + margin-bottom: 4px; +} + +.focus-task-stat-value { + font-size: 18px; + font-weight: 700; + color: var(--ft-primary); +} + +.focus-task-stat-label { + font-size: 11px; + color: var(--ft-text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* ============ Active Task Section ============ */ +.focus-task-active-section { + margin-bottom: 24px; +} + +.focus-task-start-prompt { + background: linear-gradient(135deg, var(--ft-bg-secondary) 0%, var(--ft-bg-tertiary) 100%); + border: 2px dashed var(--ft-border); + border-radius: var(--ft-radius); + padding: 32px; + text-align: center; +} + +.focus-task-prompt-icon { + font-size: 48px; + margin-bottom: 12px; + animation: ft-pulse 2s ease-in-out infinite; +} + +@keyframes ft-pulse { + 0%, 100% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.1); opacity: 0.8; } +} + +.focus-task-prompt-text { + font-size: 18px; + font-weight: 600; + color: var(--ft-text); + margin-bottom: 4px; +} + +.focus-task-prompt-hint { + font-size: 14px; + color: var(--ft-text-muted); +} + +/* ============ Active Task Card ============ */ +.focus-task-active-card { + background: linear-gradient(135deg, var(--ft-primary) 0%, var(--ft-primary-dark) 100%); + border-radius: var(--ft-radius); + padding: 24px; + color: white; + box-shadow: var(--ft-shadow-lg); + animation: ft-slideIn 0.3s ease; +} + +@keyframes ft-slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.focus-task-break-card { + background: linear-gradient(135deg, var(--ft-break) 0%, #0891b2 100%); +} + +.focus-task-active-label { + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1px; + opacity: 0.9; + margin-bottom: 8px; +} + +.focus-task-active-task-name { + font-size: 20px; + font-weight: 700; + margin-bottom: 16px; + word-break: break-word; +} + +.focus-task-timer-display { + text-align: center; + margin-bottom: 16px; +} + +.focus-task-timer-time { + font-size: 48px; + font-weight: 700; + font-variant-numeric: tabular-nums; + letter-spacing: 2px; +} + +.focus-task-timer-time.focus-task-overtime { + color: var(--ft-warning); +} + +/* Progress Bar */ +.focus-task-progress-wrap { + background: rgba(255, 255, 255, 0.2); + border-radius: 999px; + height: 8px; + overflow: hidden; + margin-bottom: 12px; +} + +.focus-task-progress-bar { + height: 100%; + background: rgba(255, 255, 255, 0.9); + border-radius: 999px; + transition: width 0.3s ease; +} + +.focus-task-progress-bar.focus-task-overtime { + background: var(--ft-warning); +} + +.focus-task-time-info { + display: flex; + justify-content: space-between; + font-size: 13px; + opacity: 0.9; + margin-bottom: 16px; +} + +.focus-task-active-controls { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.focus-task-active-controls .focus-task-btn { + flex: 1; + min-width: 80px; + background: rgba(255, 255, 255, 0.2); + color: white; + border: none; +} + +.focus-task-active-controls .focus-task-btn:hover { + background: rgba(255, 255, 255, 0.3); +} + +.focus-task-active-controls .focus-task-btn-success { + background: var(--ft-success); +} + +.focus-task-active-controls .focus-task-btn-danger { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.4); +} + +/* ============ Task List Section ============ */ +.focus-task-list-section { + flex: 1; +} + +/* Filters */ +.focus-task-filters { + display: flex; + gap: 8px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.focus-task-filter-btn { + padding: 6px 12px; + border-radius: 999px; + font-size: 13px; + background: var(--ft-bg-secondary); + border: 1px solid var(--ft-border); + color: var(--ft-text-muted); + cursor: pointer; + transition: all 0.2s ease; +} + +.focus-task-filter-btn:hover { + background: var(--ft-bg-tertiary); + color: var(--ft-text); +} + +.focus-task-filter-btn.active { + background: var(--ft-primary); + border-color: var(--ft-primary); + color: white; +} + +/* Task List */ +.focus-task-task-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +/* Empty State */ +.focus-task-empty-state { + text-align: center; + padding: 40px 20px; + color: var(--ft-text-muted); +} + +.focus-task-empty-icon { + font-size: 48px; + margin-bottom: 12px; + opacity: 0.5; +} + +.focus-task-empty-text { + font-size: 16px; + font-weight: 500; + margin-bottom: 4px; +} + +.focus-task-empty-hint { + font-size: 14px; +} + +/* ============ Task Item ============ */ +.focus-task-task-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + background: var(--ft-bg-secondary); + border-radius: var(--ft-radius-sm); + border: 1px solid transparent; + transition: all 0.2s ease; +} + +.focus-task-task-item:hover { + border-color: var(--ft-primary); + box-shadow: var(--ft-shadow); + transform: translateX(4px); +} + +.focus-task-task-item.active { + border-color: var(--ft-primary); + background: linear-gradient(90deg, rgba(99, 102, 241, 0.1) 0%, var(--ft-bg-secondary) 100%); +} + +.focus-task-task-item.completed { + opacity: 0.6; +} + +.focus-task-task-item.completed .focus-task-task-text { + text-decoration: line-through; +} + +/* Checkbox */ +.focus-task-checkbox { + width: 24px; + height: 24px; + min-width: 24px; + border: 2px solid var(--ft-primary); + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: 700; + cursor: pointer; + transition: all 0.2s ease; +} + +.focus-task-checkbox:hover { + transform: scale(1.1); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2); +} + +/* Task Content */ +.focus-task-task-content { + flex: 1; + min-width: 0; +} + +.focus-task-task-header { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 4px; +} + +.focus-task-task-text { + font-size: 14px; + font-weight: 500; + color: var(--ft-text); + word-break: break-word; +} + +.focus-task-list-badge { + font-size: 11px; + padding: 2px 8px; + border-radius: 999px; + font-weight: 500; +} + +.focus-task-task-meta { + display: flex; + gap: 12px; + font-size: 12px; + color: var(--ft-text-muted); +} + +.focus-task-overtime-text { + color: var(--ft-warning); +} + +/* Task Actions */ +.focus-task-task-actions { + display: flex; + gap: 4px; + opacity: 0; + transition: opacity 0.2s ease; +} + +.focus-task-task-item:hover .focus-task-task-actions { + opacity: 1; +} + +.focus-task-task-btn { + width: 32px; + height: 32px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + background: var(--ft-bg-tertiary); + border: none; + cursor: pointer; + font-size: 14px; + transition: all 0.2s ease; +} + +.focus-task-task-btn:hover { + background: var(--ft-primary); + color: white; + transform: scale(1.1); +} + +.focus-task-delete-btn:hover { + background: var(--ft-danger); +} + +/* ============ Floating Timer ============ */ +.focus-task-floating-timer { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 9999; + background: var(--background-primary); + border: 1px solid var(--background-modifier-border); + border-radius: 16px; + padding: 12px 16px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + cursor: move; + min-width: 180px; + transition: all 0.3s ease; +} + +.focus-task-floating-timer:hover { + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3); +} + +.focus-task-floating-timer.focus-task-active { + border-color: var(--ft-primary); + box-shadow: 0 8px 32px rgba(99, 102, 241, 0.3); +} + +.focus-task-floating-timer.focus-task-break-mode { + border-color: var(--ft-break); + box-shadow: 0 8px 32px rgba(6, 182, 212, 0.3); +} + +.focus-task-floating-inner { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.focus-task-floating-task { + font-size: 12px; + color: var(--text-muted); + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.focus-task-floating-time { + font-size: 28px; + font-weight: 700; + font-variant-numeric: tabular-nums; + color: var(--ft-primary); +} + +.focus-task-floating-time.focus-task-overtime { + color: var(--ft-warning); +} + +.focus-task-floating-controls { + display: flex; + gap: 8px; +} + +.focus-task-float-btn { + width: 36px; + height: 36px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background: var(--ft-bg-secondary); + border: 1px solid var(--ft-border); + cursor: pointer; + font-size: 14px; + transition: all 0.2s ease; +} + +.focus-task-float-btn:hover { + background: var(--ft-primary); + color: white; + border-color: var(--ft-primary); +} + +/* ============ Modal Styles ============ */ +.focus-task-modal { + padding: 20px; +} + +.focus-task-modal h2 { + margin-top: 0; + margin-bottom: 20px; +} + +.focus-task-modal-buttons { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid var(--ft-border); +} + +/* About section in settings */ +.focus-task-about { + background: var(--ft-bg-secondary); + border-radius: var(--ft-radius); + padding: 16px; + margin-top: 8px; +} + +.focus-task-about p { + margin: 0 0 12px 0; + line-height: 1.6; +} + +.focus-task-about p:last-child { + margin-bottom: 0; +} + +.focus-task-about a { + color: var(--ft-primary); +} + +/* ============ Scrollbar Styling ============ */ +.focus-task-container::-webkit-scrollbar { + width: 8px; +} + +.focus-task-container::-webkit-scrollbar-track { + background: transparent; +} + +.focus-task-container::-webkit-scrollbar-thumb { + background: var(--ft-border); + border-radius: 4px; +} + +.focus-task-container::-webkit-scrollbar-thumb:hover { + background: var(--ft-text-muted); +} + +/* ============ Animations ============ */ +@keyframes ft-checkComplete { + 0% { transform: scale(1); } + 50% { transform: scale(1.3); } + 100% { transform: scale(1); } +} + +.focus-task-checkbox.completing { + animation: ft-checkComplete 0.3s ease; +} + +/* ============ Dark mode adjustments ============ */ +.theme-dark .focus-task-floating-timer { + background: var(--background-secondary); +} + +/* ============ Mobile Responsive ============ */ +@media (max-width: 400px) { + .focus-task-header { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } + + .focus-task-active-controls { + flex-direction: column; + } + + .focus-task-active-controls .focus-task-btn { + flex: none; + width: 100%; + } + + .focus-task-task-item { + flex-wrap: wrap; + } + + .focus-task-task-actions { + opacity: 1; + width: 100%; + justify-content: flex-end; + margin-top: 8px; + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c44b729 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "inlineSourceMap": true, + "inlineSources": true, + "module": "ESNext", + "target": "ES6", + "allowJs": true, + "noImplicitAny": true, + "moduleResolution": "node", + "importHelpers": true, + "isolatedModules": true, + "strictNullChecks": true, + "lib": [ + "DOM", + "ES5", + "ES6", + "ES7" + ] + }, + "include": [ + "**/*.ts" + ] +} diff --git a/version-bump.mjs b/version-bump.mjs new file mode 100644 index 0000000..d409fa0 --- /dev/null +++ b/version-bump.mjs @@ -0,0 +1,14 @@ +import { readFileSync, writeFileSync } from "fs"; + +const targetVersion = process.env.npm_package_version; + +// read minAppVersion from manifest.json and bump version to target version +let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); +const { minAppVersion } = manifest; +manifest.version = targetVersion; +writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); + +// update versions.json with target version and minAppVersion from manifest.json +let versions = JSON.parse(readFileSync("versions.json", "utf8")); +versions[targetVersion] = minAppVersion; +writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..af9a39e --- /dev/null +++ b/versions.json @@ -0,0 +1,3 @@ +{ + "1.0.0": "0.15.0" +}