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:
+56
-36
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user