Initial commit

This commit is contained in:
2025-12-17 20:36:13 +01:00
parent d2485cec73
commit a265c31460
14 changed files with 1045 additions and 0 deletions

30
.gitignore vendored Normal file
View File

@@ -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

32
build-package.bat Normal file
View File

@@ -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

48
esbuild.config.mjs Normal file
View File

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

173
main.ts Normal file
View File

@@ -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<void> {
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<void> {
this.app.workspace.detachLeavesOfType(VIEW_TYPE_TASKWEAVER);
}
async activateView(): Promise<void> {
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<void> {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings(): Promise<void> {
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<void> {
const category: Category = {
id: generateUniqueId(),
name,
color,
emoji
};
this.settings.categories.push(category);
await this.saveSettings();
}
async deleteCategory(categoryId: string): Promise<void> {
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<void> {
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<void> {
this.settings.tasks = this.settings.tasks.filter(t => t.id !== taskId);
await this.saveSettings();
}
async toggleTaskTimer(taskId: string): Promise<void> {
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<void> {
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;
}
}

10
manifest.json Normal file
View File

@@ -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
}

24
package.json Normal file
View File

@@ -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"
}
}

51
settings.ts Normal file
View File

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

197
styles.css Normal file
View File

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

24
tsconfig.json Normal file
View File

@@ -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"
]
}

32
types.ts Normal file
View File

@@ -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: ""
};

62
utils.ts Normal file
View File

@@ -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<void> {
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);
}
}

12
version-bump.mjs Normal file
View File

@@ -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"));

3
versions.json Normal file
View File

@@ -0,0 +1,3 @@
{
"1.0.0": "0.15.0"
}

347
view.ts Normal file
View File

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