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() {
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)
+
+ Homepage naam (TV)
+ {
+ const next = e.target.value.trim();
+ const prev = (s.homepageName ?? "").trim();
+ if (next !== prev) saveShelfHomepageName(s.id, next);
+ }}
+ />
+
{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,