Show up to 5 Continue Watching items on home with a See all grid.
Cap the home row at five titles and open a continue-watching catalog when more exist. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
4b3c594609
commit
38a779c5f8
6 changed files with 68 additions and 12 deletions
|
|
@ -12,8 +12,8 @@ android {
|
|||
applicationId = "nl.vonas.mediacluster.tv"
|
||||
minSdk = 24
|
||||
targetSdk = 35
|
||||
versionCode = 11
|
||||
versionName = "0.7.4"
|
||||
versionCode = 12
|
||||
versionName = "0.7.5"
|
||||
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
||||
ndk {
|
||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
||||
|
|
|
|||
|
|
@ -326,6 +326,7 @@ data class MediaItem(
|
|||
val posterUrl: String? = null,
|
||||
val backdropUrl: String? = null,
|
||||
val mediaFileId: String? = null,
|
||||
val seriesId: String? = null,
|
||||
val positionMs: Long = 0,
|
||||
val durationMs: Long = 0,
|
||||
val progressId: String? = null,
|
||||
|
|
@ -388,10 +389,11 @@ private fun JSONArray?.toItems(): List<MediaItem> {
|
|||
posterUrl = o.optString("posterUrl").ifBlank { null },
|
||||
backdropUrl = o.optString("backdropUrl").ifBlank { null },
|
||||
mediaFileId = if (o.isNull("mediaFileId")) null else o.optString("mediaFileId").ifBlank { null },
|
||||
seriesId = if (o.isNull("seriesId")) null else o.optString("seriesId").ifBlank { null },
|
||||
positionMs = o.optLong("positionMs"),
|
||||
durationMs = o.optLong("durationMs"),
|
||||
progressId = o.optString("progressId").ifBlank { null },
|
||||
badge = o.optString("badge").ifBlank { null },
|
||||
progressId = o.optString("progressId").takeIf { it.isNotBlank() && it != "null" },
|
||||
badge = o.optString("badge").takeIf { it.isNotBlank() && it != "null" },
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,14 @@ fun BrowseScreen(
|
|||
onBack: (() -> Unit)? = null,
|
||||
onOpenMovie: (String) -> Unit,
|
||||
onOpenSeries: (String) -> Unit,
|
||||
onPlayContinue: ((
|
||||
mediaFileId: String,
|
||||
movieId: String?,
|
||||
episodeId: String?,
|
||||
seriesId: String?,
|
||||
type: String,
|
||||
startMs: Long,
|
||||
) -> Unit)? = null,
|
||||
) {
|
||||
var title by remember { mutableStateOf(titleHint ?: "Bibliotheek") }
|
||||
var items by remember { mutableStateOf<List<MediaItem>>(emptyList()) }
|
||||
|
|
@ -135,13 +143,31 @@ fun BrowseScreen(
|
|||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
items(items, key = { "${it.type}:${it.id}" }) { item ->
|
||||
items(items, key = { "${it.type}:${it.id}:${it.progressId}" }) { item ->
|
||||
PosterCard(
|
||||
item = item,
|
||||
width = 140.dp,
|
||||
height = 210.dp,
|
||||
onClick = {
|
||||
if (item.type == "movie") onOpenMovie(item.id) else onOpenSeries(item.id)
|
||||
val canResume = onPlayContinue != null &&
|
||||
!item.mediaFileId.isNullOrBlank() &&
|
||||
(item.positionMs > 0 || catalogId == "continue-watching")
|
||||
if (canResume) {
|
||||
onPlayContinue!!(
|
||||
item.mediaFileId!!,
|
||||
item.id.takeIf { item.type == "movie" },
|
||||
item.id.takeIf { item.type == "episode" },
|
||||
item.seriesId,
|
||||
if (item.type == "episode") "EPISODE" else "MOVIE",
|
||||
item.positionMs,
|
||||
)
|
||||
} else if (item.type == "movie") {
|
||||
onOpenMovie(item.id)
|
||||
} else if (item.type == "episode" && !item.seriesId.isNullOrBlank()) {
|
||||
onOpenSeries(item.seriesId!!)
|
||||
} else {
|
||||
onOpenSeries(item.id)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,6 +131,16 @@ fun McNav() {
|
|||
onBack = { nav.popBackStack() },
|
||||
onOpenMovie = { nav.navigate("movie/$it") },
|
||||
onOpenSeries = { nav.navigate("series/$it") },
|
||||
onPlayContinue = if (catalogId == "continue-watching") {
|
||||
{ mediaFileId, movieId, episodeId, seriesId, type, startMs ->
|
||||
val m = movieId?.ifBlank { null } ?: "-"
|
||||
val e = episodeId?.ifBlank { null } ?: "-"
|
||||
val s = seriesId?.ifBlank { null } ?: "-"
|
||||
nav.navigate("player/$mediaFileId/$type/$m/$e/$s/$startMs")
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
composable("movie/{id}", arguments = listOf(navArgument("id") { type = NavType.StringType })) { entry ->
|
||||
|
|
@ -479,22 +489,28 @@ private fun HomeScreen(
|
|||
}
|
||||
val continueItems = home?.continueWatching.orEmpty()
|
||||
if (continueItems.isNotEmpty()) {
|
||||
val preview = continueItems.take(5)
|
||||
item {
|
||||
ShelfBlock(
|
||||
title = "Verder kijken",
|
||||
items = continueItems,
|
||||
onSeeAll = null,
|
||||
items = preview,
|
||||
onSeeAll = if (continueItems.size > 5) {
|
||||
{ onSeeAll("continue-watching", "Verder kijken") }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onClick = { item ->
|
||||
when {
|
||||
!item.mediaFileId.isNullOrBlank() -> onPlay(
|
||||
item.mediaFileId!!,
|
||||
item.id.takeIf { item.type == "movie" },
|
||||
item.id.takeIf { item.type == "episode" },
|
||||
null,
|
||||
item.seriesId,
|
||||
if (item.type == "episode") "EPISODE" else "MOVIE",
|
||||
item.positionMs,
|
||||
)
|
||||
item.type == "movie" -> onOpenMovie(item.id)
|
||||
item.type == "episode" && !item.seriesId.isNullOrBlank() -> onOpenSeries(item.seriesId!!)
|
||||
else -> onOpenSeries(item.id)
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -185,12 +185,12 @@ export function registerViewerRoutes(
|
|||
});
|
||||
|
||||
app.get("/api/v1/client/catalog/:id", async (request) => {
|
||||
await viewers.authFromBearer(request.headers.authorization);
|
||||
const auth = await viewers.authFromBearer(request.headers.authorization);
|
||||
const { id } = request.params as { id: string };
|
||||
const q = request.query as { skip?: string; take?: string };
|
||||
const skip = Math.max(0, parseInt(q.skip || "0", 10) || 0);
|
||||
const take = Math.min(parseInt(q.take || "60", 10) || 60, 100);
|
||||
return viewers.browseCatalog(id, skip, take);
|
||||
return viewers.browseCatalog(id, skip, take, auth.viewerId);
|
||||
});
|
||||
|
||||
app.get("/api/v1/client/shelves/prefs", async (request) => {
|
||||
|
|
|
|||
|
|
@ -875,9 +875,21 @@ export class ViewerService {
|
|||
.slice(0, take);
|
||||
}
|
||||
|
||||
async browseCatalog(catalogId: string, skip: number, take: number) {
|
||||
async browseCatalog(catalogId: string, skip: number, take: number, viewerId?: string) {
|
||||
const id = catalogId.trim().toLowerCase();
|
||||
|
||||
if (id === "continue-watching" || id === "continue" || id === "verder-kijken") {
|
||||
if (!viewerId) throw new AppError("UNAUTHORIZED", "Niet ingelogd", 401);
|
||||
const all = await this.continueWatching(viewerId);
|
||||
return {
|
||||
id: "continue-watching",
|
||||
title: "Verder kijken",
|
||||
kind: "MIXED" as const,
|
||||
total: all.length,
|
||||
items: all.slice(skip, skip + take),
|
||||
};
|
||||
}
|
||||
|
||||
if (id === "movies" || id === "films") {
|
||||
const total = await prisma.movie.count({
|
||||
where: { mediaFiles: { some: { available: true } } },
|
||||
|
|
|
|||
Loading…
Reference in a new issue