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 = `