Add collapsible queue job cards
This commit is contained in:
@@ -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,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
|
||||||
|
|
||||||
|
|||||||
+56
-3
@@ -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>
|
||||||
|
<div class="job-body">
|
||||||
<pre></pre>
|
<pre></pre>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<span class="muted"></span>
|
<span class="muted"></span>
|
||||||
<div class="row" style="flex:0 0 auto"></div>
|
<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");
|
||||||
|
|||||||
Reference in New Issue
Block a user