diff --git a/.gitignore b/.gitignore index d492a75..bf32a71 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ # Build output -main.js *.js.map # npm @@ -16,4 +15,4 @@ package-lock.json Thumbs.db # Obsidian -data.json +data.json \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..3c11017 --- /dev/null +++ b/main.js @@ -0,0 +1,1018 @@ +/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ + +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// src/main.ts +var main_exports = {}; +__export(main_exports, { + default: () => FocusTaskPlugin +}); +module.exports = __toCommonJS(main_exports); +var import_obsidian3 = require("obsidian"); + +// src/types.ts +var DEFAULT_SETTINGS = { + pomodoroWorkMinutes: 25, + pomodoroBreakMinutes: 5, + longBreakMinutes: 15, + longBreakInterval: 4, + enableSounds: true, + enableCelebrations: true, + defaultEstimateMinutes: 30, + lists: [ + { id: "work", name: "Work", color: "#6366f1", icon: "\u{1F4BC}" }, + { id: "personal", name: "Personal", color: "#22c55e", icon: "\u{1F3E0}" }, + { id: "learning", name: "Learning", color: "#f59e0b", icon: "\u{1F4DA}" } + ], + autoStartBreak: false, + tickSoundEnabled: false +}; +var DEFAULT_DATA = { + tasks: [], + completedToday: 0, + totalFocusMinutesToday: 0, + streak: 0, + lastActiveDate: "", + pomodorosCompleted: 0 +}; +var VIEW_TYPE_FOCUS_TASK = "focus-task-view"; +var CELEBRATION_MESSAGES = [ + { emoji: "\u{1F4A5}", message: "Crushed it!" }, + { emoji: "\u{1F525}", message: "On fire!" }, + { emoji: "\u26A1", message: "Lightning fast!" }, + { emoji: "\u{1F3AF}", message: "Bullseye!" }, + { emoji: "\u{1F680}", message: "Blasting through!" }, + { emoji: "\u{1F4AA}", message: "Strong work!" }, + { emoji: "\u{1F31F}", message: "Stellar!" }, + { emoji: "\u2728", message: "Brilliant!" }, + { emoji: "\u{1F3C6}", message: "Champion!" }, + { emoji: "\u{1F389}", message: "Well done!" } +]; +var EARLY_FINISH_MESSAGES = [ + { emoji: "\u26A1", message: "Speed demon! Finished early!" }, + { emoji: "\u{1F3CE}\uFE0F", message: "Faster than expected!" }, + { emoji: "\u{1F3AF}", message: "Under budget! Nice work!" } +]; +var OVERTIME_MESSAGES = [ + { emoji: "\u{1F4AA}", message: "Persistence pays off!" }, + { emoji: "\u{1F3C3}", message: "Marathon runner!" }, + { emoji: "\u{1F525}", message: "The grind is real!" } +]; + +// src/view.ts +var import_obsidian2 = require("obsidian"); + +// src/modals.ts +var import_obsidian = require("obsidian"); +var QuickAddTaskModal = class extends import_obsidian.Modal { + constructor(app, plugin) { + super(app); + this.taskText = ""; + this.selectedList = "work"; + 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: "\u26A1 Add New Task" }); + new import_obsidian.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(); + } + }); + }); + new import_obsidian.Setting(contentEl).setName("Estimated Time").setDesc("How long do you think this will take?").addDropdown((dropdown) => { + const options = { + "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)); + }); + new import_obsidian.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); + }); + 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 import_obsidian.Notice("\u2705 Task added!"); + this.close(); + } else { + new import_obsidian.Notice("Please enter a task description"); + } + } + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +}; +var EditTaskModal = class extends import_obsidian.Modal { + constructor(app, plugin, task) { + super(app); + this.plugin = plugin; + this.task = { ...task }; + } + onOpen() { + const { contentEl } = this; + contentEl.addClass("focus-task-modal"); + contentEl.createEl("h2", { text: "\u270F\uFE0F Edit Task" }); + new import_obsidian.Setting(contentEl).setName("Task").addText((text) => text.setValue(this.task.text).onChange((value) => this.task.text = value)); + new import_obsidian.Setting(contentEl).setName("Estimated Time").addDropdown((dropdown) => { + const options = { + "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 import_obsidian.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 import_obsidian.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; + }); + if (this.task.actualMinutes > 0) { + new import_obsidian.Setting(contentEl).setName("Time Tracked").setDesc(`You've worked on this task for ${this.plugin.formatTimeHuman(this.task.actualMinutes)}`); + } + 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 import_obsidian.Notice("\u2705 Task updated!"); + this.close(); + }); + } + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +}; + +// src/view.ts +var FocusTaskView = class extends import_obsidian2.ItemView { + constructor(leaf, plugin) { + super(leaf); + this.currentFilter = "all"; + // References to elements that need frequent updates + this.timerTimeEl = null; + this.progressBarEl = null; + this.actualTimeEl = null; + this.pauseBtnEl = null; + this.plugin = plugin; + } + getViewType() { + return VIEW_TYPE_FOCUS_TASK; + } + getDisplayText() { + return "Focus Task"; + } + getIcon() { + return "zap"; + } + async onOpen() { + this.refresh(); + } + // Light update - only updates timer display without rebuilding DOM + updateTimerDisplay() { + if (!this.timerTimeEl) + return; + this.timerTimeEl.textContent = this.plugin.formatTime(this.plugin.currentTimerSeconds); + if (this.progressBarEl) { + 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; + } + this.progressBarEl.style.width = `${Math.min(Math.max(progressPercent, 0), 100)}%`; + } + if (this.actualTimeEl && this.plugin.activeTaskId) { + const task = this.plugin.data.tasks.find((t) => t.id === this.plugin.activeTaskId); + if (task) { + this.actualTimeEl.textContent = `Actual: ${this.plugin.formatTimeHuman(task.actualMinutes)}`; + } + } + } + refresh() { + const container = this.containerEl.children[1]; + container.empty(); + container.addClass("focus-task-container"); + this.timerTimeEl = null; + this.progressBarEl = null; + this.actualTimeEl = null; + this.pauseBtnEl = null; + this.renderHeader(container); + this.renderStatsBar(container); + this.renderActiveTask(container); + this.renderTaskList(container); + } + renderHeader(container) { + const header = container.createEl("div", { cls: "focus-task-header" }); + const titleSection = header.createEl("div", { cls: "focus-task-title-section" }); + titleSection.createEl("h2", { text: "\u26A1 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) { + const stats = this.plugin.getStats(); + const statsBar = container.createEl("div", { cls: "focus-task-stats-bar" }); + const statItems = [ + { label: "Pending", value: stats.pendingCount.toString(), icon: "\u{1F4CB}" }, + { label: "Done Today", value: stats.completedToday.toString(), icon: "\u2705" }, + { label: "Focus Time", value: this.plugin.formatTimeHuman(stats.totalFocusMinutesToday), icon: "\u23F1\uFE0F" }, + { label: "Streak", value: `${stats.streak} days`, icon: "\u{1F525}" } + ]; + 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) { + 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: "\u2615 BREAK TIME" }); + } else { + activeCard.createEl("div", { cls: "focus-task-active-label", text: "\u{1F3AF} FOCUSING ON" }); + } + activeCard.createEl("div", { cls: "focus-task-active-task-name", text: task.text }); + const timerDisplay = activeCard.createEl("div", { cls: "focus-task-timer-display" }); + this.timerTimeEl = timerDisplay.createEl("span", { + cls: "focus-task-timer-time", + text: this.plugin.formatTime(this.plugin.currentTimerSeconds) + }); + const progressWrap = activeCard.createEl("div", { cls: "focus-task-progress-wrap" }); + this.progressBarEl = 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; + } + this.progressBarEl.style.width = `${Math.min(Math.max(progressPercent, 0), 100)}%`; + if (progressPercent >= 100) + this.progressBarEl.addClass("focus-task-overtime"); + if (!this.plugin.isBreakMode) { + const timeInfo = activeCard.createEl("div", { cls: "focus-task-time-info" }); + timeInfo.createEl("span", { text: `Est: ${this.plugin.formatTimeHuman(task.estimatedMinutes)}` }); + 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(); + }); + } + } + } else { + const startPrompt = activeSection.createEl("div", { cls: "focus-task-start-prompt" }); + startPrompt.createEl("div", { cls: "focus-task-prompt-icon", text: "\u26A1" }); + startPrompt.createEl("div", { cls: "focus-task-prompt-text", text: "Ready to focus?" }); + startPrompt.createEl("div", { cls: "focus-task-prompt-hint", text: "Click \u25B6 on a task to start a Pomodoro session" }); + } + } + renderTaskList(container) { + const listSection = container.createEl("div", { cls: "focus-task-list-section" }); + 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(); + }); + }); + const taskList = listSection.createEl("div", { cls: "focus-task-task-list" }); + let tasks = this.plugin.data.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); + } + 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: "\u{1F4DD}" }); + 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, task) { + 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" : ""}` + }); + const checkbox = taskEl.createEl("div", { cls: "focus-task-checkbox" }); + checkbox.innerHTML = task.completed ? "\u2713" : ""; + checkbox.style.borderColor = (list == null ? void 0 : list.color) || "#6366f1"; + if (task.completed) { + checkbox.style.backgroundColor = (list == null ? void 0 : list.color) || "#6366f1"; + checkbox.style.color = "white"; + } + checkbox.addEventListener("click", (e) => { + e.stopPropagation(); + if (!task.completed) { + this.plugin.completeTask(task.id); + } + }); + 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"); + } + } + const actions = taskEl.createEl("div", { cls: "focus-task-task-actions" }); + if (!task.completed) { + const startBtn = actions.createEl("button", { cls: "focus-task-task-btn", attr: { "aria-label": "Start Pomodoro" } }); + startBtn.innerHTML = "\u25B6"; + startBtn.addEventListener("click", (e) => { + e.stopPropagation(); + this.plugin.startPomodoro(task.id); + }); + const stopwatchBtn = actions.createEl("button", { cls: "focus-task-task-btn", attr: { "aria-label": "Start Stopwatch" } }); + stopwatchBtn.innerHTML = "\u23F1"; + stopwatchBtn.addEventListener("click", (e) => { + e.stopPropagation(); + this.plugin.startTimer(task.id); + }); + } + const editBtn = actions.createEl("button", { cls: "focus-task-task-btn", attr: { "aria-label": "Edit" } }); + editBtn.innerHTML = "\u270F\uFE0F"; + editBtn.addEventListener("click", (e) => { + e.stopPropagation(); + new EditTaskModal(this.app, this.plugin, task).open(); + }); + const deleteBtn = actions.createEl("button", { cls: "focus-task-task-btn focus-task-delete-btn", attr: { "aria-label": "Delete" } }); + deleteBtn.innerHTML = "\u{1F5D1}"; + deleteBtn.addEventListener("click", (e) => { + e.stopPropagation(); + this.plugin.deleteTask(task.id); + }); + } +}; + +// src/main.ts +var FocusTaskPlugin = class extends import_obsidian3.Plugin { + constructor() { + super(...arguments); + // Timer state + this.timerInterval = null; + this.currentTimerSeconds = 0; + this.isTimerRunning = false; + this.isBreakMode = false; + this.activeTaskId = null; + this.pomodoroCount = 0; + // Status bar element + this.statusBarEl = null; + } + async onload() { + await this.loadAllData(); + this.checkDailyReset(); + this.registerView( + VIEW_TYPE_FOCUS_TASK, + (leaf) => new FocusTaskView(leaf, this) + ); + this.addRibbonIcon("zap", "Open Focus Task", () => { + this.activateView(); + }); + 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() + }); + this.addSettingTab(new FocusTaskSettingTab(this.app, this)); + this.createStatusBar(); + } + onunload() { + this.stopTimer(); + } + async loadAllData() { + const loaded = await this.loadData(); + this.data = Object.assign({}, DEFAULT_DATA, (loaded == null ? void 0 : loaded.data) || {}); + this.settings = Object.assign({}, DEFAULT_SETTINGS, (loaded == null ? void 0 : loaded.settings) || {}); + } + async saveAllData() { + await this.saveData({ + settings: this.settings, + data: this.data + }); + } + checkDailyReset() { + const today = new Date().toDateString(); + if (this.data.lastActiveDate !== today) { + 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; + } + this.data.completedToday = 0; + this.data.totalFocusMinutesToday = 0; + this.data.lastActiveDate = today; + this.saveAllData(); + } + } + async activateView() { + const { workspace } = this.app; + let leaf = 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, estimatedMinutes = this.settings.defaultEstimateMinutes, list = "work") { + return { + id: this.generateId(), + text, + completed: false, + estimatedMinutes, + actualMinutes: 0, + createdAt: Date.now(), + list, + notes: "", + isActive: false + }; + } + generateId() { + return Date.now().toString(36) + Math.random().toString(36).substr(2); + } + addTask(task) { + this.data.tasks.push(task); + this.saveAllData(); + this.refreshView(); + } + updateTask(taskId, updates) { + const task = this.data.tasks.find((t) => t.id === taskId); + if (task) { + Object.assign(task, updates); + this.saveAllData(); + this.refreshView(); + } + } + deleteTask(taskId) { + 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) { + 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(); + if (this.settings.enableCelebrations) { + this.showCelebration(task); + } + 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 import_obsidian3.Notice("No active task to complete"); + } + } + // ============ Timer Management ============ + startTimer(taskId) { + 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 = 0; + this.isTimerRunning = true; + this.refreshView(); + this.updateStatusBar(); + this.timerInterval = window.setInterval(() => { + this.currentTimerSeconds++; + task.actualMinutes = Math.floor(this.currentTimerSeconds / 60); + this.updateStatusBar(); + this.updateTimerDisplay(); + if (this.currentTimerSeconds === task.estimatedMinutes * 60) { + if (this.settings.enableSounds) { + this.playAlertSound(); + } + new import_obsidian3.Notice(`\u23F0 Time's up for: ${task.text}`); + } + }, 1e3); + this.saveAllData(); + } + startPomodoro(taskId) { + 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.refreshView(); + this.updateStatusBar(); + 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.updateStatusBar(); + this.updateTimerDisplay(); + if (this.currentTimerSeconds <= 0) { + this.handlePomodoroEnd(); + } + }, 1e3); + } + handlePomodoroEnd() { + if (!this.isBreakMode) { + this.pomodoroCount++; + this.data.pomodorosCompleted++; + if (this.settings.enableSounds) { + this.playAlertSound(); + } + new import_obsidian3.Notice("\u{1F345} Pomodoro complete! Time for a break."); + if (this.settings.autoStartBreak) { + this.startBreak(); + } else { + this.stopTimer(); + } + } else { + if (this.settings.enableSounds) { + this.playAlertSound(); + } + new import_obsidian3.Notice("\u26A1 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 import_obsidian3.Notice(isLongBreak ? "\u2615 Long break time!" : "\u2615 Short break time!"); + this.refreshView(); + if (!this.timerInterval) { + this.timerInterval = window.setInterval(() => { + this.currentTimerSeconds--; + this.updateStatusBar(); + this.updateTimerDisplay(); + if (this.currentTimerSeconds <= 0) { + this.handlePomodoroEnd(); + } + }, 1e3); + } + this.updateStatusBar(); + } + toggleTimer() { + if (this.isTimerRunning && this.timerInterval) { + window.clearInterval(this.timerInterval); + this.timerInterval = null; + this.isTimerRunning = false; + } else if (this.activeTaskId) { + 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.updateStatusBar(); + this.updateTimerDisplay(); + if (this.currentTimerSeconds <= 0) { + this.handlePomodoroEnd(); + } + }, 1e3); + } else { + new import_obsidian3.Notice("No active task. Select a task first."); + } + this.updateStatusBar(); + 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.updateStatusBar(); + this.saveAllData(); + this.refreshView(); + } + startFocusOnNextTask() { + const pendingTasks = this.data.tasks.filter((t) => !t.completed); + if (pendingTasks.length > 0) { + this.startPomodoro(pendingTasks[0].id); + } else { + new import_obsidian3.Notice("No pending tasks. Add a task first!"); + } + } + // ============ Status Bar Timer ============ + createStatusBar() { + this.statusBarEl = this.addStatusBarItem(); + this.statusBarEl.addClass("focus-task-status-bar"); + this.updateStatusBar(); + this.statusBarEl.addEventListener("click", () => { + this.activateView(); + }); + } + updateStatusBar() { + if (!this.statusBarEl) + return; + if (this.activeTaskId) { + const task = this.data.tasks.find((t) => t.id === this.activeTaskId); + const taskName = this.isBreakMode ? "\u2615 Break" : (task == null ? void 0 : task.text.substring(0, 20)) || "Task"; + const timeStr = this.formatTime(this.currentTimerSeconds); + const icon = this.isTimerRunning ? "\u25B6" : "\u23F8"; + this.statusBarEl.setText(`\u26A1 ${icon} ${timeStr} - ${taskName}${task && task.text.length > 20 ? "..." : ""}`); + this.statusBarEl.addClass("focus-task-status-active"); + } else { + this.statusBarEl.setText("\u26A1 Focus Task"); + this.statusBarEl.removeClass("focus-task-status-active"); + } + } + // ============ Sounds & Celebrations ============ + showCelebration(task) { + 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 import_obsidian3.Notice(`${celebration.emoji} ${celebration.message}${extraMessage}`); + } + playCompletionSound() { + try { + const audioContext = new (window.AudioContext || window.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(1e3, 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.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) { + 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) { + 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(); + } + }); + } + // Light refresh - only updates timer display without rebuilding DOM + updateTimerDisplay() { + const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_FOCUS_TASK); + leaves.forEach((leaf) => { + if (leaf.view instanceof FocusTaskView) { + leaf.view.updateTimerDisplay(); + } + }); + } + getTasksByList(listId) { + return this.data.tasks.filter((t) => t.list === listId); + } + getPendingTasks() { + return this.data.tasks.filter((t) => !t.completed); + } + getTodaysTasks() { + 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) + }; + } +}; +var FocusTaskSettingTab = class extends import_obsidian3.PluginSettingTab { + constructor(app, plugin) { + super(app, plugin); + this.plugin = plugin; + } + display() { + const { containerEl } = this; + containerEl.empty(); + containerEl.createEl("h1", { text: "\u26A1 Focus Task Settings" }); + containerEl.createEl("h2", { text: "\u{1F345} Pomodoro Timer" }); + new import_obsidian3.Setting(containerEl).setName("Work Duration").setDesc("Length of each work session in minutes").addSlider((slider) => slider.setLimits(5, 60, 5).setValue(this.plugin.settings.pomodoroWorkMinutes).setDynamicTooltip().onChange(async (value) => { + this.plugin.settings.pomodoroWorkMinutes = value; + await this.plugin.saveAllData(); + })); + new import_obsidian3.Setting(containerEl).setName("Short Break Duration").setDesc("Length of short breaks in minutes").addSlider((slider) => slider.setLimits(1, 15, 1).setValue(this.plugin.settings.pomodoroBreakMinutes).setDynamicTooltip().onChange(async (value) => { + this.plugin.settings.pomodoroBreakMinutes = value; + await this.plugin.saveAllData(); + })); + new import_obsidian3.Setting(containerEl).setName("Long Break Duration").setDesc("Length of long breaks in minutes").addSlider((slider) => slider.setLimits(5, 30, 5).setValue(this.plugin.settings.longBreakMinutes).setDynamicTooltip().onChange(async (value) => { + this.plugin.settings.longBreakMinutes = value; + await this.plugin.saveAllData(); + })); + new import_obsidian3.Setting(containerEl).setName("Long Break Interval").setDesc("Number of pomodoros before a long break").addSlider((slider) => slider.setLimits(2, 6, 1).setValue(this.plugin.settings.longBreakInterval).setDynamicTooltip().onChange(async (value) => { + this.plugin.settings.longBreakInterval = value; + await this.plugin.saveAllData(); + })); + new import_obsidian3.Setting(containerEl).setName("Auto-start Breaks").setDesc("Automatically start break timer after work session").addToggle((toggle) => toggle.setValue(this.plugin.settings.autoStartBreak).onChange(async (value) => { + this.plugin.settings.autoStartBreak = value; + await this.plugin.saveAllData(); + })); + containerEl.createEl("h2", { text: "\u2699\uFE0F General" }); + new import_obsidian3.Setting(containerEl).setName("Default Time Estimate").setDesc("Default estimated time for new tasks in minutes").addSlider((slider) => slider.setLimits(5, 120, 5).setValue(this.plugin.settings.defaultEstimateMinutes).setDynamicTooltip().onChange(async (value) => { + this.plugin.settings.defaultEstimateMinutes = value; + await this.plugin.saveAllData(); + })); + new import_obsidian3.Setting(containerEl).setName("Enable Sounds").setDesc("Play sounds for timer completion and task completion").addToggle((toggle) => toggle.setValue(this.plugin.settings.enableSounds).onChange(async (value) => { + this.plugin.settings.enableSounds = value; + await this.plugin.saveAllData(); + })); + new import_obsidian3.Setting(containerEl).setName("Enable Celebrations").setDesc("Show celebration messages when completing tasks").addToggle((toggle) => toggle.setValue(this.plugin.settings.enableCelebrations).onChange(async (value) => { + this.plugin.settings.enableCelebrations = value; + await this.plugin.saveAllData(); + })); + 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) => { + 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 import_obsidian3.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: "\u{1F4C1}" + }); + await this.plugin.saveAllData(); + this.display(); + })); + containerEl.createEl("h2", { text: "\u{1F4D6} 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 +

+ `; + } +};