diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..2206350 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run build:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.gitignore b/.gitignore index bf32a71..66ca05e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Build output *.js.map +release/ +*.zip # npm node_modules/ diff --git a/README.md b/README.md index 0972b2c..3d8f41a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A powerful task management and focus timer plugin for [Obsidian](https://obsidia ![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.5-blue?style=for-the-badge) +![Version](https://img.shields.io/badge/Version-1.0.6-blue?style=for-the-badge) ## šŸŽÆ Overview diff --git a/main.js b/main.js index 4f68a0e..caa4691 100644 --- a/main.js +++ b/main.js @@ -536,6 +536,9 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin { this.isBreakMode = false; this.activeTaskId = null; this.pomodoroCount = 0; + // Timestamp-based tracking for reliable background timing + this.timerStartTimestamp = 0; + this.pausedTimeRemaining = 0; // Focus time tracking (in seconds for accuracy) this.focusSecondsToday = 0; this.secondsWorkedOnCurrentTask = 0; @@ -545,6 +548,11 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin { async onload() { await this.loadAllData(); this.checkDailyReset(); + document.addEventListener("visibilitychange", () => { + if (!document.hidden && this.isTimerRunning) { + this.syncTimerFromTimestamp(); + } + }); this.registerView( VIEW_TYPE_FOCUS_TASK, (leaf) => new FocusTaskView(leaf, this) @@ -701,6 +709,41 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin { } } // ============ Timer Management ============ + // Sync timer based on timestamp when app returns from background + syncTimerFromTimestamp() { + if (!this.isTimerRunning || this.timerStartTimestamp === 0) + return; + const now = Date.now(); + const elapsedMs = now - this.timerStartTimestamp; + const elapsedSeconds = Math.floor(elapsedMs / 1e3); + if (this.isBreakMode) { + this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds); + if (this.currentTimerSeconds <= 0) { + this.handlePomodoroEnd(); + } + } else { + const task = this.data.tasks.find((t) => t.id === this.activeTaskId); + if (!task) + return; + if (this.pausedTimeRemaining > 0) { + this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds); + this.secondsWorkedOnCurrentTask += elapsedSeconds; + task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60); + this.focusSecondsToday += elapsedSeconds; + if (this.currentTimerSeconds <= 0) { + this.handlePomodoroEnd(); + } + } else { + this.currentTimerSeconds += elapsedSeconds; + this.secondsWorkedOnCurrentTask += elapsedSeconds; + task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60); + this.focusSecondsToday += elapsedSeconds; + } + } + this.updateStatusBar(); + this.updateTimerDisplay(); + this.saveAllData(); + } startTimer(taskId) { const task = this.data.tasks.find((t) => t.id === taskId); if (!task) @@ -712,6 +755,8 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin { this.currentTimerSeconds = 0; this.isTimerRunning = true; this.secondsWorkedOnCurrentTask = task.actualMinutes * 60; + this.timerStartTimestamp = Date.now(); + this.pausedTimeRemaining = 0; this.refreshView(); this.updateStatusBar(); this.timerInterval = window.setInterval(() => { @@ -741,6 +786,8 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin { this.currentTimerSeconds = this.settings.pomodoroWorkMinutes * 60; this.isTimerRunning = true; this.secondsWorkedOnCurrentTask = task.actualMinutes * 60; + this.timerStartTimestamp = Date.now(); + this.pausedTimeRemaining = this.currentTimerSeconds; this.refreshView(); this.updateStatusBar(); this.timerInterval = window.setInterval(() => { @@ -792,6 +839,8 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin { this.isTimerRunning = true; const isLongBreak = this.pomodoroCount % this.settings.longBreakInterval === 0; 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!"); this.refreshView(); if (!this.timerInterval) { @@ -811,8 +860,10 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin { window.clearInterval(this.timerInterval); this.timerInterval = null; this.isTimerRunning = false; + this.pausedTimeRemaining = this.currentTimerSeconds; } else if (this.activeTaskId) { this.isTimerRunning = true; + this.timerStartTimestamp = Date.now(); const task = this.data.tasks.find((t) => t.id === this.activeTaskId); this.timerInterval = window.setInterval(() => { this.currentTimerSeconds--; @@ -847,6 +898,8 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin { this.isTimerRunning = false; this.activeTaskId = null; this.secondsWorkedOnCurrentTask = 0; + this.timerStartTimestamp = 0; + this.pausedTimeRemaining = 0; this.updateStatusBar(); this.saveAllData(); this.refreshView(); diff --git a/manifest.json b/manifest.json index c0a5786..cc2e6ba 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "focus-task", "name": "Focus Task", - "version": "1.0.5", + "version": "1.0.6", "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", diff --git a/package-release.mjs b/package-release.mjs new file mode 100644 index 0000000..aca867c --- /dev/null +++ b/package-release.mjs @@ -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 ../focus-task-${manifest.version}.zip *`); + + } catch (error) { + console.error('āŒ Error packaging release:', error); + process.exit(1); + } +} + +packageRelease(); diff --git a/package.json b/package.json index bd3e5e7..87b77c0 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "focus-task", - "version": "1.0.5", + "version": "1.0.6", "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", + "package": "npm run build && node package-release.mjs", "version": "node version-bump.mjs && git add manifest.json versions.json" }, "keywords": [ diff --git a/src/main.ts b/src/main.ts index 74381d6..9795333 100644 --- a/src/main.ts +++ b/src/main.ts @@ -37,6 +37,10 @@ export default class FocusTaskPlugin extends Plugin { activeTaskId: string | null = null; 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) private focusSecondsToday: number = 0; private secondsWorkedOnCurrentTask: number = 0; @@ -46,10 +50,17 @@ export default class FocusTaskPlugin extends Plugin { async onload() { await this.loadAllData(); - + // Check and reset daily stats 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 this.registerView( VIEW_TYPE_FOCUS_TASK, @@ -251,6 +262,53 @@ export default class FocusTaskPlugin extends Plugin { // ============ Timer Management ============ + // Sync timer based on timestamp when app returns from background + syncTimerFromTimestamp() { + if (!this.isTimerRunning || this.timerStartTimestamp === 0) return; + + const now = Date.now(); + const elapsedMs = now - this.timerStartTimestamp; + const elapsedSeconds = Math.floor(elapsedMs / 1000); + + if (this.isBreakMode) { + // Break mode: countdown timer + this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds); + + if (this.currentTimerSeconds <= 0) { + this.handlePomodoroEnd(); + } + } else { + // Work mode: check if it's pomodoro (countdown) or stopwatch (count up) + const task = this.data.tasks.find(t => t.id === this.activeTaskId); + if (!task) return; + + if (this.pausedTimeRemaining > 0) { + // Pomodoro countdown mode + this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds); + + // Update actual minutes worked + this.secondsWorkedOnCurrentTask += elapsedSeconds; + task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60); + this.focusSecondsToday += elapsedSeconds; + + if (this.currentTimerSeconds <= 0) { + this.handlePomodoroEnd(); + } + } else { + // Stopwatch count up mode + this.currentTimerSeconds += elapsedSeconds; + this.secondsWorkedOnCurrentTask += elapsedSeconds; + task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60); + this.focusSecondsToday += elapsedSeconds; + } + } + + // Update displays + this.updateStatusBar(); + this.updateTimerDisplay(); + this.saveAllData(); + } + startTimer(taskId: string) { const task = this.data.tasks.find(t => t.id === taskId); if (!task) return; @@ -266,6 +324,10 @@ export default class FocusTaskPlugin extends Plugin { 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 + // Full refresh to show the active task card this.refreshView(); this.updateStatusBar(); @@ -275,14 +337,14 @@ export default class FocusTaskPlugin extends Plugin { this.currentTimerSeconds++; this.secondsWorkedOnCurrentTask++; task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60); - + // Track focus time this.focusSecondsToday++; - + // Light update - only timer display, no full refresh this.updateStatusBar(); this.updateTimerDisplay(); - + // Check if over estimate if (this.currentTimerSeconds === task.estimatedMinutes * 60) { if (this.settings.enableSounds) { @@ -309,6 +371,10 @@ export default class FocusTaskPlugin extends Plugin { // Initialize from existing actual time to preserve progress across breaks this.secondsWorkedOnCurrentTask = task.actualMinutes * 60; + // Set timestamp for background tracking (pomodoro countdown mode) + this.timerStartTimestamp = Date.now(); + this.pausedTimeRemaining = this.currentTimerSeconds; + // Full refresh to show the active task card this.refreshView(); this.updateStatusBar(); @@ -384,6 +450,10 @@ export default class FocusTaskPlugin extends Plugin { const isLongBreak = this.pomodoroCount % this.settings.longBreakInterval === 0; 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!'); // Full refresh to show break state @@ -407,13 +477,15 @@ export default class FocusTaskPlugin extends Plugin { toggleTimer() { if (this.isTimerRunning && this.timerInterval) { - // Pause + // Pause - save current state window.clearInterval(this.timerInterval); this.timerInterval = null; this.isTimerRunning = false; + this.pausedTimeRemaining = this.currentTimerSeconds; } else if (this.activeTaskId) { - // Resume + // Resume - restart with new timestamp this.isTimerRunning = true; + this.timerStartTimestamp = Date.now(); const task = this.data.tasks.find(t => t.id === this.activeTaskId); this.timerInterval = window.setInterval(() => { @@ -457,6 +529,8 @@ export default class FocusTaskPlugin extends Plugin { this.isTimerRunning = false; this.activeTaskId = null; this.secondsWorkedOnCurrentTask = 0; + this.timerStartTimestamp = 0; + this.pausedTimeRemaining = 0; this.updateStatusBar(); this.saveAllData(); this.refreshView(); diff --git a/versions.json b/versions.json index 5c6f2b0..baedea4 100644 --- a/versions.json +++ b/versions.json @@ -1,4 +1,5 @@ { "1.0.4": "0.15.0", - "1.0.5": "0.15.0" + "1.0.5": "0.15.0", + "1.0.6": "0.15.0" }