Move library browsing to main grid

This commit is contained in:
Dymas
2026-09-15 15:37:43 +02:00
parent a2b26f6680
commit 9f39478018
7 changed files with 158 additions and 45 deletions
+49 -25
View File
@@ -1,17 +1,18 @@
const state = {
titles: [],
activeId: "",
query: ""
query: "",
view: "collection"
};
const els = {
libraryBadge: document.querySelector("#libraryBadge"),
libraryPath: document.querySelector("#libraryPath"),
backBtn: document.querySelector("#backBtn"),
rescanBtn: document.querySelector("#rescanBtn"),
searchInput: document.querySelector("#searchInput"),
titleList: document.querySelector("#titleList"),
details: document.querySelector("#details"),
titleButtonTemplate: document.querySelector("#titleButtonTemplate")
titleCardTemplate: document.querySelector("#titleCardTemplate")
};
function bytes(value) {
@@ -40,9 +41,9 @@ async function loadLibrary() {
state.titles = data.titles;
els.libraryPath.textContent = data.libraryDir;
els.libraryBadge.textContent = `${state.titles.length} title${state.titles.length === 1 ? "" : "s"}`;
if (!state.activeId && state.titles.length) state.activeId = state.titles[0].id;
if (!state.titles.some((title) => title.id === state.activeId)) {
state.activeId = state.titles[0]?.id || "";
state.activeId = "";
state.view = "collection";
}
render();
}
@@ -61,28 +62,18 @@ function filteredTitles() {
}
function render() {
renderTitleList();
renderDetails();
}
function renderTitleList() {
els.titleList.innerHTML = "";
for (const title of filteredTitles()) {
const node = els.titleButtonTemplate.content.firstElementChild.cloneNode(true);
node.classList.toggle("is-active", title.id === state.activeId);
node.querySelector(".title-button__name").textContent = title.title;
node.querySelector(".title-button__meta").textContent = `${title.count} file${title.count === 1 ? "" : "s"} · ${bytes(title.size)}`;
node.addEventListener("click", () => {
state.activeId = title.id;
render();
});
els.titleList.append(node);
const activeTitle = state.titles.find((item) => item.id === state.activeId);
els.backBtn.classList.toggle("hidden", state.view !== "details");
if (state.view === "details" && activeTitle) {
renderDetails(activeTitle);
return;
}
renderCollection();
}
function renderDetails() {
const title = state.titles.find((item) => item.id === state.activeId);
if (!title) {
function renderCollection() {
const titles = filteredTitles();
if (!titles.length) {
els.details.innerHTML = `
<div class="empty">
<h2>No titles found</h2>
@@ -92,6 +83,33 @@ function renderDetails() {
return;
}
els.details.innerHTML = `
<div class="collection-grid" id="collectionGrid"></div>
`;
const grid = document.querySelector("#collectionGrid");
for (const title of titles) {
const node = els.titleCardTemplate.content.firstElementChild.cloneNode(true);
node.querySelector(".title-card__name").textContent = title.title;
node.querySelector(".title-card__meta").textContent = `${title.count} file${title.count === 1 ? "" : "s"} · ${bytes(title.size)}`;
node.querySelector(".title-card__fallback").textContent = initials(title.title);
const poster = node.querySelector(".title-card__poster");
const image = node.querySelector("img");
if (title.thumbnail) {
poster.classList.add("has-image");
image.src = title.thumbnail;
image.alt = "";
}
node.addEventListener("click", () => {
state.activeId = title.id;
state.view = "details";
render();
});
grid.append(node);
}
}
function renderDetails(title) {
els.details.innerHTML = `
<div class="hero">
<div class="poster">${title.thumbnail ? `<img src="${escapeAttr(title.thumbnail)}" alt="">` : `<span>${initials(title.title)}</span>`}</div>
@@ -192,7 +210,13 @@ function escapeAttr(value) {
els.searchInput.addEventListener("input", (event) => {
state.query = event.target.value;
renderTitleList();
state.view = "collection";
render();
});
els.backBtn.addEventListener("click", () => {
state.view = "collection";
render();
});
els.rescanBtn.addEventListener("click", async () => {
+86 -11
View File
@@ -75,6 +75,14 @@ button:disabled {
transform: none;
}
button.ghost {
background: transparent;
}
.hidden {
display: none !important;
}
input,
textarea {
width: 100%;
@@ -170,7 +178,7 @@ h3 {
.shell-note,
.muted,
.title-button__meta,
.title-card__meta,
.episode span,
.summary p,
label span {
@@ -238,13 +246,36 @@ label {
letter-spacing: 0.08em;
}
.title-list {
.sidebar-note {
display: grid;
gap: 6px;
border: 1px solid var(--line);
border-radius: 18px;
background: rgba(255, 255, 255, 0.03);
padding: 14px;
}
.sidebar-note__label {
color: #ddd0ff;
font-size: 11px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.sidebar-note p {
color: var(--muted);
font-size: 13px;
}
.collection-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
min-width: 0;
}
.title-button {
.title-card {
display: grid;
gap: 12px;
width: 100%;
@@ -258,25 +289,65 @@ label {
transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease;
}
.title-button:hover {
.title-card:hover {
border-color: var(--line-strong);
background: linear-gradient(180deg, rgba(38, 28, 64, 0.94) 0%, rgba(19, 14, 32, 0.98) 100%);
transform: translateY(-2px);
}
.title-button.is-active {
border-color: rgba(157, 123, 255, 0.58);
background: linear-gradient(180deg, rgba(48, 36, 80, 0.96), rgba(20, 15, 35, 0.96));
box-shadow:
inset 0 0 0 1px rgba(157, 123, 255, 0.16),
0 18px 38px rgba(5, 2, 15, 0.32);
.title-card__poster {
aspect-ratio: 3 / 4;
position: relative;
border: 1px solid rgba(157, 123, 255, 0.16);
border-radius: 16px;
background:
radial-gradient(circle at top, rgba(157, 123, 255, 0.28), transparent 44%),
linear-gradient(180deg, #221734 0%, #0c0915 100%);
overflow: hidden;
display: grid;
place-items: center;
}
.title-button__name {
.title-card__poster img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
opacity: 0;
transition: opacity 0.16s ease;
}
.title-card__poster.has-image img {
opacity: 1;
}
.title-card__fallback {
position: absolute;
inset: 0;
display: grid;
place-items: center;
color: #d8e5ef;
font-size: 28px;
font-weight: 800;
letter-spacing: 0;
text-transform: uppercase;
background: linear-gradient(180deg, rgba(157, 123, 255, 0.24), rgba(9, 7, 15, 0.18));
}
.title-card__poster.has-image .title-card__fallback {
display: none;
}
.title-card__name {
font-weight: 800;
overflow-wrap: anywhere;
}
.title-card__meta {
color: var(--muted);
font-size: 13px;
}
.toolbar,
.details,
.empty,
@@ -486,4 +557,8 @@ video {
.summary h2 {
font-size: 2rem;
}
.collection-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
}