Initial commit
This commit is contained in:
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal 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
32
build-package.bat
Normal 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
48
esbuild.config.mjs
Normal 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
173
main.ts
Normal 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
10
manifest.json
Normal 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
24
package.json
Normal 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
51
settings.ts
Normal 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
197
styles.css
Normal 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
24
tsconfig.json
Normal 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
32
types.ts
Normal 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
62
utils.ts
Normal 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
12
version-bump.mjs
Normal 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
3
versions.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"1.0.0": "0.15.0"
|
||||
}
|
||||
347
view.ts
Normal file
347
view.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user