diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c56315 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Dependencies +node_modules/ + +# Build output +main.js +main.js.map + +# Package folder +taskweaver-package/ + +# Logs +*.log + +# OS files +.DS_Store +Thumbs.db + +# IDE files +.vscode/ +.idea/ + +# Data files (plugin saves) +data.json + +# NPM +package-lock.json + +# Claude +.claude + diff --git a/build-package.bat b/build-package.bat new file mode 100644 index 0000000..b0e8f30 --- /dev/null +++ b/build-package.bat @@ -0,0 +1,32 @@ +@echo off +echo Building TaskWeaver plugin... + +REM Run the build +call npm run build + +if %errorlevel% neq 0 ( + echo Build failed! + exit /b %errorlevel% +) + +echo Build successful! +echo Packaging plugin files... + +REM Create package directory +if exist "taskweaver-package" rmdir /s /q "taskweaver-package" +mkdir "taskweaver-package" + +REM Copy required files +copy "main.js" "taskweaver-package\" +copy "manifest.json" "taskweaver-package\" +copy "styles.css" "taskweaver-package\" + +echo. +echo Package created successfully in 'taskweaver-package' folder! +echo. +echo Files included: +echo - main.js +echo - manifest.json +echo - styles.css +echo. +echo To install: Copy the contents of 'taskweaver-package' to your vault's .obsidian/plugins/taskweaver/ folder diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..ad06f8f --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,48 @@ +import esbuild from "esbuild"; +import process from "process"; +import builtins from "builtin-modules"; + +const banner = +`/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ +`; + +const prod = (process.argv[2] === "production"); + +const context = await esbuild.context({ + banner: { + js: banner, + }, + entryPoints: ["main.ts"], + bundle: true, + external: [ + "obsidian", + "electron", + "@codemirror/autocomplete", + "@codemirror/collab", + "@codemirror/commands", + "@codemirror/language", + "@codemirror/lint", + "@codemirror/search", + "@codemirror/state", + "@codemirror/view", + "@lezer/common", + "@lezer/highlight", + "@lezer/lr", + ...builtins], + format: "cjs", + target: "es2018", + logLevel: "info", + sourcemap: prod ? false : "inline", + treeShaking: true, + outfile: "main.js", +}); + +if (prod) { + await context.rebuild(); + process.exit(0); +} else { + await context.watch(); +} diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..0c4a664 --- /dev/null +++ b/main.ts @@ -0,0 +1,173 @@ +import { Plugin } from "obsidian"; +import { TaskWeaverView, VIEW_TYPE_TASKWEAVER } from "./view"; +import { TaskWeaverSettingTab } from "./settings"; +import { TaskWeaverSettings, DEFAULT_SETTINGS, Task, Category } from "./types"; +import { generateUniqueId, logTaskToDailyNote } from "./utils"; + +export default class TaskWeaverPlugin extends Plugin { + settings: TaskWeaverSettings; + + async onload(): Promise { + await this.loadSettings(); + + this.registerView( + VIEW_TYPE_TASKWEAVER, + (leaf) => new TaskWeaverView(leaf, this) + ); + + this.addRibbonIcon("clock", "Open TaskWeaver", () => { + this.activateView(); + }); + + this.addCommand({ + id: "open-taskweaver", + name: "Open TaskWeaver", + callback: () => { + this.activateView(); + } + }); + + this.addSettingTab(new TaskWeaverSettingTab(this.app, this)); + + this.app.workspace.onLayoutReady(() => { + this.activateView(); + }); + } + + async onunload(): Promise { + this.app.workspace.detachLeavesOfType(VIEW_TYPE_TASKWEAVER); + } + + async activateView(): Promise { + const { workspace } = this.app; + + let leaf = workspace.getLeavesOfType(VIEW_TYPE_TASKWEAVER)[0]; + + if (!leaf) { + const rightLeaf = workspace.getRightLeaf(false); + if (rightLeaf) { + await rightLeaf.setViewState({ + type: VIEW_TYPE_TASKWEAVER, + active: true, + }); + leaf = rightLeaf; + } + } + + if (leaf) { + workspace.revealLeaf(leaf); + } + } + + async loadSettings(): Promise { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveSettings(): Promise { + await this.saveData(this.settings); + this.refreshView(); + } + + refreshView(): void { + const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_TASKWEAVER); + leaves.forEach(leaf => { + if (leaf.view instanceof TaskWeaverView) { + leaf.view.render(); + } + }); + } + + async addCategory(name: string, color: string, emoji?: string): Promise { + const category: Category = { + id: generateUniqueId(), + name, + color, + emoji + }; + this.settings.categories.push(category); + await this.saveSettings(); + } + + async deleteCategory(categoryId: string): Promise { + const hasTasks = this.settings.tasks.some(t => t.categoryId === categoryId); + if (hasTasks) { + return; + } + + this.settings.categories = this.settings.categories.filter(c => c.id !== categoryId); + await this.saveSettings(); + } + + async addTask(title: string, categoryId: string): Promise { + const task: Task = { + id: generateUniqueId(), + title, + categoryId, + startTime: null, + totalElapsed: 0, + completed: false + }; + this.settings.tasks.push(task); + await this.saveSettings(); + } + + async deleteTask(taskId: string): Promise { + this.settings.tasks = this.settings.tasks.filter(t => t.id !== taskId); + await this.saveSettings(); + } + + async toggleTaskTimer(taskId: string): Promise { + const task = this.settings.tasks.find(t => t.id === taskId); + if (!task) return; + + if (task.startTime) { + const elapsed = Date.now() - task.startTime; + task.totalElapsed += elapsed; + task.startTime = null; + } else { + task.startTime = Date.now(); + } + + await this.saveSettings(); + } + + async toggleTaskComplete(taskId: string): Promise { + const task = this.settings.tasks.find(t => t.id === taskId); + if (!task) return; + + if (!task.completed) { + if (task.startTime) { + const elapsed = Date.now() - task.startTime; + task.totalElapsed += elapsed; + task.startTime = null; + } + + task.completed = true; + task.completedAt = Date.now(); + + if (this.settings.enableDailyNoteLogging) { + const category = this.settings.categories.find(c => c.id === task.categoryId); + await logTaskToDailyNote( + this.app.vault, + task, + category, + this.settings.dailyNoteFormat, + this.settings.dailyNotePath + ); + } + } else { + task.completed = false; + delete task.completedAt; + } + + await this.saveSettings(); + } + + getTaskElapsed(task: Task): number { + let elapsed = task.totalElapsed; + if (task.startTime) { + elapsed += Date.now() - task.startTime; + } + return elapsed; + } +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..e77b3fe --- /dev/null +++ b/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "taskweaver", + "name": "TaskWeaver", + "version": "1.0.0", + "minAppVersion": "0.15.0", + "description": "A simple task timer with category support and daily note logging", + "author": "", + "authorUrl": "", + "isDesktopOnly": false +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..aac8794 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "obsidian-taskweaver", + "version": "1.0.0", + "description": "A simple task timer plugin for Obsidian with category support", + "main": "main.js", + "scripts": { + "dev": "node esbuild.config.mjs", + "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", + "version": "node version-bump.mjs && git add manifest.json versions.json" + }, + "keywords": [], + "author": "", + "license": "MIT", + "devDependencies": { + "@types/node": "^16.11.6", + "@typescript-eslint/eslint-plugin": "5.29.0", + "@typescript-eslint/parser": "5.29.0", + "builtin-modules": "3.3.0", + "esbuild": "0.17.3", + "obsidian": "latest", + "tslib": "2.4.0", + "typescript": "4.7.4" + } +} diff --git a/settings.ts b/settings.ts new file mode 100644 index 0000000..a9540ff --- /dev/null +++ b/settings.ts @@ -0,0 +1,51 @@ +import { App, PluginSettingTab, Setting } from "obsidian"; +import TaskWeaverPlugin from "./main"; + +export class TaskWeaverSettingTab extends PluginSettingTab { + plugin: TaskWeaverPlugin; + + constructor(app: App, plugin: TaskWeaverPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + const { containerEl } = this; + containerEl.empty(); + + new Setting(containerEl) + .setName("Enable daily note logging") + .setDesc("Log completed tasks to your daily note") + .addToggle(toggle => toggle + .setValue(this.plugin.settings.enableDailyNoteLogging) + .onChange(async (value) => { + this.plugin.settings.enableDailyNoteLogging = value; + await this.plugin.saveSettings(); + this.display(); + })); + + if (this.plugin.settings.enableDailyNoteLogging) { + new Setting(containerEl) + .setName("Daily note path") + .setDesc("Path to your daily notes folder (e.g., 'Daily Notes' or leave empty for root)") + .addText(text => text + .setPlaceholder("Daily Notes") + .setValue(this.plugin.settings.dailyNotePath) + .onChange(async (value) => { + this.plugin.settings.dailyNotePath = value; + await this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName("Daily note format") + .setDesc("Format for logging tasks. Available placeholders: {{title}}, {{duration}}, {{emoji}}, {{category}}") + .addTextArea(text => text + .setPlaceholder("- [x] {{title}} ({{duration}}) {{emoji}}") + .setValue(this.plugin.settings.dailyNoteFormat) + .onChange(async (value) => { + this.plugin.settings.dailyNoteFormat = value; + await this.plugin.saveSettings(); + })); + } + } +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..c479135 --- /dev/null +++ b/styles.css @@ -0,0 +1,197 @@ +.taskweaver-container { + padding: var(--size-4-3); + height: 100%; + overflow-y: auto; +} + +.taskweaver-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--size-4-3); + padding-bottom: var(--size-4-2); + border-bottom: 1px solid var(--background-modifier-border); +} + +.taskweaver-header h4 { + margin: 0; + color: var(--text-normal); +} + +.taskweaver-button-group { + display: flex; + gap: var(--size-4-1); +} + +.taskweaver-btn { + padding: var(--size-4-1) var(--size-4-2); + background-color: var(--interactive-accent); + color: var(--text-on-accent); + border: none; + border-radius: var(--radius-s); + cursor: pointer; + font-size: var(--font-ui-small); + transition: background-color 0.2s; +} + +.taskweaver-btn:hover { + background-color: var(--interactive-accent-hover); +} + +.taskweaver-btn-small { + padding: var(--size-2-1) var(--size-4-1); + background-color: var(--background-modifier-error); + color: var(--text-on-accent); + border: none; + border-radius: var(--radius-s); + cursor: pointer; + font-size: var(--font-ui-smaller); + transition: background-color 0.2s; +} + +.taskweaver-btn-small:hover { + background-color: var(--background-modifier-error-hover); +} + +.taskweaver-category { + margin-bottom: var(--size-4-3); + background-color: var(--background-secondary); + border-radius: var(--radius-m); + padding: var(--size-4-2); +} + +.taskweaver-category-header { + padding: var(--size-4-2); + margin-bottom: var(--size-4-2); + border-left: 3px solid var(--interactive-accent); + background-color: var(--background-primary); + border-radius: var(--radius-s); +} + +.taskweaver-category-name { + font-weight: var(--font-semibold); + color: var(--text-normal); + font-size: var(--font-ui-medium); +} + +.taskweaver-emoji { + font-size: var(--font-ui-medium); +} + +.taskweaver-task { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--size-4-2); + margin-bottom: var(--size-2-2); + background-color: var(--background-primary); + border-radius: var(--radius-s); + border: 1px solid var(--background-modifier-border); + transition: background-color 0.2s; +} + +.taskweaver-task:hover { + background-color: var(--background-primary-alt); +} + +.taskweaver-task-info { + display: flex; + align-items: center; + gap: var(--size-4-2); + flex: 1; +} + +.taskweaver-checkbox { + cursor: pointer; + width: 16px; + height: 16px; +} + +.taskweaver-task-title { + color: var(--text-normal); + font-size: var(--font-ui-small); +} + +.taskweaver-task-completed { + text-decoration: line-through; + color: var(--text-muted); +} + +.taskweaver-task-controls { + display: flex; + align-items: center; + gap: var(--size-4-1); +} + +.taskweaver-timer { + font-family: var(--font-monospace); + color: var(--text-muted); + font-size: var(--font-ui-smaller); + min-width: 70px; + text-align: right; +} + +.taskweaver-timer-btn, +.taskweaver-delete-btn { + background: transparent; + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-s); + padding: var(--size-2-1) var(--size-2-3); + cursor: pointer; + font-size: var(--font-ui-small); + color: var(--text-normal); + transition: all 0.2s; +} + +.taskweaver-timer-btn:hover { + background-color: var(--interactive-accent); + color: var(--text-on-accent); + border-color: var(--interactive-accent); +} + +.taskweaver-delete-btn:hover { + background-color: var(--background-modifier-error); + color: var(--text-on-accent); + border-color: var(--background-modifier-error); +} + +.taskweaver-warning { + color: var(--text-error); + font-size: var(--font-ui-small); + margin: var(--size-4-2) 0; +} + +.taskweaver-categories-list { + margin-bottom: var(--size-4-4); +} + +.taskweaver-category-item { + display: flex; + align-items: center; + gap: var(--size-4-2); + padding: var(--size-4-2); + margin-bottom: var(--size-2-2); + background-color: var(--background-secondary); + border-radius: var(--radius-s); + border: 1px solid var(--background-modifier-border); +} + +.taskweaver-color-preview { + width: 24px; + height: 24px; + border-radius: var(--radius-s); + border: 1px solid var(--background-modifier-border); +} + +.taskweaver-category-info { + flex: 1; + color: var(--text-normal); + font-size: var(--font-ui-small); +} + +.modal .taskweaver-warning { + padding: var(--size-4-2); + background-color: var(--background-secondary); + border-radius: var(--radius-s); + border-left: 3px solid var(--text-error); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..c44b729 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "inlineSourceMap": true, + "inlineSources": true, + "module": "ESNext", + "target": "ES6", + "allowJs": true, + "noImplicitAny": true, + "moduleResolution": "node", + "importHelpers": true, + "isolatedModules": true, + "strictNullChecks": true, + "lib": [ + "DOM", + "ES5", + "ES6", + "ES7" + ] + }, + "include": [ + "**/*.ts" + ] +} diff --git a/types.ts b/types.ts new file mode 100644 index 0000000..90afdda --- /dev/null +++ b/types.ts @@ -0,0 +1,32 @@ +export interface Category { + id: string; + name: string; + color: string; + emoji?: string; +} + +export interface Task { + id: string; + title: string; + categoryId: string; + startTime: number | null; + totalElapsed: number; + completed: boolean; + completedAt?: number; +} + +export interface TaskWeaverSettings { + categories: Category[]; + tasks: Task[]; + enableDailyNoteLogging: boolean; + dailyNoteFormat: string; + dailyNotePath: string; +} + +export const DEFAULT_SETTINGS: TaskWeaverSettings = { + categories: [], + tasks: [], + enableDailyNoteLogging: false, + dailyNoteFormat: "- [x] {{title}} ({{duration}}) {{emoji}}", + dailyNotePath: "" +}; diff --git a/utils.ts b/utils.ts new file mode 100644 index 0000000..de629d9 --- /dev/null +++ b/utils.ts @@ -0,0 +1,62 @@ +import { TFile, Vault, moment } from "obsidian"; +import { Task, Category } from "./types"; + +export function formatDuration(milliseconds: number): string { + const totalSeconds = Math.floor(milliseconds / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + if (hours > 0) { + return `${hours}h ${minutes}m ${seconds}s`; + } else if (minutes > 0) { + return `${minutes}m ${seconds}s`; + } else { + return `${seconds}s`; + } +} + +export function generateUniqueId(): string { + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; +} + +export async function logTaskToDailyNote( + vault: Vault, + task: Task, + category: Category | undefined, + format: string, + dailyNotePath: string +): Promise { + const today = moment().format("YYYY-MM-DD"); + const dailyNoteFileName = `${today}.md`; + + let dailyNoteFolderPath = dailyNotePath.trim(); + if (dailyNoteFolderPath && !dailyNoteFolderPath.endsWith("/")) { + dailyNoteFolderPath += "/"; + } + + const fullPath = `${dailyNoteFolderPath}${dailyNoteFileName}`; + + let file = vault.getAbstractFileByPath(fullPath); + + if (!file || !(file instanceof TFile)) { + const folder = dailyNoteFolderPath.slice(0, -1); + if (folder && !vault.getAbstractFileByPath(folder)) { + await vault.createFolder(folder); + } + file = await vault.create(fullPath, ""); + } + + if (file instanceof TFile) { + const duration = formatDuration(task.totalElapsed); + const logEntry = format + .replace("{{title}}", task.title) + .replace("{{duration}}", duration) + .replace("{{emoji}}", category?.emoji || "") + .replace("{{category}}", category?.name || ""); + + const content = await vault.read(file); + const newContent = content ? `${content}\n${logEntry}` : logEntry; + await vault.modify(file, newContent); + } +} diff --git a/version-bump.mjs b/version-bump.mjs new file mode 100644 index 0000000..7e747f0 --- /dev/null +++ b/version-bump.mjs @@ -0,0 +1,12 @@ +import { readFileSync, writeFileSync } from "fs"; + +const targetVersion = process.env.npm_package_version; + +let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); +const { minAppVersion } = manifest; +manifest.version = targetVersion; +writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); + +let versions = JSON.parse(readFileSync("versions.json", "utf8")); +versions[targetVersion] = minAppVersion; +writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..af9a39e --- /dev/null +++ b/versions.json @@ -0,0 +1,3 @@ +{ + "1.0.0": "0.15.0" +} diff --git a/view.ts b/view.ts new file mode 100644 index 0000000..c757083 --- /dev/null +++ b/view.ts @@ -0,0 +1,347 @@ +import { ItemView, WorkspaceLeaf, Modal, App, Setting } from "obsidian"; +import TaskWeaverPlugin from "./main"; +import { Task, Category } from "./types"; +import { formatDuration, generateUniqueId } from "./utils"; + +export const VIEW_TYPE_TASKWEAVER = "taskweaver-view"; + +export class TaskWeaverView extends ItemView { + plugin: TaskWeaverPlugin; + private updateInterval: number | null = null; + + constructor(leaf: WorkspaceLeaf, plugin: TaskWeaverPlugin) { + super(leaf); + this.plugin = plugin; + } + + getViewType(): string { + return VIEW_TYPE_TASKWEAVER; + } + + getDisplayText(): string { + return "TaskWeaver"; + } + + getIcon(): string { + return "clock"; + } + + async onOpen(): Promise { + this.render(); + this.updateInterval = window.setInterval(() => { + this.updateTimers(); + }, 1000); + } + + async onClose(): Promise { + if (this.updateInterval !== null) { + window.clearInterval(this.updateInterval); + } + } + + render(): void { + const container = this.containerEl.children[1] as HTMLElement; + container.empty(); + container.addClass("taskweaver-container"); + + const headerDiv = container.createDiv({ cls: "taskweaver-header" }); + headerDiv.createEl("h4", { text: "TaskWeaver" }); + + const buttonContainer = headerDiv.createDiv({ cls: "taskweaver-button-group" }); + + const addTaskBtn = buttonContainer.createEl("button", { + text: "Add Task", + cls: "taskweaver-btn" + }); + addTaskBtn.onclick = () => this.openAddTaskModal(); + + const manageCategoriesBtn = buttonContainer.createEl("button", { + text: "Categories", + cls: "taskweaver-btn" + }); + manageCategoriesBtn.onclick = () => this.openManageCategoriesModal(); + + const categoriesMap = new Map(this.plugin.settings.categories.map(c => [c.id, c])); + + this.plugin.settings.categories.forEach(category => { + const categoryTasks = this.plugin.settings.tasks.filter(t => t.categoryId === category.id && !t.completed); + + if (categoryTasks.length > 0) { + this.renderCategory(container, category, categoryTasks); + } + }); + + const completedTasks = this.plugin.settings.tasks.filter(t => t.completed); + if (completedTasks.length > 0) { + const completedSection = container.createDiv({ cls: "taskweaver-category" }); + const completedHeader = completedSection.createDiv({ cls: "taskweaver-category-header" }); + completedHeader.createEl("span", { text: "✓ Completed", cls: "taskweaver-category-name" }); + + completedTasks.forEach(task => { + const category = categoriesMap.get(task.categoryId); + this.renderTask(completedSection, task, category); + }); + } + } + + renderCategory(container: HTMLElement, category: Category, tasks: Task[]): void { + const categoryDiv = container.createDiv({ cls: "taskweaver-category" }); + + const categoryHeader = categoryDiv.createDiv({ cls: "taskweaver-category-header" }); + categoryHeader.style.borderLeftColor = category.color; + + const categoryName = categoryHeader.createSpan({ cls: "taskweaver-category-name" }); + if (category.emoji) { + categoryName.createSpan({ text: `${category.emoji} `, cls: "taskweaver-emoji" }); + } + categoryName.createSpan({ text: category.name }); + + tasks.forEach(task => { + this.renderTask(categoryDiv, task, category); + }); + } + + renderTask(container: HTMLElement, task: Task, category?: Category): void { + const taskDiv = container.createDiv({ cls: "taskweaver-task" }); + + const taskInfo = taskDiv.createDiv({ cls: "taskweaver-task-info" }); + + const checkbox = taskInfo.createEl("input", { type: "checkbox" }); + checkbox.checked = task.completed; + checkbox.addClass("taskweaver-checkbox"); + checkbox.onclick = async () => { + await this.plugin.toggleTaskComplete(task.id); + }; + + const taskTitle = taskInfo.createSpan({ + text: task.title, + cls: task.completed ? "taskweaver-task-title taskweaver-task-completed" : "taskweaver-task-title" + }); + + const taskControls = taskDiv.createDiv({ cls: "taskweaver-task-controls" }); + + const timerSpan = taskControls.createSpan({ + cls: "taskweaver-timer", + attr: { "data-task-id": task.id } + }); + timerSpan.setText(formatDuration(this.plugin.getTaskElapsed(task))); + + if (!task.completed) { + const timerBtn = taskControls.createEl("button", { + text: task.startTime ? "⏸" : "▶", + cls: "taskweaver-timer-btn" + }); + timerBtn.onclick = async () => { + await this.plugin.toggleTaskTimer(task.id); + }; + + const deleteBtn = taskControls.createEl("button", { + text: "🗑", + cls: "taskweaver-delete-btn" + }); + deleteBtn.onclick = async () => { + await this.plugin.deleteTask(task.id); + }; + } + } + + updateTimers(): void { + const container = this.containerEl.children[1] as HTMLElement; + const timerElements = container.querySelectorAll(".taskweaver-timer"); + + timerElements.forEach((element) => { + const taskId = element.getAttribute("data-task-id"); + if (taskId) { + const task = this.plugin.settings.tasks.find(t => t.id === taskId); + if (task) { + element.setText(formatDuration(this.plugin.getTaskElapsed(task))); + } + } + }); + } + + openAddTaskModal(): void { + new AddTaskModal(this.app, this.plugin, (title, categoryId) => { + this.plugin.addTask(title, categoryId); + }).open(); + } + + openManageCategoriesModal(): void { + new ManageCategoriesModal(this.app, this.plugin).open(); + } +} + +class AddTaskModal extends Modal { + plugin: TaskWeaverPlugin; + onSubmit: (title: string, categoryId: string) => void; + + constructor(app: App, plugin: TaskWeaverPlugin, onSubmit: (title: string, categoryId: string) => void) { + super(app); + this.plugin = plugin; + this.onSubmit = onSubmit; + } + + onOpen(): void { + const { contentEl } = this; + contentEl.empty(); + + contentEl.createEl("h3", { text: "Add New Task" }); + + let taskTitle = ""; + let selectedCategoryId = this.plugin.settings.categories[0]?.id || ""; + + new Setting(contentEl) + .setName("Task title") + .addText(text => text + .setPlaceholder("Enter task title") + .onChange(value => { + taskTitle = value; + })); + + if (this.plugin.settings.categories.length === 0) { + contentEl.createEl("p", { + text: "Please create a category first", + cls: "taskweaver-warning" + }); + + new Setting(contentEl) + .addButton(btn => btn + .setButtonText("Create Category") + .setCta() + .onClick(() => { + this.close(); + new ManageCategoriesModal(this.app, this.plugin).open(); + })); + } else { + new Setting(contentEl) + .setName("Category") + .addDropdown(dropdown => { + this.plugin.settings.categories.forEach(cat => { + const label = cat.emoji ? `${cat.emoji} ${cat.name}` : cat.name; + dropdown.addOption(cat.id, label); + }); + dropdown.setValue(selectedCategoryId); + dropdown.onChange(value => { + selectedCategoryId = value; + }); + }); + + new Setting(contentEl) + .addButton(btn => btn + .setButtonText("Cancel") + .onClick(() => { + this.close(); + })) + .addButton(btn => btn + .setButtonText("Add") + .setCta() + .onClick(() => { + if (taskTitle && selectedCategoryId) { + this.onSubmit(taskTitle, selectedCategoryId); + this.close(); + } + })); + } + } + + onClose(): void { + const { contentEl } = this; + contentEl.empty(); + } +} + +class ManageCategoriesModal extends Modal { + plugin: TaskWeaverPlugin; + + constructor(app: App, plugin: TaskWeaverPlugin) { + super(app); + this.plugin = plugin; + } + + onOpen(): void { + this.render(); + } + + render(): void { + const { contentEl } = this; + contentEl.empty(); + + contentEl.createEl("h3", { text: "Manage Categories" }); + + const categoriesList = contentEl.createDiv({ cls: "taskweaver-categories-list" }); + + this.plugin.settings.categories.forEach(category => { + const categoryItem = categoriesList.createDiv({ cls: "taskweaver-category-item" }); + + const colorPreview = categoryItem.createDiv({ cls: "taskweaver-color-preview" }); + colorPreview.style.backgroundColor = category.color; + + const categoryInfo = categoryItem.createDiv({ cls: "taskweaver-category-info" }); + if (category.emoji) { + categoryInfo.createSpan({ text: `${category.emoji} `, cls: "taskweaver-emoji" }); + } + categoryInfo.createSpan({ text: category.name }); + + const deleteBtn = categoryItem.createEl("button", { + text: "Delete", + cls: "taskweaver-btn-small" + }); + deleteBtn.onclick = async () => { + await this.plugin.deleteCategory(category.id); + this.render(); + }; + }); + + contentEl.createEl("h4", { text: "Add New Category" }); + + let categoryName = ""; + let categoryColor = "#4a9eff"; + let categoryEmoji = ""; + + new Setting(contentEl) + .setName("Category name") + .addText(text => text + .setPlaceholder("e.g., Work, Personal") + .onChange(value => { + categoryName = value; + })); + + new Setting(contentEl) + .setName("Color") + .addText(text => text + .setValue(categoryColor) + .setPlaceholder("#4a9eff") + .onChange(value => { + categoryColor = value; + })); + + new Setting(contentEl) + .setName("Emoji (optional)") + .addText(text => text + .setPlaceholder("💼") + .onChange(value => { + categoryEmoji = value; + })); + + new Setting(contentEl) + .addButton(btn => btn + .setButtonText("Close") + .onClick(() => { + this.close(); + })) + .addButton(btn => btn + .setButtonText("Add Category") + .setCta() + .onClick(async () => { + if (categoryName) { + await this.plugin.addCategory(categoryName, categoryColor, categoryEmoji); + this.render(); + } + })); + } + + onClose(): void { + const { contentEl } = this; + contentEl.empty(); + } +}