mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-08 04:32:43 -05:00
create basic season details screen
This commit is contained in:
@@ -56,9 +56,6 @@ interface TvApi {
|
||||
@Query("session_id") sessionId: String
|
||||
): Response<StatusResponse>
|
||||
|
||||
@GET("tv/{id}/season/{season}")
|
||||
suspend fun getSeason(@Path("id") seriesId: Int, @Path("season") seasonNumber: Int): Response<Season>
|
||||
|
||||
@GET("tv/{id}/watch/providers")
|
||||
suspend fun getWatchProviders(@Path("id") seriesId: Int): Response<WatchProviderResponse>
|
||||
|
||||
@@ -73,4 +70,7 @@ interface TvApi {
|
||||
|
||||
@GET("trending/tv/{time_window}")
|
||||
suspend fun trending(@Path("time_window") timeWindow: String, @Query("page") page: Int): Response<SearchResult<SearchResultTv>>
|
||||
|
||||
@GET("tv/{id}/season/{season}")
|
||||
suspend fun getSeason(@Path("id") seriesId: Int, @Path("season") seasonNumber: Int): Response<Season>
|
||||
}
|
||||
@@ -10,4 +10,6 @@ fun Any.coroutineTask(runnable: suspend () -> Unit) {
|
||||
|
||||
fun <T> anyOf(vararg items: T, predicate: (T) -> Boolean): Boolean = items.any(predicate)
|
||||
|
||||
fun <T: Any> T.isIn(vararg items: T): Boolean = items.any { it == this }
|
||||
fun <T: Any> T.isIn(vararg items: T): Boolean = items.any { it == this }
|
||||
|
||||
fun <T> pairOf(a: T, b: T) = Pair(a, b)
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.pow
|
||||
|
||||
fun Float.dpToPx(context: Context): Float {
|
||||
return this * context.resources.displayMetrics.density
|
||||
@@ -12,3 +13,19 @@ fun Float.dpToPx(context: Context): Float {
|
||||
|
||||
@Composable
|
||||
fun Int.toDp(): Dp = (this / LocalContext.current.resources.displayMetrics.density).toInt().dp
|
||||
|
||||
fun Int.combineWith(other: Int): Int {
|
||||
val max = maxOf(this, other)
|
||||
val min = minOf(this, other)
|
||||
return 2f.pow(min).toInt() * ((2 * max) + 1)
|
||||
}
|
||||
|
||||
fun Int.toCompositeParts(): Pair<Int, Int> {
|
||||
var a = 0
|
||||
var result = this
|
||||
do {
|
||||
result /= 2
|
||||
a++
|
||||
} while (result % 2 == 0)
|
||||
return pairOf(a, result/2)
|
||||
}
|
||||
|
||||
@@ -5,9 +5,12 @@ import android.net.Uri
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -16,6 +19,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -24,12 +28,14 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import coil.request.CachePolicy
|
||||
@@ -40,10 +46,16 @@ import com.google.accompanist.pager.PagerState
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import com.owenlejeune.tvtime.R
|
||||
import com.owenlejeune.tvtime.api.LoadingState
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ExternalIds
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Episode
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MovieCastMember
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MovieCrewMember
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Person
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCastMember
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCrewMember
|
||||
import com.owenlejeune.tvtime.extensions.shimmerBackground
|
||||
import com.owenlejeune.tvtime.extensions.toDp
|
||||
import com.owenlejeune.tvtime.ui.navigation.AppNavItem
|
||||
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
|
||||
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
||||
@@ -58,6 +70,7 @@ fun DetailHeader(
|
||||
imageCollection: ImageCollection? = null,
|
||||
backdropUrl: String? = null,
|
||||
posterUrl: String? = null,
|
||||
expandedPosterAsBackdrop: Boolean = false,
|
||||
backdropContentDescription: String? = null,
|
||||
posterContentDescription: String? = null,
|
||||
rating: Float? = null,
|
||||
@@ -71,20 +84,48 @@ fun DetailHeader(
|
||||
.wrapContentHeight()
|
||||
)
|
||||
) {
|
||||
if (imageCollection != null) {
|
||||
BackdropGallery(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
showGalleryOverlay?.value = true
|
||||
},
|
||||
imageCollection = imageCollection,
|
||||
state = pagerState
|
||||
)
|
||||
if (expandedPosterAsBackdrop) {
|
||||
Box {
|
||||
val url = TmdbUtils.getFullPosterPath(posterUrl)
|
||||
val model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(url)
|
||||
.diskCacheKey(url ?: "")
|
||||
.networkCachePolicy(CachePolicy.ENABLED)
|
||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||
.build()
|
||||
AsyncImage(
|
||||
model = model,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier
|
||||
.blur(radius = 10.dp)
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1.778f)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.background(Color.Black.copy(alpha = 0.6f))
|
||||
.blur(radius = 5.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Backdrop(
|
||||
imageUrl = backdropUrl,
|
||||
contentDescription = backdropContentDescription
|
||||
)
|
||||
if (imageCollection != null) {
|
||||
BackdropGallery(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
showGalleryOverlay?.value = true
|
||||
},
|
||||
imageCollection = imageCollection,
|
||||
state = pagerState
|
||||
)
|
||||
} else {
|
||||
Backdrop(
|
||||
imageUrl = backdropUrl,
|
||||
contentDescription = backdropContentDescription
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
@@ -361,4 +402,119 @@ fun AdditionalDetailItem(
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EpisodeItem(
|
||||
episode: Episode,
|
||||
elevation: Dp = 10.dp,
|
||||
maxDescriptionLines: Int = 2
|
||||
) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = elevation),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
|
||||
}
|
||||
) {
|
||||
Box {
|
||||
episode.stillPath?.let {
|
||||
// Cloudy(
|
||||
// modifier = Modifier.background(Color.Black.copy(alpha = 0.4f))
|
||||
// ) {
|
||||
val url = TmdbUtils.getFullEpisodeStillPath(it)
|
||||
val model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(url)
|
||||
.diskCacheKey(url ?: "")
|
||||
.networkCachePolicy(CachePolicy.ENABLED)
|
||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||
.build()
|
||||
AsyncImage(
|
||||
model = model,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier
|
||||
.blur(radius = 10.dp)
|
||||
// .fillMaxWidth()
|
||||
// .wrapContentHeight()
|
||||
.matchParentSize()
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
// .fillMaxSize()
|
||||
.matchParentSize()
|
||||
.background(Color.Black.copy(alpha = 0.4f))
|
||||
.blur(radius = 5.dp)
|
||||
)
|
||||
// }
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.padding(12.dp)
|
||||
) {
|
||||
val textColor = episode.stillPath?.let { Color.White } ?: if (isSystemInDarkTheme()) Color.White else Color.Black
|
||||
Text(
|
||||
text = "S${episode.seasonNumber}E${episode.episodeNumber} • ${episode.name}",
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
TmdbUtils.convertEpisodeDate(episode.airDate)?.let {
|
||||
Text(
|
||||
text = it,
|
||||
fontStyle = FontStyle.Italic,
|
||||
fontSize = 12.sp,
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = episode.overview,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = textColor,
|
||||
maxLines = maxDescriptionLines
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CastCrewCard(
|
||||
appNavController: NavController,
|
||||
person: Person
|
||||
) {
|
||||
TwoLineImageTextCard(
|
||||
title = person.name,
|
||||
modifier = Modifier
|
||||
.width(124.dp)
|
||||
.wrapContentHeight(),
|
||||
subtitle = when (person) {
|
||||
is MovieCastMember -> person.character
|
||||
is MovieCrewMember -> person.job
|
||||
is TvCastMember -> {
|
||||
val roles = person.roles.joinToString(separator = "/") { it.role }
|
||||
val epsCount = person.totalEpisodeCount
|
||||
"$roles ($epsCount Eps.)"
|
||||
}
|
||||
is TvCrewMember -> {
|
||||
val roles = person.jobs.joinToString(separator = "/") { it.role }
|
||||
val epsCount = person.totalEpisodeCount
|
||||
"$roles ($epsCount Eps.)"
|
||||
}
|
||||
else -> null
|
||||
},
|
||||
imageUrl = TmdbUtils.getFullPersonImagePath(person),
|
||||
titleTextColor = MaterialTheme.colorScheme.onPrimary,
|
||||
subtitleTextColor = MaterialTheme.colorScheme.onSecondary,
|
||||
onItemClicked = {
|
||||
appNavController.navigate(
|
||||
AppNavItem.DetailView.withArgs(MediaViewType.PERSON, person.id)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.owenlejeune.tvtime.ui.navigation
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavHostController
|
||||
@@ -28,6 +30,7 @@ import com.owenlejeune.tvtime.ui.screens.ListDetailScreen
|
||||
import com.owenlejeune.tvtime.ui.screens.MediaDetailScreen
|
||||
import com.owenlejeune.tvtime.ui.screens.PersonDetailScreen
|
||||
import com.owenlejeune.tvtime.ui.screens.SearchScreen
|
||||
import com.owenlejeune.tvtime.ui.screens.SeasonDetailsScreen
|
||||
import com.owenlejeune.tvtime.ui.screens.SeasonListScreen
|
||||
import com.owenlejeune.tvtime.ui.screens.SettingsScreen
|
||||
import com.owenlejeune.tvtime.ui.screens.WebLinkScreen
|
||||
@@ -54,14 +57,40 @@ fun AppNavigationHost(
|
||||
exitTransition = { fadeOut(tween(500)) },
|
||||
popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End, tween(500)) }
|
||||
) {
|
||||
composable(route = AppNavItem.MainView.route) {
|
||||
applicationViewModel.currentRoute.value = AppNavItem.MainView.route
|
||||
HomeScreen(
|
||||
// About View
|
||||
composable(route = AppNavItem.AboutView.route) {
|
||||
applicationViewModel.currentRoute.value = AppNavItem.AboutView.route
|
||||
AboutScreen(appNavController = appNavController)
|
||||
}
|
||||
// Account View
|
||||
composable(
|
||||
route = AppNavItem.AccountView.route,
|
||||
deepLinks = listOf(
|
||||
navDeepLink { uriPattern = "app://tvtime.auth.{${NavConstants.ACCOUNT_KEY}}" }
|
||||
)
|
||||
) {
|
||||
val deepLink = it.arguments?.getString(NavConstants.ACCOUNT_KEY)
|
||||
applicationViewModel.currentRoute.value = AppNavItem.AccountView.route
|
||||
AccountScreen(
|
||||
appNavController = appNavController,
|
||||
mainNavStartRoute = mainNavStartRoute,
|
||||
windowSize = windowSize
|
||||
doSignInPartTwo = deepLink == NavConstants.AUTH_REDIRECT_PAGE
|
||||
)
|
||||
}
|
||||
// Cast Crew List View
|
||||
composable(
|
||||
route = AppNavItem.CastCrewListView.route.plus("/{${NavConstants.TYPE_KEY}}/{${NavConstants.ID_KEY}}"),
|
||||
arguments = listOf(
|
||||
navArgument(NavConstants.TYPE_KEY) { type = NavType.EnumType(MediaViewType::class.java) },
|
||||
navArgument(NavConstants.ID_KEY) { type = NavType.IntType }
|
||||
)
|
||||
) { navBackStackEntry ->
|
||||
val type = navBackStackEntry.arguments?.safeGetSerializable(NavConstants.TYPE_KEY, MediaViewType::class.java)!!
|
||||
val id = navBackStackEntry.arguments?.getInt(NavConstants.ID_KEY)!!
|
||||
|
||||
applicationViewModel.currentRoute.value = AppNavItem.CastCrewListView.withArgs(type, id)
|
||||
CastCrewListScreen(appNavController = appNavController, type = type, id = id)
|
||||
}
|
||||
// Detail View
|
||||
composable(
|
||||
route = AppNavItem.DetailView.route.plus("/{${NavConstants.TYPE_KEY}}/{${NavConstants.ID_KEY}}"),
|
||||
arguments = listOf(
|
||||
@@ -86,7 +115,7 @@ fun AppNavigationHost(
|
||||
itemId = id
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
MediaViewType.MOVIE, MediaViewType.TV -> {
|
||||
MediaDetailScreen(
|
||||
appNavController = appNavController,
|
||||
itemId = id,
|
||||
@@ -94,12 +123,75 @@ fun AppNavigationHost(
|
||||
windowSize = windowSize
|
||||
)
|
||||
}
|
||||
MediaViewType.SEASON -> {
|
||||
SeasonDetailsScreen(
|
||||
appNavController = appNavController,
|
||||
codedId = id
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
appNavController.popBackStack()
|
||||
Toast.makeText(LocalContext.current, stringResource(R.string.unexpected_error), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
composable(AppNavItem.SettingsView.route) {
|
||||
applicationViewModel.currentRoute.value = AppNavItem.SettingsView.route
|
||||
SettingsScreen(appNavController = appNavController)
|
||||
// Gallery View
|
||||
composable(
|
||||
route = AppNavItem.GalleryView.route.plus("/{${NavConstants.TYPE_KEY}}/{${NavConstants.ID_KEY}}"),
|
||||
arguments = listOf(
|
||||
navArgument(NavConstants.TYPE_KEY) { type = NavType.EnumType(MediaViewType::class.java) },
|
||||
navArgument(NavConstants.ID_KEY) { type = NavType.IntType }
|
||||
)
|
||||
) { navBackStackEntry ->
|
||||
val type = navBackStackEntry.arguments?.safeGetSerializable(NavConstants.TYPE_KEY, MediaViewType::class.java)!!
|
||||
val id = navBackStackEntry.arguments?.getInt(NavConstants.ID_KEY)!!
|
||||
|
||||
applicationViewModel.currentRoute.value = AppNavItem.GalleryView.withArgs(type, id)
|
||||
GalleryView(id = id, type = type, appNavController = appNavController)
|
||||
}
|
||||
// Keywords View
|
||||
composable(
|
||||
route = AppNavItem.KeywordsView.route.plus("/{${NavConstants.KEYWORD_TYPE_KEY}}?keyword={${NavConstants.KEYWORD_NAME_KEY}}&keywordId={${NavConstants.KEYWORD_ID_KEY}}"),
|
||||
arguments = listOf(
|
||||
navArgument(NavConstants.KEYWORD_TYPE_KEY) { type = NavType.EnumType(MediaViewType::class.java) },
|
||||
navArgument(NavConstants.KEYWORD_NAME_KEY) { type = NavType.StringType },
|
||||
navArgument(NavConstants.KEYWORD_ID_KEY) { type = NavType.IntType }
|
||||
)
|
||||
) { navBackStackEntry ->
|
||||
val type = navBackStackEntry.arguments?.safeGetSerializable(NavConstants.KEYWORD_TYPE_KEY, MediaViewType::class.java)!!
|
||||
val keywords = navBackStackEntry.arguments?.getString(NavConstants.KEYWORD_NAME_KEY) ?: ""
|
||||
val id = navBackStackEntry.arguments?.getInt(NavConstants.KEYWORD_ID_KEY)!!
|
||||
|
||||
applicationViewModel.currentRoute.value = AppNavItem.KeywordsView.withArgs(type, keywords, id)
|
||||
KeywordResultsScreen(
|
||||
type = type,
|
||||
keyword = keywords,
|
||||
id = id,
|
||||
appNavController = appNavController
|
||||
)
|
||||
}
|
||||
// Known For View
|
||||
composable(
|
||||
route = AppNavItem.KnownForView.route.plus("/{${NavConstants.ID_KEY}}"),
|
||||
arguments = listOf(
|
||||
navArgument(NavConstants.ID_KEY) { type = NavType.IntType }
|
||||
)
|
||||
) { navBackStackEntry ->
|
||||
val id = navBackStackEntry.arguments?.getInt(NavConstants.ID_KEY)!!
|
||||
|
||||
applicationViewModel.currentRoute.value = AppNavItem.KnownForView.withArgs(id)
|
||||
KnownForScreen(appNavController = appNavController, id = id)
|
||||
}
|
||||
// Main View
|
||||
composable(route = AppNavItem.MainView.route) {
|
||||
applicationViewModel.currentRoute.value = AppNavItem.MainView.route
|
||||
HomeScreen(
|
||||
appNavController = appNavController,
|
||||
mainNavStartRoute = mainNavStartRoute,
|
||||
windowSize = windowSize
|
||||
)
|
||||
}
|
||||
// Search View
|
||||
composable(
|
||||
route = AppNavItem.SearchView.route.plus("?searchType={${NavConstants.SEARCH_ID_KEY}}&pageTitle={${NavConstants.SEARCH_TITLE_KEY}}"),
|
||||
arguments = listOf(
|
||||
@@ -128,6 +220,24 @@ fun AppNavigationHost(
|
||||
)
|
||||
}
|
||||
}
|
||||
// Season List View
|
||||
composable(
|
||||
route = AppNavItem.SeasonListView.route.plus("/{${NavConstants.ID_KEY}}"),
|
||||
arguments = listOf(
|
||||
navArgument(NavConstants.ID_KEY) { type = NavType.IntType }
|
||||
)
|
||||
) { navBackStackEntry ->
|
||||
val id = navBackStackEntry.arguments?.getInt(NavConstants.ID_KEY)!!
|
||||
|
||||
applicationViewModel.currentRoute.value = AppNavItem.SeasonListView.withArgs(id)
|
||||
SeasonListScreen(id = id, appNavController = appNavController)
|
||||
}
|
||||
// Settings View
|
||||
composable(AppNavItem.SettingsView.route) {
|
||||
applicationViewModel.currentRoute.value = AppNavItem.SettingsView.route
|
||||
SettingsScreen(appNavController = appNavController)
|
||||
}
|
||||
// Web Link View
|
||||
composable(
|
||||
route = AppNavItem.WebLinkView.route.plus("/{${NavConstants.WEB_LINK_KEY}}"),
|
||||
arguments = listOf(
|
||||
@@ -140,91 +250,6 @@ fun AppNavigationHost(
|
||||
WebLinkScreen(url = url, appNavController = appNavController)
|
||||
}
|
||||
}
|
||||
composable(
|
||||
route = AppNavItem.AccountView.route,
|
||||
deepLinks = listOf(
|
||||
navDeepLink { uriPattern = "app://tvtime.auth.{${NavConstants.ACCOUNT_KEY}}" }
|
||||
)
|
||||
) {
|
||||
val deepLink = it.arguments?.getString(NavConstants.ACCOUNT_KEY)
|
||||
applicationViewModel.currentRoute.value = AppNavItem.AccountView.route
|
||||
AccountScreen(
|
||||
appNavController = appNavController,
|
||||
doSignInPartTwo = deepLink == NavConstants.AUTH_REDIRECT_PAGE
|
||||
)
|
||||
}
|
||||
composable(route = AppNavItem.AboutView.route) {
|
||||
applicationViewModel.currentRoute.value = AppNavItem.AboutView.route
|
||||
AboutScreen(appNavController = appNavController)
|
||||
}
|
||||
composable(
|
||||
route = AppNavItem.KeywordsView.route.plus("/{${NavConstants.KEYWORD_TYPE_KEY}}?keyword={${NavConstants.KEYWORD_NAME_KEY}}&keywordId={${NavConstants.KEYWORD_ID_KEY}}"),
|
||||
arguments = listOf(
|
||||
navArgument(NavConstants.KEYWORD_TYPE_KEY) { type = NavType.EnumType(MediaViewType::class.java) },
|
||||
navArgument(NavConstants.KEYWORD_NAME_KEY) { type = NavType.StringType },
|
||||
navArgument(NavConstants.KEYWORD_ID_KEY) { type = NavType.IntType }
|
||||
)
|
||||
) { navBackStackEntry ->
|
||||
val type = navBackStackEntry.arguments?.safeGetSerializable(NavConstants.KEYWORD_TYPE_KEY, MediaViewType::class.java)!!
|
||||
val keywords = navBackStackEntry.arguments?.getString(NavConstants.KEYWORD_NAME_KEY) ?: ""
|
||||
val id = navBackStackEntry.arguments?.getInt(NavConstants.KEYWORD_ID_KEY)!!
|
||||
|
||||
applicationViewModel.currentRoute.value = AppNavItem.KeywordsView.withArgs(type, keywords, id)
|
||||
KeywordResultsScreen(
|
||||
type = type,
|
||||
keyword = keywords,
|
||||
id = id,
|
||||
appNavController = appNavController
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = AppNavItem.KnownForView.route.plus("/{${NavConstants.ID_KEY}}"),
|
||||
arguments = listOf(
|
||||
navArgument(NavConstants.ID_KEY) { type = NavType.IntType }
|
||||
)
|
||||
) { navBackStackEntry ->
|
||||
val id = navBackStackEntry.arguments?.getInt(NavConstants.ID_KEY)!!
|
||||
|
||||
applicationViewModel.currentRoute.value = AppNavItem.KnownForView.withArgs(id)
|
||||
KnownForScreen(appNavController = appNavController, id = id)
|
||||
}
|
||||
composable(
|
||||
route = AppNavItem.GalleryView.route.plus("/{${NavConstants.TYPE_KEY}}/{${NavConstants.ID_KEY}}"),
|
||||
arguments = listOf(
|
||||
navArgument(NavConstants.TYPE_KEY) { type = NavType.EnumType(MediaViewType::class.java) },
|
||||
navArgument(NavConstants.ID_KEY) { type = NavType.IntType }
|
||||
)
|
||||
) { navBackStackEntry ->
|
||||
val type = navBackStackEntry.arguments?.safeGetSerializable(NavConstants.TYPE_KEY, MediaViewType::class.java)!!
|
||||
val id = navBackStackEntry.arguments?.getInt(NavConstants.ID_KEY)!!
|
||||
|
||||
applicationViewModel.currentRoute.value = AppNavItem.GalleryView.withArgs(type, id)
|
||||
GalleryView(id = id, type = type, appNavController = appNavController)
|
||||
}
|
||||
composable(
|
||||
route = AppNavItem.SeasonListView.route.plus("/{${NavConstants.ID_KEY}}"),
|
||||
arguments = listOf(
|
||||
navArgument(NavConstants.ID_KEY) { type = NavType.IntType }
|
||||
)
|
||||
) { navBackStackEntry ->
|
||||
val id = navBackStackEntry.arguments?.getInt(NavConstants.ID_KEY)!!
|
||||
|
||||
applicationViewModel.currentRoute.value = AppNavItem.SeasonListView.withArgs(id)
|
||||
SeasonListScreen(id = id, appNavController = appNavController)
|
||||
}
|
||||
composable(
|
||||
route = AppNavItem.CastCrewListView.route.plus("/{${NavConstants.TYPE_KEY}}/{${NavConstants.ID_KEY}}"),
|
||||
arguments = listOf(
|
||||
navArgument(NavConstants.TYPE_KEY) { type = NavType.EnumType(MediaViewType::class.java) },
|
||||
navArgument(NavConstants.ID_KEY) { type = NavType.IntType }
|
||||
)
|
||||
) { navBackStackEntry ->
|
||||
val type = navBackStackEntry.arguments?.safeGetSerializable(NavConstants.TYPE_KEY, MediaViewType::class.java)!!
|
||||
val id = navBackStackEntry.arguments?.getInt(NavConstants.ID_KEY)!!
|
||||
|
||||
applicationViewModel.currentRoute.value = AppNavItem.CastCrewListView.withArgs(type, id)
|
||||
CastCrewListScreen(appNavController = appNavController, type = type, id = id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,33 +259,33 @@ sealed class AppNavItem(val route: String) {
|
||||
val Items = listOf(MainView, DetailView, SettingsView)
|
||||
}
|
||||
|
||||
object MainView: AppNavItem("main_route")
|
||||
object AboutView: AppNavItem("about_route")
|
||||
object AccountView: AppNavItem("account_route")
|
||||
object CastCrewListView: AppNavItem("cast_crew_list_route") {
|
||||
fun withArgs(type: MediaViewType, id: Int) = route.plus("/$type/$id")
|
||||
}
|
||||
object DetailView: AppNavItem("detail_route") {
|
||||
fun withArgs(type: MediaViewType, id: Int) = route.plus("/${type}/${id}")
|
||||
}
|
||||
object SettingsView: AppNavItem("settings_route")
|
||||
object SearchView: AppNavItem("search_route") {
|
||||
fun withArgs(searchType: MediaViewType, pageTitle: String) = route.plus("?searchType=$searchType&pageTitle=$pageTitle")
|
||||
object GalleryView: AppNavItem("gallery_view_route") {
|
||||
fun withArgs(type: MediaViewType, id: Int) = route.plus("/$type/$id")
|
||||
}
|
||||
object WebLinkView: AppNavItem("web_link_route") {
|
||||
fun withArgs(url: String) = route.plus("/$url")
|
||||
}
|
||||
object AccountView: AppNavItem("account_route")
|
||||
object AboutView: AppNavItem("about_route")
|
||||
object KeywordsView: AppNavItem("keywords_route") {
|
||||
fun withArgs(type: MediaViewType, keyword: String, id: Int) = route.plus("/$type?keyword=$keyword&keywordId=$id")
|
||||
}
|
||||
object KnownForView: AppNavItem("known_for_route") {
|
||||
fun withArgs(id: Int) = route.plus("/$id")
|
||||
}
|
||||
object GalleryView: AppNavItem("gallery_view_route") {
|
||||
fun withArgs(type: MediaViewType, id: Int) = route.plus("/$type/$id")
|
||||
object MainView: AppNavItem("main_route")
|
||||
object SearchView: AppNavItem("search_route") {
|
||||
fun withArgs(searchType: MediaViewType, pageTitle: String) = route.plus("?searchType=$searchType&pageTitle=$pageTitle")
|
||||
}
|
||||
object SeasonListView: AppNavItem("season_list_route") {
|
||||
fun withArgs(id: Int) = route.plus("/$id")
|
||||
}
|
||||
object CastCrewListView: AppNavItem("cast_crew_list_route") {
|
||||
fun withArgs(type: MediaViewType, id: Int) = route.plus("/$type/$id")
|
||||
object SettingsView: AppNavItem("settings_route")
|
||||
object WebLinkView: AppNavItem("web_link_route") {
|
||||
fun withArgs(url: String) = route.plus("/$url")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ import com.owenlejeune.tvtime.ui.components.ActionsView
|
||||
import com.owenlejeune.tvtime.ui.components.AdditionalDetailItem
|
||||
import com.owenlejeune.tvtime.ui.components.AvatarImage
|
||||
import com.owenlejeune.tvtime.ui.components.BackButton
|
||||
import com.owenlejeune.tvtime.ui.components.CastCrewCard
|
||||
import com.owenlejeune.tvtime.ui.components.ChipDefaults
|
||||
import com.owenlejeune.tvtime.ui.components.ChipGroup
|
||||
import com.owenlejeune.tvtime.ui.components.ChipInfo
|
||||
@@ -872,42 +873,6 @@ private fun CastCard(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CastCrewCard(
|
||||
appNavController: NavController,
|
||||
person: Person
|
||||
) {
|
||||
TwoLineImageTextCard(
|
||||
title = person.name,
|
||||
modifier = Modifier
|
||||
.width(124.dp)
|
||||
.wrapContentHeight(),
|
||||
subtitle = when (person) {
|
||||
is MovieCastMember -> person.character
|
||||
is MovieCrewMember -> person.job
|
||||
is TvCastMember -> {
|
||||
val roles = person.roles.joinToString(separator = "/") { it.role }
|
||||
val epsCount = person.totalEpisodeCount
|
||||
"$roles ($epsCount Eps.)"
|
||||
}
|
||||
is TvCrewMember -> {
|
||||
val roles = person.jobs.joinToString(separator = "/") { it.role }
|
||||
val epsCount = person.totalEpisodeCount
|
||||
"$roles ($epsCount Eps.)"
|
||||
}
|
||||
else -> null
|
||||
},
|
||||
imageUrl = TmdbUtils.getFullPersonImagePath(person),
|
||||
titleTextColor = MaterialTheme.colorScheme.onPrimary,
|
||||
subtitleTextColor = MaterialTheme.colorScheme.onSecondary,
|
||||
onItemClicked = {
|
||||
appNavController.navigate(
|
||||
AppNavItem.DetailView.withArgs(MediaViewType.PERSON, person.id)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SeasonCard(
|
||||
itemId: Int,
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.owenlejeune.tvtime.ui.screens
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.owenlejeune.tvtime.R
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Episode
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Season
|
||||
import com.owenlejeune.tvtime.extensions.toCompositeParts
|
||||
import com.owenlejeune.tvtime.ui.components.BackButton
|
||||
import com.owenlejeune.tvtime.ui.components.CastCrewCard
|
||||
import com.owenlejeune.tvtime.ui.components.ContentCard
|
||||
import com.owenlejeune.tvtime.ui.components.DetailHeader
|
||||
import com.owenlejeune.tvtime.ui.components.EpisodeItem
|
||||
import com.owenlejeune.tvtime.ui.components.TVTTopAppBar
|
||||
import com.owenlejeune.tvtime.ui.theme.Typography
|
||||
import com.owenlejeune.tvtime.ui.viewmodel.ApplicationViewModel
|
||||
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
|
||||
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private fun fetchData(
|
||||
mainViewModel: MainViewModel,
|
||||
seasonNumber: Int,
|
||||
seriesId: Int,
|
||||
force: Boolean = false
|
||||
) {
|
||||
val scope = CoroutineScope(Dispatchers.IO)
|
||||
scope.launch { mainViewModel.getSeason(seriesId, seasonNumber, force) }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SeasonDetailsScreen(
|
||||
appNavController: NavController,
|
||||
codedId: Int
|
||||
) {
|
||||
val mainViewModel = viewModel<MainViewModel>()
|
||||
val applicationViewModel = viewModel<ApplicationViewModel>()
|
||||
|
||||
applicationViewModel.statusBarColor.value = MaterialTheme.colorScheme.background
|
||||
applicationViewModel.navigationBarColor.value = MaterialTheme.colorScheme.background
|
||||
|
||||
val (a, b) = codedId.toCompositeParts()
|
||||
val seasonNumber = minOf(a, b)
|
||||
val seriesId = maxOf(a, b)
|
||||
LaunchedEffect(Unit) {
|
||||
fetchData(mainViewModel, seasonNumber, seriesId)
|
||||
}
|
||||
|
||||
val seasonsMap = remember { mainViewModel.tvSeasons }
|
||||
val season = seasonsMap[seriesId]?.firstOrNull { it.seasonNumber == seasonNumber }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TVTTopAppBar(
|
||||
title = { },
|
||||
appNavController = appNavController,
|
||||
navigationIcon = { BackButton(navController = appNavController) }
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Box(modifier = Modifier.padding(innerPadding)) {
|
||||
SeasonContent(appNavController = appNavController, season = season)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
private fun SeasonContent(
|
||||
appNavController: NavController,
|
||||
season: Season?
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.verticalScroll(state = rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
DetailHeader(
|
||||
posterUrl = TmdbUtils.getFullPosterPath(season?.posterPath),
|
||||
elevation = 0.dp,
|
||||
expandedPosterAsBackdrop = true
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = season?.name ?: "",
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = Typography.headlineLarge,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
season?.episodes?.forEach { episode ->
|
||||
SeasonEpisodeItem(appNavController = appNavController, episode = episode)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SeasonEpisodeItem(
|
||||
appNavController: NavController,
|
||||
episode: Episode
|
||||
) {
|
||||
ContentCard {
|
||||
EpisodeItem(episode = episode, elevation = 0.dp, maxDescriptionLines = 5)
|
||||
|
||||
episode.guestStars?.let { guestStars ->
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.guest_stars_label),
|
||||
style = Typography.headlineSmall
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
guestStars.forEach {
|
||||
CastCrewCard(appNavController = appNavController, person = it)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,27 +6,20 @@ import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ExpandMore
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -43,29 +36,21 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Episode
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Season
|
||||
import com.owenlejeune.tvtime.extensions.combineWith
|
||||
import com.owenlejeune.tvtime.ui.components.BackButton
|
||||
import com.owenlejeune.tvtime.ui.components.EpisodeItem
|
||||
import com.owenlejeune.tvtime.ui.components.TVTTopAppBar
|
||||
import com.owenlejeune.tvtime.ui.navigation.AppNavItem
|
||||
import com.owenlejeune.tvtime.ui.viewmodel.ApplicationViewModel
|
||||
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
|
||||
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -80,7 +65,7 @@ fun SeasonListScreen(
|
||||
applicationViewModel.navigationBarColor.value = MaterialTheme.colorScheme.background
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val numSeasons = mainViewModel.detailedTv[id]!!.numberOfSeasons
|
||||
val numSeasons = mainViewModel.detailedTv[id]?.numberOfSeasons ?: 0
|
||||
for (i in 0..numSeasons) {
|
||||
mainViewModel.getSeason(id, i, true)
|
||||
}
|
||||
@@ -113,7 +98,12 @@ fun SeasonListScreen(
|
||||
val seasons = seasonsMap[id] ?: emptySet()
|
||||
|
||||
seasons.sortedBy { it.seasonNumber }.forEachIndexed { index, season ->
|
||||
SeasonSection(season = season, expandedByDefault = index == 0)
|
||||
SeasonSection(
|
||||
appNavController = appNavController,
|
||||
seriesId = id,
|
||||
season = season,
|
||||
expandedByDefault = index == 0
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
@@ -123,7 +113,12 @@ fun SeasonListScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SeasonSection(season: Season, expandedByDefault: Boolean) {
|
||||
private fun SeasonSection(
|
||||
appNavController: NavController,
|
||||
seriesId: Int,
|
||||
season: Season,
|
||||
expandedByDefault: Boolean
|
||||
) {
|
||||
var isExpanded by remember { mutableStateOf(expandedByDefault) }
|
||||
|
||||
Row(
|
||||
@@ -135,7 +130,8 @@ private fun SeasonSection(season: Season, expandedByDefault: Boolean) {
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.clickable {
|
||||
|
||||
val combinedId = seriesId.combineWith(season.seasonNumber)
|
||||
appNavController.navigate(AppNavItem.DetailView.withArgs(type = MediaViewType.SEASON, id = combinedId))
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
@@ -182,78 +178,4 @@ private fun SeasonSection(season: Season, expandedByDefault: Boolean) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EpisodeItem(episode: Episode) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.height(112.dp)
|
||||
) {
|
||||
episode.stillPath?.let {
|
||||
// Cloudy(
|
||||
// modifier = Modifier.background(Color.Black.copy(alpha = 0.4f))
|
||||
// ) {
|
||||
val url = TmdbUtils.getFullEpisodeStillPath(it)
|
||||
val model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(url)
|
||||
.diskCacheKey(url ?: "")
|
||||
.networkCachePolicy(CachePolicy.ENABLED)
|
||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||
.build()
|
||||
AsyncImage(
|
||||
model = model,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier
|
||||
.blur(radius = 10.dp)
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.4f))
|
||||
.blur(radius = 5.dp)
|
||||
)
|
||||
// }
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.padding(12.dp)
|
||||
) {
|
||||
val textColor = episode.stillPath?.let { Color.White } ?: if (isSystemInDarkTheme()) Color.White else Color.Black
|
||||
Text(
|
||||
text = "S${episode.seasonNumber}E${episode.episodeNumber} • ${episode.name}",
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
TmdbUtils.convertEpisodeDate(episode.airDate)?.let {
|
||||
Text(
|
||||
text = it,
|
||||
fontStyle = FontStyle.Italic,
|
||||
fontSize = 12.sp,
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = episode.overview,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ enum class MediaViewType {
|
||||
TV,
|
||||
@SerializedName("person")
|
||||
PERSON,
|
||||
SEASON,
|
||||
EPISODE,
|
||||
MIXED,
|
||||
LIST;
|
||||
|
||||
@@ -260,4 +260,6 @@
|
||||
<string name="place_of_birth">Place of birth</string>
|
||||
<string name="birthday">Birthday</string>
|
||||
<string name="date_of_death">Date of death</string>
|
||||
<string name="unexpected_error">An unexpected error occurred</string>
|
||||
<string name="guest_stars_label">Guest stars</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user