mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-19 02:00:54 -05:00
create basic season details screen
This commit is contained in:
@@ -56,9 +56,6 @@ interface TvApi {
|
|||||||
@Query("session_id") sessionId: String
|
@Query("session_id") sessionId: String
|
||||||
): Response<StatusResponse>
|
): 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")
|
@GET("tv/{id}/watch/providers")
|
||||||
suspend fun getWatchProviders(@Path("id") seriesId: Int): Response<WatchProviderResponse>
|
suspend fun getWatchProviders(@Path("id") seriesId: Int): Response<WatchProviderResponse>
|
||||||
|
|
||||||
@@ -73,4 +70,7 @@ interface TvApi {
|
|||||||
|
|
||||||
@GET("trending/tv/{time_window}")
|
@GET("trending/tv/{time_window}")
|
||||||
suspend fun trending(@Path("time_window") timeWindow: String, @Query("page") page: Int): Response<SearchResult<SearchResultTv>>
|
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>
|
||||||
}
|
}
|
||||||
@@ -11,3 +11,5 @@ fun Any.coroutineTask(runnable: suspend () -> Unit) {
|
|||||||
fun <T> anyOf(vararg items: T, predicate: (T) -> Boolean): Boolean = items.any(predicate)
|
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.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
fun Float.dpToPx(context: Context): Float {
|
fun Float.dpToPx(context: Context): Float {
|
||||||
return this * context.resources.displayMetrics.density
|
return this * context.resources.displayMetrics.density
|
||||||
@@ -12,3 +13,19 @@ fun Float.dpToPx(context: Context): Float {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Int.toDp(): Dp = (this / LocalContext.current.resources.displayMetrics.density).toInt().dp
|
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.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
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.Divider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -16,6 +19,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.blur
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
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.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
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.Dp
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
import coil.request.CachePolicy
|
import coil.request.CachePolicy
|
||||||
@@ -40,10 +46,16 @@ import com.google.accompanist.pager.PagerState
|
|||||||
import com.google.accompanist.pager.rememberPagerState
|
import com.google.accompanist.pager.rememberPagerState
|
||||||
import com.owenlejeune.tvtime.R
|
import com.owenlejeune.tvtime.R
|
||||||
import com.owenlejeune.tvtime.api.LoadingState
|
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.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.shimmerBackground
|
||||||
import com.owenlejeune.tvtime.extensions.toDp
|
import com.owenlejeune.tvtime.extensions.toDp
|
||||||
|
import com.owenlejeune.tvtime.ui.navigation.AppNavItem
|
||||||
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
|
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
|
||||||
import com.owenlejeune.tvtime.utils.TmdbUtils
|
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||||
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
||||||
@@ -58,6 +70,7 @@ fun DetailHeader(
|
|||||||
imageCollection: ImageCollection? = null,
|
imageCollection: ImageCollection? = null,
|
||||||
backdropUrl: String? = null,
|
backdropUrl: String? = null,
|
||||||
posterUrl: String? = null,
|
posterUrl: String? = null,
|
||||||
|
expandedPosterAsBackdrop: Boolean = false,
|
||||||
backdropContentDescription: String? = null,
|
backdropContentDescription: String? = null,
|
||||||
posterContentDescription: String? = null,
|
posterContentDescription: String? = null,
|
||||||
rating: Float? = null,
|
rating: Float? = null,
|
||||||
@@ -71,6 +84,33 @@ fun DetailHeader(
|
|||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
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 {
|
||||||
if (imageCollection != null) {
|
if (imageCollection != null) {
|
||||||
BackdropGallery(
|
BackdropGallery(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -86,6 +126,7 @@ fun DetailHeader(
|
|||||||
contentDescription = backdropContentDescription
|
contentDescription = backdropContentDescription
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -362,3 +403,118 @@ fun AdditionalDetailItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
package com.owenlejeune.tvtime.ui.navigation
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavHostController
|
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.MediaDetailScreen
|
||||||
import com.owenlejeune.tvtime.ui.screens.PersonDetailScreen
|
import com.owenlejeune.tvtime.ui.screens.PersonDetailScreen
|
||||||
import com.owenlejeune.tvtime.ui.screens.SearchScreen
|
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.SeasonListScreen
|
||||||
import com.owenlejeune.tvtime.ui.screens.SettingsScreen
|
import com.owenlejeune.tvtime.ui.screens.SettingsScreen
|
||||||
import com.owenlejeune.tvtime.ui.screens.WebLinkScreen
|
import com.owenlejeune.tvtime.ui.screens.WebLinkScreen
|
||||||
@@ -54,14 +57,40 @@ fun AppNavigationHost(
|
|||||||
exitTransition = { fadeOut(tween(500)) },
|
exitTransition = { fadeOut(tween(500)) },
|
||||||
popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End, tween(500)) }
|
popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End, tween(500)) }
|
||||||
) {
|
) {
|
||||||
composable(route = AppNavItem.MainView.route) {
|
// About View
|
||||||
applicationViewModel.currentRoute.value = AppNavItem.MainView.route
|
composable(route = AppNavItem.AboutView.route) {
|
||||||
HomeScreen(
|
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,
|
appNavController = appNavController,
|
||||||
mainNavStartRoute = mainNavStartRoute,
|
doSignInPartTwo = deepLink == NavConstants.AUTH_REDIRECT_PAGE
|
||||||
windowSize = windowSize
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// 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(
|
composable(
|
||||||
route = AppNavItem.DetailView.route.plus("/{${NavConstants.TYPE_KEY}}/{${NavConstants.ID_KEY}}"),
|
route = AppNavItem.DetailView.route.plus("/{${NavConstants.TYPE_KEY}}/{${NavConstants.ID_KEY}}"),
|
||||||
arguments = listOf(
|
arguments = listOf(
|
||||||
@@ -86,7 +115,7 @@ fun AppNavigationHost(
|
|||||||
itemId = id
|
itemId = id
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
MediaViewType.MOVIE, MediaViewType.TV -> {
|
||||||
MediaDetailScreen(
|
MediaDetailScreen(
|
||||||
appNavController = appNavController,
|
appNavController = appNavController,
|
||||||
itemId = id,
|
itemId = id,
|
||||||
@@ -94,12 +123,75 @@ fun AppNavigationHost(
|
|||||||
windowSize = windowSize
|
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(
|
composable(
|
||||||
route = AppNavItem.SearchView.route.plus("?searchType={${NavConstants.SEARCH_ID_KEY}}&pageTitle={${NavConstants.SEARCH_TITLE_KEY}}"),
|
route = AppNavItem.SearchView.route.plus("?searchType={${NavConstants.SEARCH_ID_KEY}}&pageTitle={${NavConstants.SEARCH_TITLE_KEY}}"),
|
||||||
arguments = listOf(
|
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(
|
composable(
|
||||||
route = AppNavItem.WebLinkView.route.plus("/{${NavConstants.WEB_LINK_KEY}}"),
|
route = AppNavItem.WebLinkView.route.plus("/{${NavConstants.WEB_LINK_KEY}}"),
|
||||||
arguments = listOf(
|
arguments = listOf(
|
||||||
@@ -140,91 +250,6 @@ fun AppNavigationHost(
|
|||||||
WebLinkScreen(url = url, appNavController = appNavController)
|
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)
|
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") {
|
object DetailView: AppNavItem("detail_route") {
|
||||||
fun withArgs(type: MediaViewType, id: Int) = route.plus("/${type}/${id}")
|
fun withArgs(type: MediaViewType, id: Int) = route.plus("/${type}/${id}")
|
||||||
}
|
}
|
||||||
object SettingsView: AppNavItem("settings_route")
|
object GalleryView: AppNavItem("gallery_view_route") {
|
||||||
object SearchView: AppNavItem("search_route") {
|
fun withArgs(type: MediaViewType, id: Int) = route.plus("/$type/$id")
|
||||||
fun withArgs(searchType: MediaViewType, pageTitle: String) = route.plus("?searchType=$searchType&pageTitle=$pageTitle")
|
|
||||||
}
|
}
|
||||||
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") {
|
object KeywordsView: AppNavItem("keywords_route") {
|
||||||
fun withArgs(type: MediaViewType, keyword: String, id: Int) = route.plus("/$type?keyword=$keyword&keywordId=$id")
|
fun withArgs(type: MediaViewType, keyword: String, id: Int) = route.plus("/$type?keyword=$keyword&keywordId=$id")
|
||||||
}
|
}
|
||||||
object KnownForView: AppNavItem("known_for_route") {
|
object KnownForView: AppNavItem("known_for_route") {
|
||||||
fun withArgs(id: Int) = route.plus("/$id")
|
fun withArgs(id: Int) = route.plus("/$id")
|
||||||
}
|
}
|
||||||
object GalleryView: AppNavItem("gallery_view_route") {
|
object MainView: AppNavItem("main_route")
|
||||||
fun withArgs(type: MediaViewType, id: Int) = route.plus("/$type/$id")
|
object SearchView: AppNavItem("search_route") {
|
||||||
|
fun withArgs(searchType: MediaViewType, pageTitle: String) = route.plus("?searchType=$searchType&pageTitle=$pageTitle")
|
||||||
}
|
}
|
||||||
object SeasonListView: AppNavItem("season_list_route") {
|
object SeasonListView: AppNavItem("season_list_route") {
|
||||||
fun withArgs(id: Int) = route.plus("/$id")
|
fun withArgs(id: Int) = route.plus("/$id")
|
||||||
}
|
}
|
||||||
object CastCrewListView: AppNavItem("cast_crew_list_route") {
|
object SettingsView: AppNavItem("settings_route")
|
||||||
fun withArgs(type: MediaViewType, id: Int) = route.plus("/$type/$id")
|
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.AdditionalDetailItem
|
||||||
import com.owenlejeune.tvtime.ui.components.AvatarImage
|
import com.owenlejeune.tvtime.ui.components.AvatarImage
|
||||||
import com.owenlejeune.tvtime.ui.components.BackButton
|
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.ChipDefaults
|
||||||
import com.owenlejeune.tvtime.ui.components.ChipGroup
|
import com.owenlejeune.tvtime.ui.components.ChipGroup
|
||||||
import com.owenlejeune.tvtime.ui.components.ChipInfo
|
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
|
@Composable
|
||||||
private fun SeasonCard(
|
private fun SeasonCard(
|
||||||
itemId: Int,
|
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.core.tween
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.ExpandMore
|
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.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -43,29 +36,21 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.blur
|
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
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.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.BackButton
|
||||||
|
import com.owenlejeune.tvtime.ui.components.EpisodeItem
|
||||||
import com.owenlejeune.tvtime.ui.components.TVTTopAppBar
|
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.ApplicationViewModel
|
||||||
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
|
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
|
||||||
import com.owenlejeune.tvtime.utils.TmdbUtils
|
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -80,7 +65,7 @@ fun SeasonListScreen(
|
|||||||
applicationViewModel.navigationBarColor.value = MaterialTheme.colorScheme.background
|
applicationViewModel.navigationBarColor.value = MaterialTheme.colorScheme.background
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
val numSeasons = mainViewModel.detailedTv[id]!!.numberOfSeasons
|
val numSeasons = mainViewModel.detailedTv[id]?.numberOfSeasons ?: 0
|
||||||
for (i in 0..numSeasons) {
|
for (i in 0..numSeasons) {
|
||||||
mainViewModel.getSeason(id, i, true)
|
mainViewModel.getSeason(id, i, true)
|
||||||
}
|
}
|
||||||
@@ -113,7 +98,12 @@ fun SeasonListScreen(
|
|||||||
val seasons = seasonsMap[id] ?: emptySet()
|
val seasons = seasonsMap[id] ?: emptySet()
|
||||||
|
|
||||||
seasons.sortedBy { it.seasonNumber }.forEachIndexed { index, season ->
|
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))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
@@ -123,7 +113,12 @@ fun SeasonListScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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) }
|
var isExpanded by remember { mutableStateOf(expandedByDefault) }
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@@ -135,7 +130,8 @@ private fun SeasonSection(season: Season, expandedByDefault: Boolean) {
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
.clip(RoundedCornerShape(10.dp))
|
.clip(RoundedCornerShape(10.dp))
|
||||||
.clickable {
|
.clickable {
|
||||||
|
val combinedId = seriesId.combineWith(season.seasonNumber)
|
||||||
|
appNavController.navigate(AppNavItem.DetailView.withArgs(type = MediaViewType.SEASON, id = combinedId))
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -183,77 +179,3 @@ 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,
|
TV,
|
||||||
@SerializedName("person")
|
@SerializedName("person")
|
||||||
PERSON,
|
PERSON,
|
||||||
|
SEASON,
|
||||||
EPISODE,
|
EPISODE,
|
||||||
MIXED,
|
MIXED,
|
||||||
LIST;
|
LIST;
|
||||||
|
|||||||
@@ -260,4 +260,6 @@
|
|||||||
<string name="place_of_birth">Place of birth</string>
|
<string name="place_of_birth">Place of birth</string>
|
||||||
<string name="birthday">Birthday</string>
|
<string name="birthday">Birthday</string>
|
||||||
<string name="date_of_death">Date of death</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>
|
</resources>
|
||||||
Reference in New Issue
Block a user