diff --git a/apps/admin-ui/src/app/settings/page.tsx b/apps/admin-ui/src/app/settings/page.tsx index c2b9a75..77d1990 100644 --- a/apps/admin-ui/src/app/settings/page.tsx +++ b/apps/admin-ui/src/app/settings/page.tsx @@ -16,6 +16,7 @@ function parseTab(v: string | null): SettingsTab { interface ShelfRow { id: string; name: string; + homepageName?: string | null; slug: string; kind: "MOVIE" | "SERIES"; sortOrder: number; @@ -230,6 +231,21 @@ export default function SettingsPage() { loadShelves(); } + async function saveShelfHomepageName(id: string, homepageName: string) { + const res = await fetch(`/api/v1/admin/library/shelves/${id}`, { + method: "PATCH", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ homepageName: homepageName.trim() || null }), + }); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + alert(data.error?.message ?? "Opslaan mislukt"); + return; + } + loadShelves(); + } + async function saveConfig() { setCfgBusy(true); setCfgMsg(null); @@ -365,6 +381,7 @@ export default function SettingsPage() {

Bibliotheek-planken

Namen zoals Kids Films of NL Series. Op Nodes koppel je scanpaden aan een plank. + Met "Homepage naam" toon je op de TV-startpagina een kortere of andere titel.

@@ -396,6 +413,7 @@ export default function SettingsPage() { Naam + Homepage naam Type Pad(en) Bestanden @@ -406,6 +424,24 @@ export default function SettingsPage() { {shelves.map((s) => ( {s.name} + + { + const next = e.target.value.trim(); + const prev = (s.homepageName ?? "").trim(); + if (next !== prev) saveShelfHomepageName(s.id, next); + }} + onKeyDown={(e) => { + if (e.key === "Enter") (e.target as HTMLInputElement).blur(); + }} + style={{ minWidth: "10rem" }} + /> + {s.kind === "MOVIE" ? "Films" : "Series"} {s.paths?.length ? ( @@ -439,7 +475,7 @@ export default function SettingsPage() { ))} {shelves.length === 0 && ( - + Laden… @@ -461,6 +497,20 @@ export default function SettingsPage() { {s.mediaFileCount} bestanden {s.scanRootCount} pad(en) + {s.paths?.length ? (
{s.paths.map((p) => ( diff --git a/apps/android-tv/app/build.gradle.kts b/apps/android-tv/app/build.gradle.kts index 0fb73b3..5e037ab 100644 --- a/apps/android-tv/app/build.gradle.kts +++ b/apps/android-tv/app/build.gradle.kts @@ -12,8 +12,8 @@ android { applicationId = "nl.vonas.mediacluster.tv" minSdk = 24 targetSdk = 35 - versionCode = 19 - versionName = "0.8.2" + versionCode = 21 + versionName = "0.8.4" buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"") ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a") diff --git a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/data/ApiClient.kt b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/data/ApiClient.kt index 4d40596..4b91f2b 100644 --- a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/data/ApiClient.kt +++ b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/data/ApiClient.kt @@ -312,10 +312,14 @@ data class HomePayload( data class ShelfRow( val id: String, val title: String, + val name: String = title, val kind: String, val slug: String = "", val items: List, -) +) { + /** Canonical shelf name for browse / sub-nav (not the homepage label). */ + val browseTitle: String get() = name.ifBlank { title } +} data class MediaItem( val type: String, @@ -409,6 +413,7 @@ private fun JSONArray?.toShelves(): List { ShelfRow( id = o.optString("id"), title = o.optString("title"), + name = o.optString("name").ifBlank { o.optString("title") }, kind = o.optString("kind"), slug = o.optString("slug").ifBlank { o.optString("id") }, items = o.optJSONArray("items").toItems(), diff --git a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/BrowseScreen.kt b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/BrowseScreen.kt index 1b68dae..512c23e 100644 --- a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/BrowseScreen.kt +++ b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/BrowseScreen.kt @@ -154,9 +154,9 @@ fun BrowseScreen( LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 140.dp), state = gridState, - contentPadding = PaddingValues(bottom = 40.dp), - horizontalArrangement = Arrangement.spacedBy(14.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(start = 8.dp, top = 12.dp, end = 8.dp, bottom = 40.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxSize(), ) { items(items, key = { "${it.type}:${it.id}:${it.progressId}" }) { item -> @@ -164,6 +164,7 @@ fun BrowseScreen( item = item, width = 140.dp, height = 210.dp, + modifier = Modifier.padding(6.dp), onClick = { val canResume = onPlayContinue != null && !item.mediaFileId.isNullOrBlank() && diff --git a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/McNav.kt b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/McNav.kt index 46cc931..c22232a 100644 --- a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/McNav.kt +++ b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/McNav.kt @@ -626,7 +626,7 @@ private fun HomeScreen( ShelfBlock( title = shelf.title, items = shelf.items, - onSeeAll = { onSeeAll(catalogId, shelf.title) }, + onSeeAll = { onSeeAll(catalogId, shelf.browseTitle) }, onClick = { item -> onContentFocused() if (item.type == "movie") onOpenMovie(item.id) else onOpenSeries(item.id) @@ -836,9 +836,9 @@ private fun ShelfBlock( onClick: (MediaItem) -> Unit, onLongClick: ((MediaItem) -> Unit)? = null, ) { - Column(Modifier.padding(start = 48.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) { + Column(verticalArrangement = Arrangement.spacedBy(14.dp)) { Row( - Modifier.fillMaxWidth().padding(end = 48.dp), + Modifier.fillMaxWidth().padding(horizontal = 48.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -852,9 +852,10 @@ private fun ShelfBlock( McButton("Alles") { onSeeAll() } } } + // contentPadding i.p.v. Column-startpadding: focus-scale mag in de marge tekenen LazyRow( horizontalArrangement = Arrangement.spacedBy(10.dp), - contentPadding = PaddingValues(end = 48.dp), + contentPadding = PaddingValues(start = 48.dp, end = 48.dp, top = 18.dp, bottom = 18.dp), ) { items(items, key = { "${it.type}:${it.id}:${it.progressId}" }) { item -> PosterCard( diff --git a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/SearchScreen.kt b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/SearchScreen.kt index e45c77a..5c3e786 100644 --- a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/SearchScreen.kt +++ b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/SearchScreen.kt @@ -260,9 +260,9 @@ fun SearchScreen( LazyVerticalGrid( columns = GridCells.Adaptive(minSize = 110.dp), state = gridState, - contentPadding = PaddingValues(bottom = 24.dp, end = 8.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalArrangement = Arrangement.spacedBy(14.dp), + contentPadding = PaddingValues(start = 8.dp, end = 8.dp, top = 12.dp, bottom = 24.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxSize(), ) { items(results, key = { "${it.type}:${it.id}" }) { item -> @@ -270,6 +270,7 @@ fun SearchScreen( item = item, width = 110.dp, height = 165.dp, + modifier = Modifier.padding(6.dp), onClick = { if (item.type == "movie") onOpenMovie(item.id) else onOpenSeries(item.id) }, diff --git a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/components/Components.kt b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/components/Components.kt index 55ae60f..b9ceb81 100644 --- a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/components/Components.kt +++ b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/components/Components.kt @@ -137,7 +137,7 @@ fun PosterCard( }, ), shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(4.dp)), - scale = ClickableSurfaceDefaults.scale(focusedScale = 1.14f), + scale = ClickableSurfaceDefaults.scale(focusedScale = 1.10f), border = ClickableSurfaceDefaults.border( focusedBorder = Border( border = androidx.compose.foundation.BorderStroke(3.dp, Color.White), diff --git a/apps/master-api/prisma/migrations/20260901160000_shelf_homepage_name/migration.sql b/apps/master-api/prisma/migrations/20260901160000_shelf_homepage_name/migration.sql new file mode 100644 index 0000000..c16675f --- /dev/null +++ b/apps/master-api/prisma/migrations/20260901160000_shelf_homepage_name/migration.sql @@ -0,0 +1,2 @@ +-- Optional display label for Android TV / client home rows (falls back to name). +ALTER TABLE "library_shelves" ADD COLUMN IF NOT EXISTS "homepage_name" TEXT; diff --git a/apps/master-api/prisma/schema.prisma b/apps/master-api/prisma/schema.prisma index 0a889c3..a1995be 100644 --- a/apps/master-api/prisma/schema.prisma +++ b/apps/master-api/prisma/schema.prisma @@ -278,9 +278,10 @@ model Node { /** Named Stremio catalog / shelf shared across all nodes (e.g. Kids Films). */ model LibraryShelf { - id String @id @default(uuid()) - name String - slug String @unique + id String @id @default(uuid()) + name String + homepageName String? @map("homepage_name") + slug String @unique kind LibraryShelfKind sortOrder Int @default(0) @map("sort_order") enabled Boolean @default(true) diff --git a/apps/master-api/src/admin/routes.ts b/apps/master-api/src/admin/routes.ts index 8262269..9f30091 100644 --- a/apps/master-api/src/admin/routes.ts +++ b/apps/master-api/src/admin/routes.ts @@ -596,6 +596,7 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config) shelves: shelves.map((s) => ({ id: s.id, name: s.name, + homepageName: s.homepageName, slug: s.slug, kind: s.kind, sortOrder: s.sortOrder, @@ -636,6 +637,7 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config) const { id } = request.params as { id: string }; const body = request.body as { name?: string; + homepageName?: string | null; enabled?: boolean; sortOrder?: number; }; @@ -645,6 +647,10 @@ export async function registerAdminRoutes(app: FastifyInstance, config: Config) if (!name) throw new AppError("INVALID_REQUEST", "name required", 400); data.name = name; } + if (body.homepageName !== undefined) { + const homepageName = body.homepageName?.trim(); + data.homepageName = homepageName ? homepageName : null; + } if (body.enabled !== undefined) data.enabled = body.enabled; if (body.sortOrder !== undefined) data.sortOrder = body.sortOrder; const shelf = await prisma.libraryShelf.update({ where: { id }, data }); diff --git a/apps/master-api/src/viewer/service.ts b/apps/master-api/src/viewer/service.ts index f68b55d..f7a6f6a 100644 --- a/apps/master-api/src/viewer/service.ts +++ b/apps/master-api/src/viewer/service.ts @@ -326,6 +326,7 @@ export class ViewerService { return { id: s.id, name: s.name, + homepageName: s.homepageName, slug: s.slug, kind: s.kind as "MOVIE" | "SERIES", visible: pref?.visible ?? true, @@ -338,6 +339,7 @@ export class ViewerService { const rows: Array<{ id: string; title: string; + name: string; kind: "MOVIE" | "SERIES"; slug: string; items: Array<{ @@ -356,6 +358,7 @@ export class ViewerService { rows.push({ id: "system:recent-movies", title: "Recent toegevoegd", + name: "Recent toegevoegd", kind: "MOVIE", slug: "recent-movies", items: recentMovies, @@ -365,6 +368,7 @@ export class ViewerService { rows.push({ id: "system:recent-series", title: "Recent toegevoegde series", + name: "Recent toegevoegde series", kind: "SERIES", slug: "recent-series", items: recentSeries, @@ -374,6 +378,7 @@ export class ViewerService { rows.push({ id: "system:4k-movies", title: "4K films", + name: "4K films", kind: "MOVIE", slug: "4k-movies", items: fourK, @@ -383,9 +388,11 @@ export class ViewerService { for (const shelf of ordered) { const items = await this.shelfItems(shelf.id, shelf.kind, 48); if (items.length) { + const homepageLabel = shelf.homepageName?.trim(); rows.push({ id: shelf.id, - title: shelf.name, + title: homepageLabel || shelf.name, + name: shelf.name, kind: shelf.kind, slug: shelf.slug, items,