小优化
This commit is contained in:
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 }
|
||||
|
||||
Reference in new issue
Block a user