小优化

This commit is contained in:
beyond committed 2026-08-31 15:54:09 +08:00
1 parent 7f24697ece
commit 6cb94b7793
4 files changed
+123 -14

No files matched your search

@@ -149,6 +149,7 @@ object TmdbRepository {
private const val IMAGE_BASE = "https://image.tmdb.org/t/p/w342"
private val gson = Gson()
private val extrasCache = ConcurrentHashMap<Int, TmdbSeriesExtras>()
private val movieExtrasCache = ConcurrentHashMap<Int, TmdbSeriesExtras>()
private val client = OkHttpClient.Builder()
.connectTimeout(6, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
@@ -231,6 +232,97 @@ object TmdbRepository {
}
}
// ── Movie support ──────────────────────────────────────────────────────
suspend fun getMovieExtras(
tmdbId: String?,
title: String,
year: Int?,
): TmdbSeriesExtras = withContext(Dispatchers.IO) {
if (!isConfigured) return@withContext TmdbSeriesExtras()
Log.d("TmdbRepository", "getMovieExtras: tmdbId=$tmdbId, title=$title, year=$year")
val resolvedId = tmdbId?.toIntOrNull()
?: searchMovieId(title, year)
?: searchMovieId(title, null)
?: return@withContext TmdbSeriesExtras().also {
Log.w("TmdbRepository", "getMovieExtras: could not resolve TMDB ID for '$title'")
}
Log.d("TmdbRepository", "getMovieExtras: resolvedId=$resolvedId")
movieExtrasCache[resolvedId]?.let { cached ->
Log.d("TmdbRepository", "getMovieExtras: returning cached result")
return@withContext cached
}
coroutineScope {
val cast = async { getMovieCredits(resolvedId) }
val recommendations = async { getMovieRecommendations(resolvedId) }
val result = TmdbSeriesExtras(cast.await(), recommendations.await())
if (result.cast.isNotEmpty() || result.recommendations.isNotEmpty()) {
movieExtrasCache[resolvedId] = result
}
Log.d("TmdbRepository", "Loaded ${result.cast.size} movie cast and ${result.recommendations.size} movie recommendations")
result
}
}
private fun searchMovieId(title: String, year: Int?): Int? {
if (title.isBlank()) return null
val result = performMovieSearch(title, year?.toString())
Log.d("TmdbRepository", "searchMovieId: '$title' year=$year -> $result")
return result
}
private fun performMovieSearch(title: String, year: String?): Int? {
val url = Uri.parse("$API_BASE/search/movie").buildUpon()
.appendQueryParameter("query", title)
.appendQueryParameter("language", language())
.appendQueryParameter("include_adult", "false")
.apply { year?.let { appendQueryParameter("year", it) } }
.build()
return get(url.toString(), TmdbSearchResponse::class.java)?.results?.firstOrNull()?.id
}
private fun getMovieCredits(movieId: Int): List<TmdbCastMember> {
val url = Uri.parse("$API_BASE/movie/$movieId/credits").buildUpon()
.appendQueryParameter("language", language())
.build()
val result = get(url.toString(), TmdbCreditsResponse::class.java)?.cast.orEmpty()
.filter { it.name.isNotBlank() }
.take(24)
.map { item ->
TmdbCastMember(
id = item.id,
name = item.name,
originalName = item.originalName,
character = item.roles.firstOrNull()?.character.orEmpty(),
profileUrl = item.profilePath?.let { IMAGE_BASE + it },
)
}
Log.d("TmdbRepository", "getMovieCredits: movieId=$movieId -> ${result.size} cast")
return result
}
private fun getMovieRecommendations(movieId: Int): List<TmdbRecommendation> {
val url = Uri.parse("$API_BASE/movie/$movieId/recommendations").buildUpon()
.appendQueryParameter("language", language())
.appendQueryParameter("page", "1")
.build()
val result = get(url.toString(), TmdbRecommendationsResponse::class.java)?.results.orEmpty()
.filter { it.name.isNotBlank() }
.take(20)
.map { item ->
TmdbRecommendation(
id = item.id,
name = item.name,
originalName = item.originalName,
posterUrl = item.posterPath?.let { IMAGE_BASE + it },
firstAirYear = item.firstAirDate.take(4),
rating = item.voteAverage,
)
}
Log.d("TmdbRepository", "getMovieRecommendations: movieId=$movieId -> ${result.size} recommendations")
return result
}
/** Get calendar episodes for all watchlist items. */
suspend fun getCalendarEpisodes(watchlist: List<WatchlistEntity>): List<TmdbEpisodeCalendar> = withContext(Dispatchers.IO) {
if (!isConfigured || watchlist.isEmpty()) return@withContext emptyList()
@@ -87,6 +87,16 @@ fun AppNavHost(
}
}
val openHome: () -> Unit = {
if (navController.currentDestination?.route != AppRoute.Home.pattern) {
navController.navigate(AppRoute.Home.pattern) {
popUpTo(AppRoute.Home.pattern) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
}
NavHost(
navController = navController,
startDestination = AppRoute.Home.pattern,
@@ -130,7 +140,7 @@ fun AppNavHost(
LibraryScreen(
servers = servers,
selectedServer = selectedServer,
onSelectServer = onSelectServer,
onSelectServer = { id -> onSelectServer(id); openHome() },
onDeleteServer = onDeleteServer,
onEditServer = { server -> navigateOnce(AppRoute.ServerEditor.create(server.id)) },
isGrid = isGrid,
@@ -142,7 +142,7 @@ fun MediaDetailScreen(
var resources by remember(item.id) { mutableStateOf<List<DetailResource>>(emptyList()) }
var tmdbExtras by remember(item.id) { mutableStateOf(TmdbSeriesExtras()) }
var loadingTmdb by remember(item.id) {
mutableStateOf(item.type == "Series" && TmdbRepository.isConfigured)
mutableStateOf(TmdbRepository.isConfigured)
}
var isFavorited by remember(item.id, server?.id) { mutableStateOf(false) }
var isWatching by remember(item.id, server?.id) { mutableStateOf(false) }
@@ -331,14 +331,19 @@ fun MediaDetailScreen(
.firstOrNull { it.key.equals("Tmdb", ignoreCase = true) }
?.value
LaunchedEffect(detail.id, detail.name, detail.productionYear, tmdbId, loadingFullDetail) {
if (detail.type != "Series" || !TmdbRepository.isConfigured) return@LaunchedEffect
if (loadingFullDetail) {
if (!TmdbRepository.isConfigured) return@LaunchedEffect
if (detail.type == "Series" && loadingFullDetail) {
loadingTmdb = true
return@LaunchedEffect
}
loadingTmdb = true
android.util.Log.d("MediaDetail", "TMDB loading: type=${detail.type}, tmdbId=$tmdbId, name=${detail.name}, year=${detail.productionYear}")
loadingTmdb = detail.type == "Series"
tmdbExtras = try {
TmdbRepository.getSeriesExtras(tmdbId, detail.name, detail.productionYear)
if (detail.type == "Series") {
TmdbRepository.getSeriesExtras(tmdbId, detail.name, detail.productionYear)
} else {
TmdbRepository.getMovieExtras(tmdbId, detail.name, detail.productionYear)
}
} catch (error: CancellationException) {
throw error
} catch (_: Exception) {
@@ -346,6 +351,7 @@ fun MediaDetailScreen(
} finally {
loadingTmdb = false
}
android.util.Log.d("MediaDetail", "TMDB result: cast=${tmdbExtras.cast.size}, recs=${tmdbExtras.recommendations.size}")
}
val castMembers = remember(detail.people, tmdbExtras.cast, server?.fullUrl) {
@@ -454,7 +460,7 @@ fun MediaDetailScreen(
// Favorite + Watchlist buttons (bottom, smaller)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically,
) {
DetailSmallButton(
@@ -471,7 +477,6 @@ fun MediaDetailScreen(
},
)
if (detail.type == "Series") {
Spacer(Modifier.width(12.dp))
DetailSmallButton(
icon = LumaIcon.Calendar,
label = if (isWatching) stringResource(Res.string.detail_unwatch) else stringResource(Res.string.detail_watch),
@@ -635,12 +640,12 @@ fun MediaDetailScreen(
ResourceSection(resources = resources, loading = loadingResources)
}
if (detail.type == "Series") {
if (castMembers.isNotEmpty() || loadingTmdb) {
Spacer(Modifier.height(22.dp))
CastSection(castMembers, loading = loadingTmdb && castMembers.isEmpty())
}
if (detail.type == "Series") {
if (tmdbExtras.recommendations.isNotEmpty() || loadingTmdb) {
Spacer(Modifier.height(22.dp))
RecommendationSection(tmdbExtras.recommendations, loading = loadingTmdb && tmdbExtras.recommendations.isEmpty())
}
@@ -917,10 +922,11 @@ private fun DetailSmallButton(
label: String,
tint: Color,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
LiquidGlassButton(
onClick = onClick,
modifier = Modifier.height(36.dp).widthIn(min = 80.dp),
modifier = modifier.height(36.dp).widthIn(min = 100.dp),
cornerRadius = 999.dp,
fill = GlassTokens.glassFillStrong,
) {
@@ -462,8 +462,7 @@ private fun CalendarTab(onEpisodeClick: (String, String) -> Unit = { _, _ -> })
Box(
Modifier
.weight(1f)
.fillMaxWidth()
.padding(bottom = bottomBarContentPadding(86.dp).calculateBottomPadding()),
.fillMaxWidth(),
) {
if (loading) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -488,7 +487,9 @@ private fun CalendarTab(onEpisodeClick: (String, String) -> Unit = { _, _ -> })
val groupedByDate = filteredEpisodes.groupBy { it.airDate }.toSortedMap()
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 16.dp),
contentPadding = PaddingValues(
bottom = 16.dp + bottomBarContentPadding(86.dp).calculateBottomPadding(),
),
) {
groupedByDate.forEach { (dateStr, episodes) ->
val date = try { java.time.LocalDate.parse(dateStr) } catch (_: Exception) { null }