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(); } }