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
This commit is contained in:
Yuyao Huang
2026-05-08 15:47:25 +08:00
parent 79fde447e9
commit 6b05ba3e2c
4 changed files with 174 additions and 136 deletions
+56 -36
View File
@@ -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 = '<p style="color: #7f8c8d;">No completed tasks yet</p>';
return;
}
container.innerHTML = finishedTasks.map(task => `
<div class="finished-item" onclick="selectTask(${task.id})">
<div class="finished-item-title">${escapeHtml(task.title)}</div>
<div class="finished-item-time">Completed: ${formatTime(task.finished_time)}</div>
</div>
`).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();
});