Add shelf homepage names and fix TV poster clipping on home rows.
Admin can set a separate homepage label per library shelf; the client home API uses it for row titles while browse keeps the canonical name. Also fixes LazyRow focus clipping on the TV home screen (0.8.4). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
7c808e9167
commit
159cd48692
11 changed files with 93 additions and 19 deletions
|
|
@ -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() {
|
|||
<h2 className="section-title">Bibliotheek-planken</h2>
|
||||
<p className="muted">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -396,6 +413,7 @@ export default function SettingsPage() {
|
|||
<thead>
|
||||
<tr>
|
||||
<th>Naam</th>
|
||||
<th>Homepage naam</th>
|
||||
<th>Type</th>
|
||||
<th>Pad(en)</th>
|
||||
<th>Bestanden</th>
|
||||
|
|
@ -406,6 +424,24 @@ export default function SettingsPage() {
|
|||
{shelves.map((s) => (
|
||||
<tr key={s.id}>
|
||||
<td>{s.name}</td>
|
||||
<td>
|
||||
<input
|
||||
key={`${s.id}-${s.homepageName ?? ""}`}
|
||||
type="text"
|
||||
defaultValue={s.homepageName ?? ""}
|
||||
placeholder={s.name}
|
||||
aria-label={`Homepage naam voor ${s.name}`}
|
||||
onBlur={(e) => {
|
||||
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" }}
|
||||
/>
|
||||
</td>
|
||||
<td>{s.kind === "MOVIE" ? "Films" : "Series"}</td>
|
||||
<td className="path-cell">
|
||||
{s.paths?.length ? (
|
||||
|
|
@ -439,7 +475,7 @@ export default function SettingsPage() {
|
|||
))}
|
||||
{shelves.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} className="muted" style={{ textAlign: "center" }}>
|
||||
<td colSpan={6} className="muted" style={{ textAlign: "center" }}>
|
||||
Laden…
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -461,6 +497,20 @@ export default function SettingsPage() {
|
|||
<span>{s.mediaFileCount} bestanden</span>
|
||||
<span>{s.scanRootCount} pad(en)</span>
|
||||
</div>
|
||||
<label className="field-stack" style={{ marginTop: "0.75rem" }}>
|
||||
<span className="muted tiny">Homepage naam (TV)</span>
|
||||
<input
|
||||
key={`${s.id}-${s.homepageName ?? ""}-mobile`}
|
||||
type="text"
|
||||
defaultValue={s.homepageName ?? ""}
|
||||
placeholder={s.name}
|
||||
onBlur={(e) => {
|
||||
const next = e.target.value.trim();
|
||||
const prev = (s.homepageName ?? "").trim();
|
||||
if (next !== prev) saveShelfHomepageName(s.id, next);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
{s.paths?.length ? (
|
||||
<div className="path-block">
|
||||
{s.paths.map((p) => (
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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<MediaItem>,
|
||||
)
|
||||
) {
|
||||
/** 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> {
|
|||
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(),
|
||||
|
|
|
|||
|
|
@ -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() &&
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue