;
@@ -145,8 +147,9 @@
if (showWorkspacePicker) { showWorkspacePicker = false; return; }
}
- function handleDragStart(e: DragEvent, taskId: string) {
+ function handleDragStart(e: DragEvent, taskId: string, group?: string) {
dragId = taskId;
+ dragGroup = group ?? null;
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", taskId);
@@ -173,7 +176,8 @@
}
}
- function handleDragOver(e: DragEvent, taskId: string) {
+ function handleDragOver(e: DragEvent, taskId: string, group?: string) {
+ if (group === "Overdue" && dragGroup !== "Overdue") return;
e.preventDefault();
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
dragOverId = taskId;
@@ -182,13 +186,35 @@
function handleDragEnd() {
dragId = null;
dragOverId = null;
+ dragGroup = null;
}
- async function handleDrop(e: DragEvent, targetId: string) {
+ async function handleDrop(e: DragEvent, targetId: string, group?: string) {
e.preventDefault();
- if (!dragId || dragId === targetId) { handleDragEnd(); return; }
+ const taskId = dragId;
+ const sourceGroup = dragGroup;
+ if (!taskId || taskId === targetId) { handleDragEnd(); return; }
+ if (group === "Overdue" && sourceGroup !== "Overdue") { handleDragEnd(); return; }
+
+ if (group !== undefined && group !== sourceGroup) {
+ const targetGroup = app.groupedPendingTasks?.find((g) => g.label === group);
+ const task = app.pendingTasks.find((t) => t.id === taskId);
+ if (task && targetGroup !== undefined) {
+ let newDueDate: string | null = null;
+ if (targetGroup.date !== null) {
+ const target = new Date(targetGroup.date);
+ if (task.has_time && task.due_date) {
+ const existing = new Date(task.due_date);
+ target.setHours(existing.getHours(), existing.getMinutes(), existing.getSeconds(), 0);
+ }
+ newDueDate = target.toISOString();
+ }
+ await app.updateTask({ ...task, due_date: newDueDate, has_time: newDueDate ? task.has_time : false });
+ }
+ }
+
const targetIndex = app.pendingTasks.findIndex((t) => t.id === targetId);
- if (targetIndex >= 0) await app.reorderTask(dragId, targetIndex);
+ if (targetIndex >= 0) await app.reorderTask(taskId, targetIndex);
handleDragEnd();
}
@@ -535,15 +561,17 @@
Delete completed
{/if}
-
+ {#if !app.isGoogleTasks}
+
+ {/if}
{/if}
@@ -575,22 +603,53 @@
Select a list
{:else}
- {#each app.pendingTasks as task (task.id)}
-
- handleDragStart(e, task.id)}
- ondragover={(e) => handleDragOver(e, task.id)}
- ondragend={handleDragEnd}
- ondrop={(e) => handleDrop(e, task.id)}
- class="{dragId === task.id ? 'opacity-30' : ''} {dragOverId === task.id && dragId !== task.id ? 'border-t-2 border-t-primary' : ''}"
- >
-
-
- {/each}
-
- {#if app.pendingTasks.length === 0}
- No tasks. Add one below.
+ {#if app.groupedPendingTasks}
+ {#each app.groupedPendingTasks as group (group.label)}
+ {group.label}
+ {#each group.tasks as task (task.id)}
+
+ handleDragStart(e, task.id, group.label)}
+ ondragover={(e) => handleDragOver(e, task.id, group.label)}
+ ondragend={handleDragEnd}
+ ondrop={(e) => handleDrop(e, task.id, group.label)}
+ class="{dragId === task.id ? 'opacity-30' : ''} {dragOverId === task.id && dragId !== task.id ? 'border-t-2 border-t-primary' : ''}"
+ >
+
+
+ {#if showSubtasks}
+ {#each app.getSubtasks(task.id).filter(s => s.status === "backlog") as subtask (subtask.id)}
+
+ {/each}
+ {/if}
+ {/each}
+ {/each}
+ {#if app.pendingTasks.length === 0}
+ No tasks. Add one below.
+ {/if}
+ {:else}
+ {#each app.pendingTasks as task (task.id)}
+
+ handleDragStart(e, task.id)}
+ ondragover={(e) => handleDragOver(e, task.id)}
+ ondragend={handleDragEnd}
+ ondrop={(e) => handleDrop(e, task.id)}
+ class="{dragId === task.id ? 'opacity-30' : ''} {dragOverId === task.id && dragId !== task.id ? 'border-t-2 border-t-primary' : ''}"
+ >
+
+
+ {#if showSubtasks}
+ {#each app.getSubtasks(task.id).filter(s => s.status === "backlog") as subtask (subtask.id)}
+
+ {/each}
+ {/if}
+ {/each}
+ {#if app.pendingTasks.length === 0}
+ No tasks. Add one below.
+ {/if}
{/if}
{#if app.completedTasks.length > 0}
diff --git a/apps/tauri/src/lib/stores/app.svelte.ts b/apps/tauri/src/lib/stores/app.svelte.ts
index 22179dc..039443a 100644
--- a/apps/tauri/src/lib/stores/app.svelte.ts
+++ b/apps/tauri/src/lib/stores/app.svelte.ts
@@ -52,6 +52,66 @@ let activeList = $derived(lists.find((l) => l.id === activeListId) ?? null);
let pendingTasks = $derived(tasks.filter((t) => t.status === "backlog" && !t.parent_id));
let completedTasks = $derived(tasks.filter((t) => t.status === "completed" && !t.parent_id));
+type TaskGroup = { label: string; tasks: Task[]; date: Date | null };
+
+let groupedPendingTasks = $derived.by((): TaskGroup[] | null => {
+ if (!activeList?.group_by_due_date) return null;
+ const now = new Date();
+ const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+ const tomorrowStart = new Date(todayStart);
+ tomorrowStart.setDate(todayStart.getDate() + 1);
+
+ const overdue: Task[] = [];
+ const today: Task[] = [];
+ const tomorrow: Task[] = [];
+ const futureByDay = new Map();
+ const noDate: Task[] = [];
+
+ for (const task of pendingTasks) {
+ if (!task.due_date) {
+ noDate.push(task);
+ } else {
+ const d = new Date(task.due_date);
+ const dayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate());
+ if (dayStart < todayStart) overdue.push(task);
+ else if (dayStart.getTime() === todayStart.getTime()) today.push(task);
+ else if (dayStart.getTime() === tomorrowStart.getTime()) tomorrow.push(task);
+ else {
+ const key = dayStart.toISOString();
+ if (!futureByDay.has(key)) futureByDay.set(key, { date: dayStart, tasks: [] });
+ futureByDay.get(key)!.tasks.push(task);
+ }
+ }
+ }
+
+ const taskOrderIndex = new Map(pendingTasks.map((t, i) => [t.id, i]));
+ const sortByDue = (a: Task, b: Task) => {
+ const dateDiff = new Date(a.due_date!).getTime() - new Date(b.due_date!).getTime();
+ if (dateDiff !== 0) return dateDiff;
+ return (taskOrderIndex.get(a.id) ?? 0) - (taskOrderIndex.get(b.id) ?? 0);
+ };
+ overdue.sort(sortByDue);
+ today.sort(sortByDue);
+ tomorrow.sort(sortByDue);
+
+ const groups: TaskGroup[] = [];
+ if (overdue.length) groups.push({ label: "Overdue", tasks: overdue, date: null });
+ if (today.length) groups.push({ label: "Today", tasks: today, date: todayStart });
+ if (tomorrow.length) groups.push({ label: "Tomorrow", tasks: tomorrow, date: tomorrowStart });
+
+ const currentYear = now.getFullYear();
+ for (const [, { date, tasks }] of [...futureByDay.entries()].sort(([a], [b]) => a.localeCompare(b))) {
+ tasks.sort(sortByDue);
+ const opts: Intl.DateTimeFormatOptions = date.getFullYear() !== currentYear
+ ? { weekday: "short", month: "short", day: "numeric", year: "numeric" }
+ : { weekday: "short", month: "short", day: "numeric" };
+ groups.push({ label: date.toLocaleDateString(undefined, opts), tasks, date });
+ }
+
+ if (noDate.length) groups.push({ label: "No Date", tasks: noDate, date: null });
+ return groups;
+});
+
// Build a map of parent_id -> children for subtask hierarchy
let childrenMap = $derived.by(() => {
const map = new Map();
@@ -533,6 +593,9 @@ export const app = {
get pendingTasks() {
return pendingTasks;
},
+ get groupedPendingTasks() {
+ return groupedPendingTasks;
+ },
get completedTasks() {
return completedTasks;
},
diff --git a/crates/onyx-core/src/config.rs b/crates/onyx-core/src/config.rs
index d7807be..4498b06 100644
--- a/crates/onyx-core/src/config.rs
+++ b/crates/onyx-core/src/config.rs
@@ -9,6 +9,7 @@ use crate::error::{Error, Result};
pub enum WorkspaceMode {
Local,
Webdav,
+ GoogleTasks,
}
impl Default for WorkspaceMode {
@@ -27,6 +28,9 @@ pub struct WorkspaceConfig {
pub webdav_url: Option,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub webdav_path: Option,
+ /// Display name / email of the connected Google account (GoogleTasks workspaces only).
+ #[serde(skip_serializing_if = "Option::is_none", default)]
+ pub google_account: Option,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub last_sync: Option>,
#[serde(skip_serializing_if = "Option::is_none", default)]
@@ -39,7 +43,7 @@ pub struct WorkspaceConfig {
impl WorkspaceConfig {
pub fn new(name: String, path: PathBuf) -> Self {
- Self { name, path, mode: WorkspaceMode::Local, webdav_url: None, webdav_path: None, last_sync: None, theme: None, sync_interval_secs: None, sync_interval_unfocused_secs: None }
+ Self { name, path, mode: WorkspaceMode::Local, webdav_url: None, webdav_path: None, google_account: None, last_sync: None, theme: None, sync_interval_secs: None, sync_interval_unfocused_secs: None }
}
}