Add collapsible queue job cards

This commit is contained in:
Dymas
2026-05-23 13:59:46 +02:00
parent 948753fe28
commit 5bec8713a5
4 changed files with 67 additions and 9 deletions
+4
View File
@@ -1,5 +1,9 @@
# Changelog # Changelog
## 0.35.2 - 2026-05-23
- Made queue job cards collapsible and defaulted finished jobs to collapsed so long queue histories take less space while running and pending jobs stay expanded.
## 0.35.1 - 2026-05-23 ## 0.35.1 - 2026-05-23
- Changed the shared sidebar navigation to a vertical one-link-per-line stack so the menu stays balanced after adding the dedicated Queue page. - Changed the shared sidebar navigation to a vertical one-link-per-line stack so the menu stays balanced after adding the dedicated Queue page.
+2 -1
View File
@@ -2,7 +2,7 @@
Small local web UI for a system-wide `ani-cli` install. Small local web UI for a system-wide `ani-cli` install.
Current version: `0.35.1` Current version: `0.35.2`
## Project Layout ## Project Layout
@@ -166,6 +166,7 @@ Highlights:
- Automatic queue polling every 2.5 seconds. - Automatic queue polling every 2.5 seconds.
- Dedicated queue pagination and cleanup controls. - Dedicated queue pagination and cleanup controls.
- Search and queue workflows are now separated into distinct pages. - Search and queue workflows are now separated into distinct pages.
- Each queue job card can be collapsed, and finished jobs start collapsed by default to keep the page compact.
### Config Page ### Config Page
+1 -1
View File
@@ -1 +1 @@
0.35.1 0.35.2
+60 -7
View File
@@ -209,21 +209,48 @@ QUEUE_HTML = r"""<!doctype html>
gap: 14px; gap: 14px;
transition: border-color 0.16s ease, transform 0.16s ease; transition: border-color 0.16s ease, transform 0.16s ease;
} }
.job.collapsed {
gap: 0;
}
.job:hover { .job:hover {
border-color: var(--line-strong); border-color: var(--line-strong);
transform: translateY(-1px); transform: translateY(-1px);
} }
.job-head { .job-head {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: auto 1fr auto;
gap: 12px; gap: 12px;
align-items: start; align-items: start;
min-width: 0; min-width: 0;
} }
.job-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
min-width: 40px;
padding: 0;
border-radius: 12px;
font-size: 18px;
line-height: 1;
}
.job-toggle::before {
content: "";
}
.job.collapsed .job-toggle::before {
content: "";
}
.job-title { .job-title {
font-weight: 800; font-weight: 800;
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.job-body {
display: grid;
gap: 14px;
}
.job.collapsed .job-body {
display: none;
}
.status { .status {
border-radius: 999px; border-radius: 999px;
padding: 6px 10px; padding: 6px 10px;
@@ -271,7 +298,8 @@ QUEUE_HTML = r"""<!doctype html>
.app { grid-template-columns: 1fr; } .app { grid-template-columns: 1fr; }
aside { border-right: 0; border-bottom: 1px solid var(--line); } aside { border-right: 0; border-bottom: 1px solid var(--line); }
.toolbar, .row { align-items: stretch; flex-direction: column; } .toolbar, .row { align-items: stretch; flex-direction: column; }
.job-head { grid-template-columns: 1fr; } .job-head { grid-template-columns: auto 1fr; }
.job-head .status { grid-column: 2; justify-self: start; }
} }
</style> </style>
</head> </head>
@@ -322,7 +350,8 @@ QUEUE_HTML = r"""<!doctype html>
queuePages: 1, queuePages: 1,
queueTotal: 0, queueTotal: 0,
queueRequestId: 0, queueRequestId: 0,
queueTimer: null queueTimer: null,
collapsedJobs: {}
}; };
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
@@ -342,6 +371,17 @@ QUEUE_HTML = r"""<!doctype html>
return btn; return btn;
} }
function isCollapsed(job) {
if (Object.prototype.hasOwnProperty.call(state.collapsedJobs, job.id)) {
return state.collapsedJobs[job.id];
}
return job.status === "done";
}
function setCollapsed(jobId, collapsed) {
state.collapsedJobs[jobId] = collapsed;
}
function renderQueue(data) { function renderQueue(data) {
const jobs = data.jobs || []; const jobs = data.jobs || [];
state.queuePage = data.page || 1; state.queuePage = data.page || 1;
@@ -357,21 +397,34 @@ QUEUE_HTML = r"""<!doctype html>
for (const job of jobs) { for (const job of jobs) {
const item = document.createElement("article"); const item = document.createElement("article");
item.className = "job"; item.className = "job";
const collapsed = isCollapsed(job);
if (collapsed) item.classList.add("collapsed");
const log = [...(job.log || [])].slice(-28).reverse().join("\n"); const log = [...(job.log || [])].slice(-28).reverse().join("\n");
item.innerHTML = ` item.innerHTML = `
<div class="job-head"> <div class="job-head">
<button class="job-toggle ghost" type="button" aria-label="Toggle job details" aria-expanded="true"></button>
<div> <div>
<div class="job-title"></div> <div class="job-title"></div>
<p class="muted"></p> <p class="muted"></p>
</div> </div>
<span class="status"></span> <span class="status"></span>
</div> </div>
<pre></pre> <div class="job-body">
<div class="toolbar"> <pre></pre>
<span class="muted"></span> <div class="toolbar">
<div class="row" style="flex:0 0 auto"></div> <span class="muted"></span>
<div class="row" style="flex:0 0 auto"></div>
</div>
</div> </div>
`; `;
const toggle = item.querySelector(".job-toggle");
toggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
toggle.addEventListener("click", () => {
const nextCollapsed = !item.classList.contains("collapsed");
item.classList.toggle("collapsed", nextCollapsed);
toggle.setAttribute("aria-expanded", nextCollapsed ? "false" : "true");
setCollapsed(job.id, nextCollapsed);
});
item.querySelector(".job-title").textContent = job.title; item.querySelector(".job-title").textContent = job.title;
const libraryName = job.anime_name || job.title; const libraryName = job.anime_name || job.title;
const season = String(job.season || "1").padStart(2, "0"); const season = String(job.season || "1").padStart(2, "0");