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])); const activeTask = this.plugin.settings.tasks.find(t => t.startTime !== null); if (activeTask) { const category = categoriesMap.get(activeTask.categoryId); this.renderActiveTimer(container, activeTask, category); } 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); }); } } renderActiveTimer(container: HTMLElement, task: Task, category?: Category): void { const timerSection = container.createDiv({ cls: "taskweaver-active-timer" }); if (category) { timerSection.style.borderColor = category.color; } const timerHeader = timerSection.createDiv({ cls: "taskweaver-active-timer-header" }); timerHeader.createEl("span", { text: "Active Task", cls: "taskweaver-active-timer-label" }); const taskTitleDiv = timerSection.createDiv({ cls: "taskweaver-active-timer-title" }); if (category?.emoji) { taskTitleDiv.createSpan({ text: `${category.emoji} `, cls: "taskweaver-emoji" }); } taskTitleDiv.createSpan({ text: task.title }); const timerDisplay = timerSection.createDiv({ cls: "taskweaver-active-timer-display", attr: { "data-task-id": task.id } }); timerDisplay.setText(formatDuration(this.plugin.getTaskElapsed(task))); const timerControls = timerSection.createDiv({ cls: "taskweaver-active-timer-controls" }); const pauseBtn = timerControls.createEl("button", { text: "Pause", cls: "taskweaver-timer-control-btn pause" }); pauseBtn.onclick = async () => { await this.plugin.toggleTaskTimer(task.id); }; const stopBtn = timerControls.createEl("button", { text: "Stop", cls: "taskweaver-timer-control-btn stop" }); stopBtn.onclick = async () => { await this.plugin.stopTaskTimer(task.id); }; const completeBtn = timerControls.createEl("button", { text: "Complete", cls: "taskweaver-timer-control-btn complete" }); completeBtn.onclick = async () => { await this.plugin.toggleTaskComplete(task.id); }; } 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" }); if (task.startTime) { taskDiv.addClass("taskweaver-task-running"); } const taskInfo = taskDiv.createDiv({ cls: "taskweaver-task-info" }); 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-small", attr: { "data-task-id": task.id } }); timerSpan.setText(formatDuration(this.plugin.getTaskElapsed(task))); if (!task.completed) { if (!task.startTime) { const startBtn = taskControls.createEl("button", { text: "Start", cls: "taskweaver-start-btn" }); startBtn.onclick = async () => { await this.plugin.toggleTaskTimer(task.id); }; } const deleteBtn = taskControls.createEl("button", { text: "Delete", cls: "taskweaver-delete-btn" }); deleteBtn.onclick = async () => { await this.plugin.deleteTask(task.id); }; } else { const checkbox = taskControls.createEl("input", { type: "checkbox" }); checkbox.checked = true; checkbox.addClass("taskweaver-checkbox"); checkbox.onclick = async () => { await this.plugin.toggleTaskComplete(task.id); }; } } updateTimers(): void { const container = this.containerEl.children[1] as HTMLElement; const activeTimerDisplay = container.querySelector(".taskweaver-active-timer-display"); if (activeTimerDisplay) { const taskId = activeTimerDisplay.getAttribute("data-task-id"); if (taskId) { const task = this.plugin.settings.tasks.find(t => t.id === taskId); if (task) { activeTimerDisplay.setText(formatDuration(this.plugin.getTaskElapsed(task))); } } } const timerElements = container.querySelectorAll(".taskweaver-timer-small"); 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 { const categorySection = contentEl.createDiv({ cls: "taskweaver-modal-section" }); categorySection.createEl("label", { text: "Category", cls: "taskweaver-modal-label" }); const categoriesGrid = categorySection.createDiv({ cls: "taskweaver-categories-grid" }); this.plugin.settings.categories.forEach(cat => { const categoryBtn = categoriesGrid.createDiv({ cls: "taskweaver-category-btn" }); categoryBtn.style.borderColor = cat.color; if (cat.id === selectedCategoryId) { categoryBtn.addClass("selected"); categoryBtn.style.backgroundColor = cat.color + "20"; } if (cat.emoji) { categoryBtn.createSpan({ text: cat.emoji, cls: "taskweaver-category-btn-emoji" }); } categoryBtn.createSpan({ text: cat.name, cls: "taskweaver-category-btn-name" }); categoryBtn.onclick = () => { selectedCategoryId = cat.id; categoriesGrid.querySelectorAll(".taskweaver-category-btn").forEach(btn => { btn.removeClass("selected"); (btn as HTMLElement).style.backgroundColor = ""; }); categoryBtn.addClass("selected"); categoryBtn.style.backgroundColor = cat.color + "20"; }; }); 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; })); const colorSetting = new Setting(contentEl) .setName("Color"); const colorContainer = colorSetting.settingEl.createDiv({ cls: "taskweaver-color-picker-container" }); const presetColors = [ "#4a9eff", "#ff6b6b", "#51cf66", "#ffd43b", "#ff8787", "#cc5de8", "#339af0", "#ff922b", "#20c997", "#e599f7", "#74c0fc", "#a9e34b" ]; const presetContainer = colorContainer.createDiv({ cls: "taskweaver-preset-colors" }); presetColors.forEach(color => { const colorOption = presetContainer.createDiv({ cls: "taskweaver-color-option" }); colorOption.style.backgroundColor = color; if (color === categoryColor) { colorOption.addClass("selected"); } colorOption.onclick = () => { categoryColor = color; presetContainer.querySelectorAll(".taskweaver-color-option").forEach(el => el.removeClass("selected") ); colorOption.addClass("selected"); colorPicker.value = color; }; }); const customColorContainer = colorContainer.createDiv({ cls: "taskweaver-custom-color" }); customColorContainer.createEl("label", { text: "Custom: " }); const colorPicker = customColorContainer.createEl("input", { type: "color" }); colorPicker.value = categoryColor; colorPicker.oninput = () => { categoryColor = colorPicker.value; presetContainer.querySelectorAll(".taskweaver-color-option").forEach(el => el.removeClass("selected") ); }; 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(); } }