From 9f39478018aa39fe08774420151dd0f476194f3f Mon Sep 17 00:00:00 2001 From: Dymas Date: Tue, 15 Sep 2026 15:37:43 +0200 Subject: [PATCH] Move library browsing to main grid --- CHANGELOG.md | 6 +++ README.md | 2 + VERSION | 2 +- package.json | 2 +- public/assets/app.js | 74 +++++++++++++++++++----------- public/assets/styles.css | 97 +++++++++++++++++++++++++++++++++++----- public/index.html | 20 ++++++--- 7 files changed, 158 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cac10c..66f879a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.3.0 - 2026-09-15 + +- Moved movie and series browsing out of the sidebar into a Jellyfin-style main collection grid. +- Added click-through collection cards that open the selected movie or series folder details and episodes in the main panel. +- Added a collection return action and kept sidebar controls focused on navigation, search, and rescans. + ## 1.2.1 - 2026-09-15 - Set the Docker Compose container host binding to `0.0.0.0` so Docker's localhost port mapping can reach h-player while keeping the host publish on `127.0.0.1`. diff --git a/README.md b/README.md index 17e3aab..9d62084 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ h-player is a small local web app for browsing a folder-based private video collection. The interface follows Kaizoku's dark purple glass-panel design, including the same card gradients, rounded controls, cover fallback treatment, and accent colors. The app remains a standalone private library browser and does not integrate with Plex, Jellyfin, Kodi, or any external metadata provider. +The sidebar holds app navigation, search, and rescanning controls. Movies and series appear in the main collection grid, similar to Jellyfin-style library browsing. Selecting a collection card opens that movie or series folder with its metadata, seasons, episodes, and playable files. + ## Folder Layout Place media in this shape: diff --git a/VERSION b/VERSION index 6085e94..f0bb29e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.1 +1.3.0 diff --git a/package.json b/package.json index 9ba07d2..d970dcb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "h-player", - "version": "1.2.1", + "version": "1.3.0", "private": true, "description": "A small private web app for browsing a folder-based video collection.", "scripts": { diff --git a/public/assets/app.js b/public/assets/app.js index c063eb4..c3a5da9 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -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 = `

No titles found

@@ -92,6 +83,33 @@ function renderDetails() { return; } + els.details.innerHTML = ` +
+ `; + + 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 = `
${title.thumbnail ? `` : `${initials(title.title)}`}
@@ -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 () => { diff --git a/public/assets/styles.css b/public/assets/styles.css index 488efee..d198fa1 100644 --- a/public/assets/styles.css +++ b/public/assets/styles.css @@ -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)); + } } diff --git a/public/index.html b/public/index.html index 1f55b48..4f8753e 100644 --- a/public/index.html +++ b/public/index.html @@ -26,8 +26,10 @@ Search collection - -
+
@@ -36,7 +38,7 @@

Collection

- Local +
@@ -48,10 +50,14 @@
-