From 6b05ba3e2c1a26d953f99a968e26aff1141f4a8c Mon Sep 17 00:00:00 2001 From: Yuyao Huang Date: Fri, 8 May 2026 15:47:25 +0800 Subject: [PATCH] Improve tasks UI: scroll-to-focus picker, landscape layout, height alignment - Complete tasks now displayed in scroll view alongside unfinished tasks - Priority order: completed tasks first (by finished_time desc), then unfinished (by order asc) - Time picker-style scroll: wheel scroll snaps per task, center item gets visual focus - Landscape mode (>=1024px): scroll view + edit panel side by side, panel always visible - Portrait mode: edit panel slides in from right on tap - Fixed flex layout so scroll view and edit panel align perfectly in height --- database.py | 6 +- static/css/tasks.css | 137 ++++++++++++++++++++++++------------------- static/js/tasks.js | 92 +++++++++++++++++------------ templates/tasks.html | 75 ++++++++++++----------- 4 files changed, 174 insertions(+), 136 deletions(-) diff --git a/database.py b/database.py index 2d41133..b028a43 100644 --- a/database.py +++ b/database.py @@ -130,8 +130,8 @@ def get_tasks_sorted(goal_id): all_tasks = tasks_table.search(Task.goal_id == goal_id) unfinished = [t for t in all_tasks if t.get("status") != "done"] finished = [t for t in all_tasks if t.get("status") == "done"] - + unfinished.sort(key=lambda t: t.get("order", 0)) finished.sort(key=lambda t: t.get("finished_time", ""), reverse=True) - - return unfinished + finished + + return finished + unfinished diff --git a/static/css/tasks.css b/static/css/tasks.css index f01f9ed..065b157 100644 --- a/static/css/tasks.css +++ b/static/css/tasks.css @@ -42,7 +42,7 @@ .scroll-view { flex: 1; - height: 300px; + height: 600px; overflow-y: auto; background: white; border-radius: 8px; @@ -50,7 +50,7 @@ } .tasks-list { - padding: 120px 1rem; + padding: 45vh 2rem; } .task-item { @@ -165,63 +165,6 @@ opacity: 1; } -.finished-section { - flex: 0 0 300px; - background: white; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - padding: 1rem; - max-height: 600px; - overflow-y: auto; -} - -.finished-section h2 { - color: #2c3e50; - margin-bottom: 1rem; - font-size: 1.2rem; -} - -.finished-list { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.finished-item { - padding: 0.75rem; - background: #f8f9fa; - border-radius: 4px; - border-left: 3px solid #28a745; -} - -.finished-item-title { - font-weight: 500; - margin-bottom: 0.25rem; -} - -.finished-item-time { - font-size: 0.8rem; - color: #6c757d; -} - -.side-panel { - position: fixed; - top: 0; - right: -400px; - width: 400px; - height: 100%; - background: white; - box-shadow: -2px 0 8px rgba(0,0,0,0.1); - transition: right 0.3s ease; - z-index: 1000; - display: flex; - flex-direction: column; -} - -.side-panel.active { - right: 0; -} - .side-panel-header { padding: 1rem; border-bottom: 1px solid #ddd; @@ -259,6 +202,82 @@ margin-top: 1rem; } +@media (orientation: portrait), (max-width: 1023px) { + .side-panel { + position: fixed; + top: 0; + right: -400px; + width: 400px; + height: 100%; + background: white; + box-shadow: -2px 0 8px rgba(0,0,0,0.1); + transition: right 0.3s ease; + z-index: 1000; + display: flex; + flex-direction: column; + } + + .side-panel.active { + right: 0; + } +} + +@media (orientation: landscape) and (min-width: 1024px) { + .tasks-page { + display: flex; + flex-direction: column; + gap: 1rem; + height: calc(100vh - 2rem); + } + + .tasks-header { + flex-direction: row; + align-items: center; + justify-content: space-between; + flex-shrink: 0; + } + + .tasks-main { + display: flex; + gap: 1rem; + flex: 1; + min-height: 0; + } + + .tasks-container { + flex: 1; + display: flex; + margin-bottom: 0; + overflow: hidden; + } + + .scroll-view { + height: auto; + flex: 1; + } + + .side-panel { + position: relative; + right: 0; + width: 350px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + border-radius: 8px; + z-index: 1; + flex-shrink: 0; + display: flex; + flex-direction: column; + overflow: hidden; + } + + .side-panel-close { + display: none; + } + + .create-task-container { + flex-shrink: 0; + } +} + .empty-state { text-align: center; padding: 3rem; diff --git a/static/js/tasks.js b/static/js/tasks.js index 0f1f29c..6341c9d 100644 --- a/static/js/tasks.js +++ b/static/js/tasks.js @@ -1,6 +1,5 @@ let goals = []; let tasks = []; -let finishedTasks = []; let selectedGoalId = null; let selectedTaskId = null; let sortableInstance = null; @@ -27,20 +26,22 @@ async function loadGoals() { async function loadTasks() { if (!selectedGoalId) return; - + try { - const allTasks = await get(`/api/tasks?goal_id=${selectedGoalId}`); - tasks = allTasks.filter(t => t.status !== "done"); - finishedTasks = allTasks.filter(t => t.status === "done"); - + tasks = await get(`/api/tasks?goal_id=${selectedGoalId}`); + renderTasks(); - renderFinishedTasks(); initSortable(); initScrollFocus(); - + const doingTask = tasks.find(t => t.status === "doing"); if (doingTask) { scrollToTask(doingTask.id); + if (isLandscapeMode()) { + selectTask(doingTask.id); + } + } else if (isLandscapeMode() && tasks.length > 0) { + selectTask(tasks[0].id); } } catch (error) { console.error("Failed to load tasks:", error); @@ -68,22 +69,6 @@ function renderTasks() { `).join(""); } -function renderFinishedTasks() { - const container = document.getElementById("finished-list"); - - if (finishedTasks.length === 0) { - container.innerHTML = '

No completed tasks yet

'; - return; - } - - container.innerHTML = finishedTasks.map(task => ` -
-
${escapeHtml(task.title)}
-
Completed: ${formatTime(task.finished_time)}
-
- `).join(""); -} - function initSortable() { const container = document.getElementById("tasks-list"); @@ -142,46 +127,61 @@ function initScrollFocus() { function handleScrollFocus() { const scrollView = document.getElementById("scroll-view"); const taskItems = document.querySelectorAll(".task-item"); - + const scrollViewRect = scrollView.getBoundingClientRect(); const focusCenter = scrollViewRect.top + scrollViewRect.height / 2; - + let closestItem = null; let closestDistance = Infinity; - + taskItems.forEach(item => { const itemRect = item.getBoundingClientRect(); const itemCenter = itemRect.top + itemRect.height / 2; const distance = Math.abs(itemCenter - focusCenter); - + item.classList.remove("in-focus"); - + if (distance < closestDistance) { closestDistance = distance; closestItem = item; } }); - + if (closestItem) { closestItem.classList.add("in-focus"); + + if (isLandscapeMode()) { + const taskId = parseInt(closestItem.dataset.taskId); + if (taskId !== selectedTaskId) { + selectTask(taskId); + } + } } } +function isLandscapeMode() { + return window.innerWidth > window.innerHeight && window.innerWidth >= 1024; +} + function selectTask(taskId) { selectedTaskId = taskId; - const task = [...tasks, ...finishedTasks].find(t => t.id === taskId); - + const task = tasks.find(t => t.id === taskId); + if (!task) return; - + document.getElementById("edit-task-title").value = task.title; document.getElementById("edit-task-desc").value = task.desc || ""; document.getElementById("edit-task-status").value = task.status; document.getElementById("side-panel-error").textContent = ""; - - document.getElementById("side-panel").classList.add("active"); + + const sidePanel = document.getElementById("side-panel"); + if (!sidePanel.classList.contains("active")) { + sidePanel.classList.add("active"); + } } function closeSidePanel() { + if (isLandscapeMode()) return; document.getElementById("side-panel").classList.remove("active"); selectedTaskId = null; } @@ -204,7 +204,7 @@ async function saveTask() { try { await put(`/api/tasks/${selectedTaskId}`, { title, desc }); - const currentTask = [...tasks, ...finishedTasks].find(t => t.id === selectedTaskId); + const currentTask = tasks.find(t => t.id === selectedTaskId); if (status !== currentTask?.status) { await patch(`/api/tasks/${selectedTaskId}/status`, { status }); } @@ -282,6 +282,25 @@ function formatTime(isoString) { return date.toLocaleString(); } +function initWheelScroll() { + const scrollView = document.getElementById("scroll-view"); + + scrollView.addEventListener("wheel", (e) => { + e.preventDefault(); + + const taskItem = scrollView.querySelector(".task-item"); + if (!taskItem) return; + + const taskHeight = taskItem.offsetHeight + 8; + const direction = e.deltaY > 0 ? 1 : -1; + + scrollView.scrollBy({ + top: taskHeight * direction, + behavior: "smooth" + }); + }, { passive: false }); +} + document.addEventListener("DOMContentLoaded", () => { document.getElementById("goal-selector").addEventListener("change", (e) => { selectedGoalId = parseInt(e.target.value); @@ -297,5 +316,6 @@ document.addEventListener("DOMContentLoaded", () => { document.getElementById("save-task-btn").addEventListener("click", saveTask); document.getElementById("delete-task-btn").addEventListener("click", deleteTask); + initWheelScroll(); loadGoals(); }); diff --git a/templates/tasks.html b/templates/tasks.html index cce4746..dd45103 100644 --- a/templates/tasks.html +++ b/templates/tasks.html @@ -16,18 +16,48 @@ -
-
-
+
+
+
+
+
-
-

Completed Tasks

-
+
+
+

Edit Task

+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
- +
+ +
- -
-
-

Edit Task

- -
-
-
- - -
-
- - -
-
- - -
-
-
- - -
-
-
{% endblock %} {% block extra_js %}