473 lines
14 KiB
TypeScript
473 lines
14 KiB
TypeScript
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 "zap";
|
|
}
|
|
|
|
async onOpen(): Promise<void> {
|
|
this.render();
|
|
this.updateInterval = window.setInterval(() => {
|
|
this.updateTimers();
|
|
}, 1000);
|
|
}
|
|
|
|
async onClose(): Promise<void> {
|
|
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" });
|
|
const headerTitle = headerDiv.createEl("h4");
|
|
headerTitle.createSpan({ text: "⚡ ", cls: "taskweaver-header-icon" });
|
|
headerTitle.createSpan({ 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" });
|
|
|
|
// Only show timer for completed tasks
|
|
if (task.completed) {
|
|
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();
|
|
}
|
|
}
|