Polished timer and UI
This commit is contained in:
129
styles.css
129
styles.css
@@ -53,6 +53,98 @@
|
|||||||
background-color: var(--background-modifier-error-hover);
|
background-color: var(--background-modifier-error-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.taskweaver-active-timer {
|
||||||
|
background: linear-gradient(135deg, var(--background-secondary) 0%, var(--background-primary-alt) 100%);
|
||||||
|
border: 2px solid var(--interactive-accent);
|
||||||
|
border-radius: var(--radius-l);
|
||||||
|
padding: var(--size-4-4);
|
||||||
|
margin-bottom: var(--size-4-4);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskweaver-active-timer-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--size-4-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskweaver-active-timer-label {
|
||||||
|
font-size: var(--font-ui-smaller);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskweaver-active-timer-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--font-ui-large);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-normal);
|
||||||
|
margin-bottom: var(--size-4-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskweaver-active-timer-display {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 48px;
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
color: var(--interactive-accent);
|
||||||
|
margin: var(--size-4-4) 0;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskweaver-active-timer-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--size-4-2);
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: var(--size-4-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskweaver-timer-control-btn {
|
||||||
|
padding: var(--size-4-2) var(--size-4-4);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-m);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-ui-medium);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex: 1;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskweaver-timer-control-btn.pause {
|
||||||
|
background-color: var(--color-orange);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskweaver-timer-control-btn.pause:hover {
|
||||||
|
background-color: var(--color-orange);
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskweaver-timer-control-btn.stop {
|
||||||
|
background-color: var(--background-modifier-error);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskweaver-timer-control-btn.stop:hover {
|
||||||
|
background-color: var(--background-modifier-error);
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskweaver-timer-control-btn.complete {
|
||||||
|
background-color: var(--color-green);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskweaver-timer-control-btn.complete:hover {
|
||||||
|
background-color: var(--color-green);
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
.taskweaver-category {
|
.taskweaver-category {
|
||||||
margin-bottom: var(--size-4-3);
|
margin-bottom: var(--size-4-3);
|
||||||
background-color: var(--background-secondary);
|
background-color: var(--background-secondary);
|
||||||
@@ -123,32 +215,47 @@
|
|||||||
gap: var(--size-4-1);
|
gap: var(--size-4-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.taskweaver-timer {
|
.taskweaver-task-running {
|
||||||
|
opacity: 0.6;
|
||||||
|
background-color: var(--background-primary-alt) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskweaver-timer-small {
|
||||||
font-family: var(--font-monospace);
|
font-family: var(--font-monospace);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: var(--font-ui-smaller);
|
font-size: var(--font-ui-smaller);
|
||||||
min-width: 70px;
|
min-width: 60px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.taskweaver-timer-btn,
|
.taskweaver-start-btn {
|
||||||
|
background-color: var(--interactive-accent);
|
||||||
|
color: var(--text-on-accent);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
padding: var(--size-2-1) var(--size-4-2);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-ui-small);
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskweaver-start-btn:hover {
|
||||||
|
background-color: var(--interactive-accent-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
.taskweaver-delete-btn {
|
.taskweaver-delete-btn {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid var(--background-modifier-border);
|
border: 1px solid var(--background-modifier-border);
|
||||||
border-radius: var(--radius-s);
|
border-radius: var(--radius-s);
|
||||||
padding: var(--size-2-1) var(--size-2-3);
|
padding: var(--size-2-1) var(--size-2-3);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: var(--font-ui-small);
|
font-size: var(--font-ui-smaller);
|
||||||
color: var(--text-normal);
|
color: var(--text-muted);
|
||||||
transition: all 0.2s;
|
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 {
|
.taskweaver-delete-btn:hover {
|
||||||
background-color: var(--background-modifier-error);
|
background-color: var(--background-modifier-error);
|
||||||
color: var(--text-on-accent);
|
color: var(--text-on-accent);
|
||||||
|
|||||||
73
utils.ts
73
utils.ts
@@ -1,4 +1,4 @@
|
|||||||
import { App, TFile } from "obsidian";
|
import { App, TFile, Notice, moment } from "obsidian";
|
||||||
import { Task, Category } from "./types";
|
import { Task, Category } from "./types";
|
||||||
|
|
||||||
export function formatDuration(milliseconds: number): string {
|
export function formatDuration(milliseconds: number): string {
|
||||||
@@ -20,34 +20,66 @@ export function generateUniqueId(): string {
|
|||||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getDailyNote(app: App): Promise<TFile> {
|
async function getDailyNote(app: App): Promise<TFile | null> {
|
||||||
const { vault } = app;
|
const { vault } = app;
|
||||||
|
|
||||||
// @ts-ignore - Access internal daily notes API
|
// @ts-ignore - Access internal plugins
|
||||||
const dailyNotesPlugin = app.internalPlugins.plugins["daily-notes"];
|
const dailyNotesPlugin = app.internalPlugins.plugins["daily-notes"];
|
||||||
|
|
||||||
if (!dailyNotesPlugin || !dailyNotesPlugin.enabled) {
|
if (!dailyNotesPlugin || !dailyNotesPlugin.enabled) {
|
||||||
throw new Error("Daily notes plugin is not enabled. Please enable it in Settings → Core plugins.");
|
new Notice("Daily notes plugin is not enabled. Please enable it in Settings → Core plugins.");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore - Access daily notes interface
|
// @ts-ignore - Access the daily notes instance
|
||||||
const { createDailyNote, getDailyNote, getAllDailyNotes } = app.internalPlugins.plugins["daily-notes"].instance;
|
const dailyNotesInterface = dailyNotesPlugin.instance;
|
||||||
|
|
||||||
// @ts-ignore
|
if (!dailyNotesInterface) {
|
||||||
const dailyNotes = getAllDailyNotes();
|
new Notice("Could not access daily notes interface");
|
||||||
// @ts-ignore
|
return null;
|
||||||
const today = window.moment();
|
|
||||||
// @ts-ignore
|
|
||||||
let dailyNote = getDailyNote(today, dailyNotes);
|
|
||||||
|
|
||||||
if (!dailyNote) {
|
|
||||||
// @ts-ignore
|
|
||||||
dailyNote = await createDailyNote(today);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// @ts-ignore
|
||||||
|
const { format, folder, template } = dailyNotesInterface.options || {};
|
||||||
|
|
||||||
|
const dateFormat = format || "YYYY-MM-DD";
|
||||||
|
const dailyNotesFolder = folder || "";
|
||||||
|
const today = moment().format(dateFormat);
|
||||||
|
|
||||||
|
const fileName = `${today}.md`;
|
||||||
|
const filePath = dailyNotesFolder ? `${dailyNotesFolder}/${fileName}` : fileName;
|
||||||
|
|
||||||
|
let dailyNote = vault.getAbstractFileByPath(filePath);
|
||||||
|
|
||||||
|
if (!dailyNote || !(dailyNote instanceof TFile)) {
|
||||||
|
if (dailyNotesFolder && !vault.getAbstractFileByPath(dailyNotesFolder)) {
|
||||||
|
await vault.createFolder(dailyNotesFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
let initialContent = "";
|
||||||
|
if (template) {
|
||||||
|
const templateFile = vault.getAbstractFileByPath(template);
|
||||||
|
if (templateFile instanceof TFile) {
|
||||||
|
initialContent = await vault.read(templateFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dailyNote = await vault.create(filePath, initialContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dailyNote instanceof TFile) {
|
||||||
return dailyNote;
|
return dailyNote;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting daily note:", error);
|
||||||
|
new Notice("Error accessing daily note: " + error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function logTaskToDailyNote(
|
export async function logTaskToDailyNote(
|
||||||
app: App,
|
app: App,
|
||||||
task: Task,
|
task: Task,
|
||||||
@@ -57,6 +89,11 @@ export async function logTaskToDailyNote(
|
|||||||
try {
|
try {
|
||||||
const dailyNote = await getDailyNote(app);
|
const dailyNote = await getDailyNote(app);
|
||||||
|
|
||||||
|
if (!dailyNote) {
|
||||||
|
new Notice("Could not find or create daily note");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const duration = formatDuration(task.totalElapsed);
|
const duration = formatDuration(task.totalElapsed);
|
||||||
const logEntry = format
|
const logEntry = format
|
||||||
.replace("{{title}}", task.title)
|
.replace("{{title}}", task.title)
|
||||||
@@ -67,8 +104,10 @@ export async function logTaskToDailyNote(
|
|||||||
const content = await app.vault.read(dailyNote);
|
const content = await app.vault.read(dailyNote);
|
||||||
const newContent = content ? `${content}\n${logEntry}` : logEntry;
|
const newContent = content ? `${content}\n${logEntry}` : logEntry;
|
||||||
await app.vault.modify(dailyNote, newContent);
|
await app.vault.modify(dailyNote, newContent);
|
||||||
|
|
||||||
|
new Notice("Task logged to daily note");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to log task to daily note:", error);
|
console.error("Failed to log task to daily note:", error);
|
||||||
throw error;
|
new Notice("Failed to log task: " + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
100
view.ts
100
view.ts
@@ -63,6 +63,12 @@ export class TaskWeaverView extends ItemView {
|
|||||||
|
|
||||||
const categoriesMap = new Map(this.plugin.settings.categories.map(c => [c.id, c]));
|
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 => {
|
this.plugin.settings.categories.forEach(category => {
|
||||||
const categoryTasks = this.plugin.settings.tasks.filter(t => t.categoryId === category.id && !t.completed);
|
const categoryTasks = this.plugin.settings.tasks.filter(t => t.categoryId === category.id && !t.completed);
|
||||||
|
|
||||||
@@ -84,6 +90,55 @@ export class TaskWeaverView extends ItemView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.toggleTaskTimer(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 {
|
renderCategory(container: HTMLElement, category: Category, tasks: Task[]): void {
|
||||||
const categoryDiv = container.createDiv({ cls: "taskweaver-category" });
|
const categoryDiv = container.createDiv({ cls: "taskweaver-category" });
|
||||||
|
|
||||||
@@ -104,14 +159,11 @@ export class TaskWeaverView extends ItemView {
|
|||||||
renderTask(container: HTMLElement, task: Task, category?: Category): void {
|
renderTask(container: HTMLElement, task: Task, category?: Category): void {
|
||||||
const taskDiv = container.createDiv({ cls: "taskweaver-task" });
|
const taskDiv = container.createDiv({ cls: "taskweaver-task" });
|
||||||
|
|
||||||
const taskInfo = taskDiv.createDiv({ cls: "taskweaver-task-info" });
|
if (task.startTime) {
|
||||||
|
taskDiv.addClass("taskweaver-task-running");
|
||||||
|
}
|
||||||
|
|
||||||
const checkbox = taskInfo.createEl("input", { type: "checkbox" });
|
const taskInfo = taskDiv.createDiv({ cls: "taskweaver-task-info" });
|
||||||
checkbox.checked = task.completed;
|
|
||||||
checkbox.addClass("taskweaver-checkbox");
|
|
||||||
checkbox.onclick = async () => {
|
|
||||||
await this.plugin.toggleTaskComplete(task.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const taskTitle = taskInfo.createSpan({
|
const taskTitle = taskInfo.createSpan({
|
||||||
text: task.title,
|
text: task.title,
|
||||||
@@ -121,34 +173,54 @@ export class TaskWeaverView extends ItemView {
|
|||||||
const taskControls = taskDiv.createDiv({ cls: "taskweaver-task-controls" });
|
const taskControls = taskDiv.createDiv({ cls: "taskweaver-task-controls" });
|
||||||
|
|
||||||
const timerSpan = taskControls.createSpan({
|
const timerSpan = taskControls.createSpan({
|
||||||
cls: "taskweaver-timer",
|
cls: "taskweaver-timer-small",
|
||||||
attr: { "data-task-id": task.id }
|
attr: { "data-task-id": task.id }
|
||||||
});
|
});
|
||||||
timerSpan.setText(formatDuration(this.plugin.getTaskElapsed(task)));
|
timerSpan.setText(formatDuration(this.plugin.getTaskElapsed(task)));
|
||||||
|
|
||||||
if (!task.completed) {
|
if (!task.completed) {
|
||||||
const timerBtn = taskControls.createEl("button", {
|
if (!task.startTime) {
|
||||||
text: task.startTime ? "⏸" : "▶",
|
const startBtn = taskControls.createEl("button", {
|
||||||
cls: "taskweaver-timer-btn"
|
text: "Start",
|
||||||
|
cls: "taskweaver-start-btn"
|
||||||
});
|
});
|
||||||
timerBtn.onclick = async () => {
|
startBtn.onclick = async () => {
|
||||||
await this.plugin.toggleTaskTimer(task.id);
|
await this.plugin.toggleTaskTimer(task.id);
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const deleteBtn = taskControls.createEl("button", {
|
const deleteBtn = taskControls.createEl("button", {
|
||||||
text: "🗑",
|
text: "Delete",
|
||||||
cls: "taskweaver-delete-btn"
|
cls: "taskweaver-delete-btn"
|
||||||
});
|
});
|
||||||
deleteBtn.onclick = async () => {
|
deleteBtn.onclick = async () => {
|
||||||
await this.plugin.deleteTask(task.id);
|
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 {
|
updateTimers(): void {
|
||||||
const container = this.containerEl.children[1] as HTMLElement;
|
const container = this.containerEl.children[1] as HTMLElement;
|
||||||
const timerElements = container.querySelectorAll(".taskweaver-timer");
|
|
||||||
|
|
||||||
|
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) => {
|
timerElements.forEach((element) => {
|
||||||
const taskId = element.getAttribute("data-task-id");
|
const taskId = element.getAttribute("data-task-id");
|
||||||
if (taskId) {
|
if (taskId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user