10 Commits

Author SHA1 Message Date
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
f5e4a16aff Bug Fixes - Updated Version v1.0.4 2025-11-22 20:31:57 +01:00
9014f086d6 Bug Fixes 2025-11-22 20:26:48 +01:00
d4f2af179f Bug Fixes 2025-11-22 20:26:18 +01:00
aeb1d62895 Bug fixes 2025-11-22 18:05:01 +01:00
9de2b00a2f Bug Fixes 2025-11-22 18:03:37 +01:00
66652afc90 Bug Fixes 2025-11-22 17:53:41 +01:00
8 changed files with 438 additions and 225 deletions

View File

@@ -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.3-blue?style=for-the-badge)
![Version](https://img.shields.io/badge/Version-1.0.5-blue?style=for-the-badge)
## 🎯 Overview
@@ -46,6 +46,7 @@ Focus Task brings the power of time-boxed task management directly into your Obs
- **Streak Counter**: Build momentum with consecutive productive days
- **Time Comparison**: Compare estimated vs. actual time to improve planning
- **Pomodoro Count**: Track total pomodoros completed
- **Daily Note Logging**: Automatically log completed tasks to your daily notes with timestamps and performance metrics
### 🎨 User Experience
- **Status Bar Timer**: timer that stays visible while you work
@@ -136,7 +137,7 @@ Best for: Tracking time on open-ended tasks
| 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 |
| Auto-start Breaks | Automatically start break timer | On |
### General
| Setting | Description | Default |
@@ -146,6 +147,27 @@ Best for: Tracking time on open-ended tasks
| Enable Celebrations | Show celebration messages | 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. Focus Task respects your Daily Notes configuration (folder, date format, and template).
### Lists
Customize your task lists with:
- Custom names

248
main.js
View File

@@ -43,12 +43,10 @@ var DEFAULT_SETTINGS = {
{ id: "personal", name: "Personal", color: "#22c55e", icon: "\u{1F3E0}" },
{ id: "learning", name: "Learning", color: "#f59e0b", icon: "\u{1F4DA}" }
],
autoStartBreak: false,
autoStartBreak: true,
tickSoundEnabled: false,
// Daily note logging
logToDaily: false,
dailyNoteFormat: "YYYY-MM-DD",
dailyNoteHeading: "## \u26A1 Completed Tasks"
logToDaily: false
};
var DEFAULT_DATA = {
tasks: [],
@@ -321,9 +319,11 @@ var FocusTaskView = class extends import_obsidian2.ItemView {
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: "\u2615 BREAK TIME" });
const breakLabel = this.plugin.currentTimerSeconds > 0 ? "\u2615 BREAK TIME" : "\u2728 BREAK COMPLETE";
activeCard.createEl("div", { cls: "focus-task-active-label", text: breakLabel });
} 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: "focus-task-active-label", text: workLabel });
}
activeCard.createEl("div", { cls: "focus-task-active-task-name", text: task.text });
const timerDisplay = activeCard.createEl("div", { cls: "focus-task-timer-display" });
@@ -350,25 +350,62 @@ var FocusTaskView = class extends import_obsidian2.ItemView {
this.actualTimeEl = timeInfo.createEl("span", { text: `Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}` });
}
const controls = activeCard.createEl("div", { cls: "focus-task-active-controls" });
this.pauseBtnEl = controls.createEl("button", { cls: "focus-task-btn focus-task-btn-secondary" });
this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? "\u23F8 Pause" : "\u25B6 Resume";
this.pauseBtnEl.addEventListener("click", () => this.plugin.toggleTimer());
if (!this.plugin.isBreakMode) {
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.addEventListener("click", () => {
this.plugin.isBreakMode = false;
this.plugin.stopTimer();
this.refresh();
});
if (this.plugin.currentTimerSeconds > 0) {
this.pauseBtnEl = controls.createEl("button", { cls: "focus-task-btn focus-task-btn-secondary" });
this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? "\u23F8 Pause" : "\u25B6 Resume";
this.pauseBtnEl.addEventListener("click", () => this.plugin.toggleTimer());
const skipBreakBtn = controls.createEl("button", { cls: "focus-task-btn focus-task-btn-primary" });
skipBreakBtn.innerHTML = "\u23ED Skip Break";
skipBreakBtn.addEventListener("click", () => {
this.plugin.isBreakMode = false;
this.plugin.startPomodoro(task.id);
});
const stopBtn = controls.createEl("button", { cls: "focus-task-btn focus-task-btn-danger" });
stopBtn.innerHTML = "\u2715 Stop";
stopBtn.addEventListener("click", () => {
this.plugin.isBreakMode = false;
this.plugin.stopTimer();
});
} else {
const resumeWorkBtn = controls.createEl("button", { cls: "focus-task-btn focus-task-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: "focus-task-btn focus-task-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: "focus-task-btn focus-task-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: "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());
} else {
const startBreakBtn = controls.createEl("button", { cls: "focus-task-btn focus-task-btn-secondary" });
startBreakBtn.innerHTML = "\u2615 Start Break";
startBreakBtn.addEventListener("click", () => this.plugin.startBreak());
const continueBtn = controls.createEl("button", { cls: "focus-task-btn focus-task-btn-primary" });
continueBtn.innerHTML = "\u25B6 Continue Working";
continueBtn.addEventListener("click", () => this.plugin.startPomodoro(task.id));
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());
}
}
}
} else {
@@ -501,6 +538,7 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
this.pomodoroCount = 0;
// Focus time tracking (in seconds for accuracy)
this.focusSecondsToday = 0;
this.secondsWorkedOnCurrentTask = 0;
// Status bar element
this.statusBarEl = null;
}
@@ -673,11 +711,13 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
this.isBreakMode = false;
this.currentTimerSeconds = 0;
this.isTimerRunning = true;
this.secondsWorkedOnCurrentTask = task.actualMinutes * 60;
this.refreshView();
this.updateStatusBar();
this.timerInterval = window.setInterval(() => {
this.currentTimerSeconds++;
task.actualMinutes = Math.floor(this.currentTimerSeconds / 60);
this.secondsWorkedOnCurrentTask++;
task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60);
this.focusSecondsToday++;
this.updateStatusBar();
this.updateTimerDisplay();
@@ -700,14 +740,14 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
this.isBreakMode = false;
this.currentTimerSeconds = this.settings.pomodoroWorkMinutes * 60;
this.isTimerRunning = true;
this.secondsWorkedOnCurrentTask = task.actualMinutes * 60;
this.refreshView();
this.updateStatusBar();
let secondsWorked = 0;
this.timerInterval = window.setInterval(() => {
this.currentTimerSeconds--;
if (!this.isBreakMode) {
secondsWorked++;
task.actualMinutes = Math.floor(secondsWorked / 60);
this.secondsWorkedOnCurrentTask++;
task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60);
this.focusSecondsToday++;
}
this.updateStatusBar();
@@ -718,6 +758,14 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
}, 1e3);
}
handlePomodoroEnd() {
if (this.timerInterval) {
window.clearInterval(this.timerInterval);
this.timerInterval = null;
}
this.currentTimerSeconds = 0;
this.isTimerRunning = false;
this.updateStatusBar();
this.updateTimerDisplay();
if (!this.isBreakMode) {
this.pomodoroCount++;
this.data.pomodorosCompleted++;
@@ -728,20 +776,20 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
if (this.settings.autoStartBreak) {
this.startBreak();
} else {
this.stopTimer();
this.refreshView();
}
} else {
if (this.settings.enableSounds) {
this.playAlertSound();
}
new import_obsidian3.Notice("\u26A1 Break over! Ready to focus?");
this.isBreakMode = false;
this.stopTimer();
this.refreshView();
}
this.saveAllData();
}
startBreak() {
this.isBreakMode = true;
this.isTimerRunning = true;
const isLongBreak = this.pomodoroCount % this.settings.longBreakInterval === 0;
this.currentTimerSeconds = (isLongBreak ? this.settings.longBreakMinutes : this.settings.pomodoroBreakMinutes) * 60;
new import_obsidian3.Notice(isLongBreak ? "\u2615 Long break time!" : "\u2615 Short break time!");
@@ -769,7 +817,8 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
this.timerInterval = window.setInterval(() => {
this.currentTimerSeconds--;
if (task && !this.isBreakMode) {
task.actualMinutes = Math.floor((this.settings.pomodoroWorkMinutes * 60 - this.currentTimerSeconds) / 60);
this.secondsWorkedOnCurrentTask++;
task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60);
this.focusSecondsToday++;
}
this.updateStatusBar();
@@ -797,6 +846,7 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
}
this.isTimerRunning = false;
this.activeTaskId = null;
this.secondsWorkedOnCurrentTask = 0;
this.updateStatusBar();
this.saveAllData();
this.refreshView();
@@ -888,7 +938,6 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
// ============ Daily Note Logging ============
async logTaskToDailyNote(task) {
try {
const dailyNotePath = this.getDailyNotePath();
const list = this.settings.lists.find((l) => l.id === task.list);
const timeDiff = task.actualMinutes - task.estimatedMinutes;
let timeComparison = "";
@@ -905,58 +954,91 @@ var FocusTaskPlugin = class extends import_obsidian3.Plugin {
hour12: false
});
const taskEntry = `- [x] ${task.text} | ${(list == null ? void 0 : list.icon) || "\u{1F4CB}"} ${(list == null ? void 0 : list.name) || "Task"} | \u23F1\uFE0F ${this.formatTimeHuman(task.actualMinutes)} / ${this.formatTimeHuman(task.estimatedMinutes)} (${timeComparison}) | \u2705 ${completedTime}`;
await this.appendToDailyNote(dailyNotePath, taskEntry);
await this.appendToDailyNote(taskEntry);
} catch (e) {
console.error("Failed to log task to daily note:", e);
new import_obsidian3.Notice("Failed to log task to daily note");
new import_obsidian3.Notice("Failed to log task to daily note. Make sure Daily Notes core plugin is enabled.");
}
}
getDailyNotePath() {
getDailyNoteSettings() {
var _a, _b, _c;
const dailyNotesPlugin = (_b = (_a = this.app.internalPlugins) == null ? void 0 : _a.plugins) == null ? void 0 : _b["daily-notes"];
if (!(dailyNotesPlugin == null ? void 0 : dailyNotesPlugin.enabled)) {
return null;
}
const settings = (_c = dailyNotesPlugin.instance) == null ? void 0 : _c.options;
return {
folder: (settings == null ? void 0 : settings.folder) || "",
format: (settings == null ? void 0 : settings.format) || "YYYY-MM-DD",
template: (settings == null ? void 0 : settings.template) || ""
};
}
formatDailyNoteDate(format) {
const now = new Date();
const format = this.settings.dailyNoteFormat;
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, "0");
const day = now.getDate().toString().padStart(2, "0");
let filename = format.replace("YYYY", year.toString()).replace("MM", month).replace("DD", day);
return `${filename}.md`;
const month = now.getMonth() + 1;
const day = now.getDate();
let result = format;
result = result.replace(/YYYY/g, year.toString());
result = result.replace(/YY/g, year.toString().slice(-2));
result = result.replace(/MMMM/g, now.toLocaleDateString("en-US", { month: "long" }));
result = result.replace(/MMM/g, now.toLocaleDateString("en-US", { month: "short" }));
result = result.replace(/MM/g, month.toString().padStart(2, "0"));
result = result.replace(/(?<![A-Za-z])M(?![A-Za-z])/g, month.toString());
result = result.replace(/dddd/g, now.toLocaleDateString("en-US", { weekday: "long" }));
result = result.replace(/ddd/g, now.toLocaleDateString("en-US", { weekday: "short" }));
result = result.replace(/DD/g, day.toString().padStart(2, "0"));
result = result.replace(/(?<![A-Za-z])D(?![A-Za-z])/g, day.toString());
return result;
}
async appendToDailyNote(path, content) {
async getOrCreateDailyNote() {
const { vault } = this.app;
const heading = this.settings.dailyNoteHeading;
const dailySettings = this.getDailyNoteSettings();
if (!dailySettings) {
new import_obsidian3.Notice("Daily Notes core plugin is not enabled. Please enable it in Settings \u2192 Core plugins.");
return null;
}
const filename = this.formatDailyNoteDate(dailySettings.format);
const folder = dailySettings.folder ? `${dailySettings.folder}/` : "";
const path = `${folder}${filename}.md`;
let file = vault.getAbstractFileByPath(path);
if (!file) {
await vault.create(path, `# ${new Date().toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" })}
${heading}
${content}
`);
new import_obsidian3.Notice("\u{1F4DD} Task logged to new daily note");
return;
if (file && file instanceof import_obsidian3.TFile) {
return file;
}
if (!(file instanceof import_obsidian3.TFile)) {
new import_obsidian3.Notice("Daily note path is not a file");
return;
}
let existingContent = await vault.read(file);
if (existingContent.includes(heading)) {
const headingIndex = existingContent.indexOf(heading);
const afterHeading = headingIndex + heading.length;
const nextHeadingMatch = existingContent.slice(afterHeading).match(/\n##? /);
let insertPosition;
if (nextHeadingMatch && nextHeadingMatch.index !== void 0) {
insertPosition = afterHeading + nextHeadingMatch.index;
} else {
insertPosition = existingContent.length;
try {
if (dailySettings.folder) {
const folderExists = vault.getAbstractFileByPath(dailySettings.folder);
if (!folderExists) {
await vault.createFolder(dailySettings.folder);
}
}
const before = existingContent.slice(0, insertPosition);
const after = existingContent.slice(insertPosition);
const newContent = before.trimEnd() + "\n" + content + "\n" + after.trimStart();
await vault.modify(file, newContent);
} else {
const newContent = existingContent.trimEnd() + "\n\n" + heading + "\n\n" + content + "\n";
await vault.modify(file, newContent);
let content = "";
if (dailySettings.template) {
const templatePath = dailySettings.template.endsWith(".md") ? dailySettings.template : `${dailySettings.template}.md`;
const templateFile = vault.getAbstractFileByPath(templatePath);
if (templateFile && templateFile instanceof import_obsidian3.TFile) {
content = await vault.read(templateFile);
content = content.replace(/{{date}}/g, filename).replace(/{{time}}/g, new Date().toLocaleTimeString()).replace(/{{title}}/g, filename);
}
}
const newFile = await vault.create(path, content);
new import_obsidian3.Notice(`\u{1F4DD} Created daily note: ${filename}`);
return newFile;
} catch (e) {
console.error("Failed to create daily note:", e);
new import_obsidian3.Notice("Failed to create daily note");
return null;
}
}
async appendToDailyNote(content) {
const file = await this.getOrCreateDailyNote();
if (!file) {
return;
}
const { vault } = this.app;
const existingContent = await vault.read(file);
const newContent = existingContent.trimEnd() + "\n" + content + "\n";
await vault.modify(file, newContent);
new import_obsidian3.Notice("\u{1F4DD} Task logged to daily note");
}
// ============ Utilities ============
@@ -1067,18 +1149,20 @@ var FocusTaskSettingTab = class extends import_obsidian3.PluginSettingTab {
await this.plugin.saveAllData();
}));
containerEl.createEl("h2", { text: "\u{1F4DD} Daily Note Integration" });
new import_obsidian3.Setting(containerEl).setName("Log completed tasks to daily note").setDesc("When you complete a task, add an entry to your daily note").addToggle((toggle) => toggle.setValue(this.plugin.settings.logToDaily).onChange(async (value) => {
new import_obsidian3.Setting(containerEl).setName("Log completed tasks to daily note").setDesc("When you complete a task, add an entry to your daily note. Uses the core Daily Notes plugin settings.").addToggle((toggle) => toggle.setValue(this.plugin.settings.logToDaily).onChange(async (value) => {
this.plugin.settings.logToDaily = value;
await this.plugin.saveAllData();
}));
new import_obsidian3.Setting(containerEl).setName("Daily note filename format").setDesc("Format for daily note filename (YYYY = year, MM = month, DD = day)").addText((text) => text.setPlaceholder("YYYY-MM-DD").setValue(this.plugin.settings.dailyNoteFormat).onChange(async (value) => {
this.plugin.settings.dailyNoteFormat = value || "YYYY-MM-DD";
await this.plugin.saveAllData();
}));
new import_obsidian3.Setting(containerEl).setName("Section heading").setDesc("Heading under which completed tasks will be logged").addText((text) => text.setPlaceholder("## \u26A1 Completed Tasks").setValue(this.plugin.settings.dailyNoteHeading).onChange(async (value) => {
this.plugin.settings.dailyNoteHeading = value || "## \u26A1 Completed Tasks";
await this.plugin.saveAllData();
}));
const infoEl = containerEl.createEl("div", { cls: "setting-item-description" });
infoEl.style.marginTop = "-10px";
infoEl.style.marginBottom = "20px";
infoEl.innerHTML = `
<small>
This feature uses the <strong>Daily Notes</strong> core plugin.
Configure your daily note folder, date format, and template in
<em>Settings \u2192 Core plugins \u2192 Daily notes</em>.
</small>
`;
containerEl.createEl("h2", { text: "\u{1F4CB} Lists" });
this.plugin.settings.lists.forEach((list, index) => {
new import_obsidian3.Setting(containerEl).setName(`${list.icon} ${list.name}`).addText((text) => text.setValue(list.name).setPlaceholder("List name").onChange(async (value) => {

View File

@@ -1,7 +1,7 @@
{
"id": "focus-task",
"name": "Focus Task",
"version": "1.0.3",
"version": "1.0.5",
"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",

View File

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

View File

@@ -36,9 +36,10 @@ export default class FocusTaskPlugin extends Plugin {
isBreakMode: boolean = false;
activeTaskId: string | null = null;
pomodoroCount: number = 0;
// Focus time tracking (in seconds for accuracy)
private focusSecondsToday: number = 0;
private secondsWorkedOnCurrentTask: number = 0;
// Status bar element
statusBarEl: HTMLElement | null = null;
@@ -263,6 +264,7 @@ export default class FocusTaskPlugin extends Plugin {
this.isBreakMode = false;
this.currentTimerSeconds = 0;
this.isTimerRunning = true;
this.secondsWorkedOnCurrentTask = task.actualMinutes * 60;
// Full refresh to show the active task card
this.refreshView();
@@ -271,7 +273,8 @@ export default class FocusTaskPlugin extends Plugin {
// Start interval (count up mode - stopwatch)
this.timerInterval = window.setInterval(() => {
this.currentTimerSeconds++;
task.actualMinutes = Math.floor(this.currentTimerSeconds / 60);
this.secondsWorkedOnCurrentTask++;
task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60);
// Track focus time
this.focusSecondsToday++;
@@ -303,23 +306,23 @@ export default class FocusTaskPlugin extends Plugin {
this.currentTimerSeconds = this.settings.pomodoroWorkMinutes * 60;
this.isTimerRunning = true;
// Initialize from existing actual time to preserve progress across breaks
this.secondsWorkedOnCurrentTask = task.actualMinutes * 60;
// Full refresh to show the active task card
this.refreshView();
this.updateStatusBar();
// Track seconds worked for accurate focus time
let secondsWorked = 0;
this.timerInterval = window.setInterval(() => {
this.currentTimerSeconds--;
if (!this.isBreakMode) {
secondsWorked++;
task.actualMinutes = Math.floor(secondsWorked / 60);
this.secondsWorkedOnCurrentTask++;
task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60);
// Increment focus time by 1 second
this.focusSecondsToday++;
}
// Light update - only timer display, no full refresh
this.updateStatusBar();
this.updateTimerDisplay();
@@ -331,45 +334,61 @@ export default class FocusTaskPlugin extends Plugin {
}
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) {
// 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();
this.refreshView();
}
} else {
// Break ended
// Break ended - keep timer at 0 until user resumes
if (this.settings.enableSounds) {
this.playAlertSound();
}
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();
}
startBreak() {
this.isBreakMode = true;
this.isTimerRunning = 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!');
// Full refresh to show break state
this.refreshView();
if (!this.timerInterval) {
this.timerInterval = window.setInterval(() => {
this.currentTimerSeconds--;
@@ -382,7 +401,7 @@ export default class FocusTaskPlugin extends Plugin {
}
}, 1000);
}
this.updateStatusBar();
}
@@ -396,11 +415,12 @@ export default class FocusTaskPlugin extends Plugin {
// 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.secondsWorkedOnCurrentTask++;
task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60);
// Track focus time
this.focusSecondsToday++;
}
@@ -415,7 +435,7 @@ export default class FocusTaskPlugin extends Plugin {
} else {
new Notice('No active task. Select a task first.');
}
// Full refresh to update pause/resume button state
this.updateStatusBar();
this.refreshView();
@@ -426,16 +446,17 @@ export default class FocusTaskPlugin extends Plugin {
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.secondsWorkedOnCurrentTask = 0;
this.updateStatusBar();
this.saveAllData();
this.refreshView();
@@ -551,7 +572,6 @@ export default class FocusTaskPlugin extends Plugin {
async logTaskToDailyNote(task: FocusTask) {
try {
const dailyNotePath = this.getDailyNotePath();
const list = this.settings.lists.find(l => l.id === task.list);
// Format the task entry
@@ -573,83 +593,133 @@ export default class FocusTaskPlugin extends Plugin {
const taskEntry = `- [x] ${task.text} | ${list?.icon || '📋'} ${list?.name || 'Task'} | ⏱️ ${this.formatTimeHuman(task.actualMinutes)} / ${this.formatTimeHuman(task.estimatedMinutes)} (${timeComparison}) | ✅ ${completedTime}`;
await this.appendToDailyNote(dailyNotePath, taskEntry);
await this.appendToDailyNote(taskEntry);
} catch (e) {
console.error('Failed to log task to daily note:', e);
new Notice('Failed to log task to daily note');
new Notice('Failed to log task to daily note. Make sure Daily Notes core plugin is enabled.');
}
}
getDailyNotePath(): string {
const now = new Date();
const format = this.settings.dailyNoteFormat;
getDailyNoteSettings(): { folder: string; format: string; template: string } | null {
// Access the core Daily Notes plugin settings
const dailyNotesPlugin = (this.app as any).internalPlugins?.plugins?.['daily-notes'];
// Simple date formatting
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
if (!dailyNotesPlugin?.enabled) {
return null;
}
let filename = format
.replace('YYYY', year.toString())
.replace('MM', month)
.replace('DD', day);
return `${filename}.md`;
const settings = dailyNotesPlugin.instance?.options;
return {
folder: settings?.folder || '',
format: settings?.format || 'YYYY-MM-DD',
template: settings?.template || '',
};
}
async appendToDailyNote(path: string, content: string) {
const { vault } = this.app;
const heading = this.settings.dailyNoteHeading;
formatDailyNoteDate(format: string): string {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
const day = now.getDate();
// Use placeholders to avoid replacement conflicts
// Replace longer tokens first, use unique placeholders
let result = format;
// Year tokens
result = result.replace(/YYYY/g, year.toString());
result = result.replace(/YY/g, year.toString().slice(-2));
// Month tokens (longer first)
result = result.replace(/MMMM/g, now.toLocaleDateString('en-US', { month: 'long' }));
result = result.replace(/MMM/g, now.toLocaleDateString('en-US', { month: 'short' }));
result = result.replace(/MM/g, month.toString().padStart(2, '0'));
// Only replace standalone M, not part of other tokens
result = result.replace(/(?<![A-Za-z])M(?![A-Za-z])/g, month.toString());
// Day tokens (longer first)
result = result.replace(/dddd/g, now.toLocaleDateString('en-US', { weekday: 'long' }));
result = result.replace(/ddd/g, now.toLocaleDateString('en-US', { weekday: 'short' }));
result = result.replace(/DD/g, day.toString().padStart(2, '0'));
// Only replace standalone D, not part of other tokens
result = result.replace(/(?<![A-Za-z])D(?![A-Za-z])/g, day.toString());
return result;
}
async getOrCreateDailyNote(): Promise<TFile | null> {
const { vault } = this.app;
const dailySettings = this.getDailyNoteSettings();
if (!dailySettings) {
new Notice('Daily Notes core plugin is not enabled. Please enable it in Settings → Core plugins.');
return null;
}
const filename = this.formatDailyNoteDate(dailySettings.format);
const folder = dailySettings.folder ? `${dailySettings.folder}/` : '';
const path = `${folder}${filename}.md`;
// Check if daily note exists
let file = vault.getAbstractFileByPath(path);
if (!file) {
// Create the daily note if it doesn't exist
await vault.create(path, `# ${new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}\n\n${heading}\n\n${content}\n`);
new Notice('📝 Task logged to new daily note');
return;
if (file && file instanceof TFile) {
return file;
}
if (!(file instanceof TFile)) {
new Notice('Daily note path is not a file');
return;
}
// Read existing content
let existingContent = await vault.read(file);
// Check if heading exists
if (existingContent.includes(heading)) {
// Find the heading and append after it
const headingIndex = existingContent.indexOf(heading);
const afterHeading = headingIndex + heading.length;
// Find the next heading or end of file
const nextHeadingMatch = existingContent.slice(afterHeading).match(/\n##? /);
let insertPosition: number;
if (nextHeadingMatch && nextHeadingMatch.index !== undefined) {
// Insert before the next heading
insertPosition = afterHeading + nextHeadingMatch.index;
} else {
// Append to end of file
insertPosition = existingContent.length;
// Create the daily note
try {
// Ensure folder exists
if (dailySettings.folder) {
const folderExists = vault.getAbstractFileByPath(dailySettings.folder);
if (!folderExists) {
await vault.createFolder(dailySettings.folder);
}
}
// Insert the new task entry
const before = existingContent.slice(0, insertPosition);
const after = existingContent.slice(insertPosition);
// Check if template exists and use it
let content = '';
if (dailySettings.template) {
const templatePath = dailySettings.template.endsWith('.md')
? dailySettings.template
: `${dailySettings.template}.md`;
const templateFile = vault.getAbstractFileByPath(templatePath);
if (templateFile && templateFile instanceof TFile) {
content = await vault.read(templateFile);
// Replace template variables
content = content
.replace(/{{date}}/g, filename)
.replace(/{{time}}/g, new Date().toLocaleTimeString())
.replace(/{{title}}/g, filename);
}
}
// Make sure there's a newline before our content
const newContent = before.trimEnd() + '\n' + content + '\n' + after.trimStart();
await vault.modify(file, newContent);
} else {
// Add the heading and content at the end
const newContent = existingContent.trimEnd() + '\n\n' + heading + '\n\n' + content + '\n';
await vault.modify(file, newContent);
// Create the file
const newFile = await vault.create(path, content);
new Notice(`📝 Created daily note: ${filename}`);
return newFile;
} catch (e) {
console.error('Failed to create daily note:', e);
new Notice('Failed to create daily note');
return null;
}
}
async appendToDailyNote(content: string) {
const file = await this.getOrCreateDailyNote();
if (!file) {
return;
}
const { vault } = this.app;
// Read existing content and append to the end
const existingContent = await vault.read(file);
const newContent = existingContent.trimEnd() + '\n' + content + '\n';
await vault.modify(file, newContent);
new Notice('📝 Task logged to daily note');
}
@@ -843,7 +913,7 @@ class FocusTaskSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName('Log completed tasks to daily note')
.setDesc('When you complete a task, add an entry to your daily note')
.setDesc('When you complete a task, add an entry to your daily note. Uses the core Daily Notes plugin settings.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.logToDaily)
.onChange(async value => {
@@ -851,27 +921,17 @@ class FocusTaskSettingTab extends PluginSettingTab {
await this.plugin.saveAllData();
}));
new Setting(containerEl)
.setName('Daily note filename format')
.setDesc('Format for daily note filename (YYYY = year, MM = month, DD = day)')
.addText(text => text
.setPlaceholder('YYYY-MM-DD')
.setValue(this.plugin.settings.dailyNoteFormat)
.onChange(async value => {
this.plugin.settings.dailyNoteFormat = value || 'YYYY-MM-DD';
await this.plugin.saveAllData();
}));
new Setting(containerEl)
.setName('Section heading')
.setDesc('Heading under which completed tasks will be logged')
.addText(text => text
.setPlaceholder('## ⚡ Completed Tasks')
.setValue(this.plugin.settings.dailyNoteHeading)
.onChange(async value => {
this.plugin.settings.dailyNoteHeading = value || '## ⚡ Completed Tasks';
await this.plugin.saveAllData();
}));
// Show info about Daily Notes plugin
const infoEl = containerEl.createEl('div', { cls: 'setting-item-description' });
infoEl.style.marginTop = '-10px';
infoEl.style.marginBottom = '20px';
infoEl.innerHTML = `
<small>
This feature uses the <strong>Daily Notes</strong> core plugin.
Configure your daily note folder, date format, and template in
<em>Settings → Core plugins → Daily notes</em>.
</small>
`;
// Lists Management
containerEl.createEl('h2', { text: '📋 Lists' });
@@ -937,4 +997,4 @@ class FocusTaskSettingTab extends PluginSettingTab {
</p>
`;
}
}
}

View File

@@ -34,8 +34,6 @@ export interface FocusTaskSettings {
tickSoundEnabled: boolean;
// Daily note logging
logToDaily: boolean;
dailyNoteFormat: string;
dailyNoteHeading: string;
}
export interface FocusTaskData {
@@ -60,12 +58,10 @@ export const DEFAULT_SETTINGS: FocusTaskSettings = {
{ id: 'personal', name: 'Personal', color: '#22c55e', icon: '🏠' },
{ id: 'learning', name: 'Learning', color: '#f59e0b', icon: '📚' },
],
autoStartBreak: false,
autoStartBreak: true,
tickSoundEnabled: false,
// Daily note logging
logToDaily: false,
dailyNoteFormat: 'YYYY-MM-DD',
dailyNoteHeading: '## ⚡ Completed Tasks',
};
export const DEFAULT_DATA: FocusTaskData = {
@@ -104,4 +100,4 @@ export const OVERTIME_MESSAGES = [
{ emoji: '💪', message: 'Persistence pays off!' },
{ emoji: '🏃', message: 'Marathon runner!' },
{ emoji: '🔥', message: 'The grind is real!' },
];
];

View File

@@ -145,9 +145,11 @@ export class FocusTaskView extends ItemView {
if (this.plugin.isBreakMode) {
activeCard.addClass('focus-task-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: 'focus-task-active-label', text: breakLabel });
} 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: 'focus-task-active-label', text: workLabel });
}
activeCard.createEl('div', { cls: 'focus-task-active-task-name', text: task.text });
@@ -186,29 +188,77 @@ export class FocusTaskView extends ItemView {
// Controls
const controls = activeCard.createEl('div', { cls: 'focus-task-active-controls' });
this.pauseBtnEl = controls.createEl('button', { cls: 'focus-task-btn focus-task-btn-secondary' });
this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? '⏸ Pause' : '▶ Resume';
this.pauseBtnEl.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();
});
// Break mode controls
if (this.plugin.currentTimerSeconds > 0) {
// Break is still counting down
this.pauseBtnEl = controls.createEl('button', { cls: 'focus-task-btn focus-task-btn-secondary' });
this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? '⏸ Pause' : '▶ Resume';
this.pauseBtnEl.addEventListener('click', () => this.plugin.toggleTimer());
const skipBreakBtn = controls.createEl('button', { cls: 'focus-task-btn focus-task-btn-primary' });
skipBreakBtn.innerHTML = '⏭ Skip Break';
skipBreakBtn.addEventListener('click', () => {
this.plugin.isBreakMode = false;
this.plugin.startPomodoro(task.id);
});
const stopBtn = controls.createEl('button', { cls: 'focus-task-btn focus-task-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: 'focus-task-btn focus-task-btn-success' });
resumeWorkBtn.innerHTML = '▶ Resume Work';
resumeWorkBtn.addEventListener('click', () => {
this.plugin.isBreakMode = false;
this.plugin.startPomodoro(task.id);
});
const stopBtn = controls.createEl('button', { cls: 'focus-task-btn focus-task-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: 'focus-task-btn focus-task-btn-secondary' });
this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? '⏸ Pause' : '▶ Resume';
this.pauseBtnEl.addEventListener('click', () => this.plugin.toggleTimer());
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());
} else {
// Work session finished - show break and completion options
const startBreakBtn = controls.createEl('button', { cls: 'focus-task-btn focus-task-btn-secondary' });
startBreakBtn.innerHTML = '☕ Start Break';
startBreakBtn.addEventListener('click', () => this.plugin.startBreak());
const continueBtn = controls.createEl('button', { cls: 'focus-task-btn focus-task-btn-primary' });
continueBtn.innerHTML = '▶ Continue Working';
continueBtn.addEventListener('click', () => this.plugin.startPomodoro(task.id));
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());
}
}
}
} else {

View File

@@ -1,3 +1,4 @@
{
"1.0.3": "0.15.0"
"1.0.4": "0.15.0",
"1.0.5": "0.15.0"
}