mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-08 04:32:43 -05:00
move seasons and episode list to separate screen
This commit is contained in:
@@ -143,6 +143,9 @@ dependencies {
|
||||
def compose_markdown = "0.3.3"
|
||||
implementation "com.github.jeziellago:compose-markdown:$compose_markdown"
|
||||
|
||||
def cloudy = "0.1.2"
|
||||
implementation "com.github.skydoves:cloudy:$cloudy"
|
||||
|
||||
// testing
|
||||
def junit = "4.13.2"
|
||||
def androidx_junit = "1.1.3"
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import java.util.Date
|
||||
|
||||
data class Episode(
|
||||
@SerializedName("name")
|
||||
val name: String,
|
||||
@SerializedName("overview")
|
||||
val overview: String,
|
||||
@SerializedName("episode_number")
|
||||
val episodeNumber: Int,
|
||||
@SerializedName("season_number")
|
||||
val seasonNumber: Int,
|
||||
@SerializedName("still_path")
|
||||
val stillPath: String?,
|
||||
@SerializedName("air_date")
|
||||
val airDate: String?
|
||||
@SerializedName("air_date") val airDate: Date?,
|
||||
@SerializedName("episode_number") val episodeNumber: Int,
|
||||
@SerializedName("id") val id: Int,
|
||||
@SerializedName("name") val name: String,
|
||||
@SerializedName("overview") val overview: String,
|
||||
@SerializedName("production_code") val productionCode: String,
|
||||
@SerializedName("runtime") val runtime: Int,
|
||||
@SerializedName("season_number") val seasonNumber: Int,
|
||||
@SerializedName("show_id") val showId: Int,
|
||||
@SerializedName("still_path") val stillPath: String?,
|
||||
@SerializedName("vote_average") val voteAverage: Float,
|
||||
@SerializedName("vote_count") val voteCount: Int,
|
||||
@SerializedName("crew") val crew: List<CrewMember>?,
|
||||
@SerializedName("guest_starts") val guestStars: List<CastMember>?
|
||||
)
|
||||
@@ -1,10 +1,15 @@
|
||||
package com.owenlejeune.tvtime.api.tmdb.api.v3.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import java.util.Date
|
||||
|
||||
data class Season(
|
||||
@SerializedName("name")
|
||||
val name: String,
|
||||
@SerializedName("episodes")
|
||||
val episodes: List<Episode>
|
||||
@SerializedName("_id") val _id: String,
|
||||
@SerializedName("air_date") val airDate: Date?,
|
||||
@SerializedName("episodes") val episodes: List<Episode>,
|
||||
@SerializedName("name") val name: String,
|
||||
@SerializedName("overview") val overview: String,
|
||||
@SerializedName("id") val id: Int,
|
||||
@SerializedName("poster_path") val posterPath: String?,
|
||||
@SerializedName("season_number") val seasonNumber: Int
|
||||
)
|
||||
@@ -18,4 +18,8 @@ fun <T> List<T>.bringToFront(predicate: (T) -> Boolean): List<T> {
|
||||
}
|
||||
}
|
||||
return frontItems.plus(orig)
|
||||
}
|
||||
|
||||
fun <T> List<T>.subListLimit(limit: Int, fromIndex: Int = 0): List<T> {
|
||||
return subList(fromIndex = fromIndex, toIndex = minOf(size, fromIndex+limit))
|
||||
}
|
||||
@@ -28,6 +28,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.SeasonListScreen
|
||||
import com.owenlejeune.tvtime.ui.screens.SettingsScreen
|
||||
import com.owenlejeune.tvtime.ui.screens.WebLinkScreen
|
||||
import com.owenlejeune.tvtime.utils.NavConstants
|
||||
@@ -200,6 +201,16 @@ fun AppNavigationHost(
|
||||
|
||||
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)!!
|
||||
|
||||
SeasonListScreen(id = id, appNavController = appNavController)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,5 +244,8 @@ sealed class AppNavItem(val route: String) {
|
||||
object GalleryView: AppNavItem("gallery_view_route") {
|
||||
fun withArgs(type: MediaViewType, id: Int) = route.plus("/$type/$id")
|
||||
}
|
||||
object SeasonListView: AppNavItem("season_list_route") {
|
||||
fun withArgs(id: Int) = route.plus("/$id")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
@@ -14,16 +12,13 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Movie
|
||||
import androidx.compose.material.icons.filled.Send
|
||||
import androidx.compose.material.icons.outlined.ExpandMore
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
@@ -38,13 +33,13 @@ import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.PagerState
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import com.owenlejeune.tvtime.R
|
||||
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
|
||||
import com.owenlejeune.tvtime.extensions.WindowSizeClass
|
||||
import com.owenlejeune.tvtime.extensions.getCalendarYear
|
||||
import com.owenlejeune.tvtime.extensions.lazyPagingItems
|
||||
import com.owenlejeune.tvtime.extensions.listItems
|
||||
import com.owenlejeune.tvtime.ui.components.*
|
||||
@@ -53,7 +48,6 @@ import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
|
||||
import com.owenlejeune.tvtime.utils.SessionManager
|
||||
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||
import com.owenlejeune.tvtime.utils.types.MediaViewType
|
||||
import com.owenlejeune.tvtime.utils.types.TabNavItem
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)
|
||||
@@ -204,24 +198,55 @@ private fun MediaViewContent(
|
||||
ActionsView(itemId = itemId, type = type, modifier = Modifier.padding(start = 20.dp))
|
||||
}
|
||||
|
||||
if (type == MediaViewType.MOVIE) {
|
||||
MainContentMovie(
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
) {
|
||||
OverviewCard(
|
||||
itemId = itemId,
|
||||
mediaItem = mediaItem,
|
||||
type = type,
|
||||
appNavController = appNavController,
|
||||
windowSize = windowSize,
|
||||
mainViewModel = mainViewModel
|
||||
mainViewModel = mainViewModel,
|
||||
appNavController = appNavController
|
||||
)
|
||||
} else {
|
||||
MainContentTv(
|
||||
|
||||
CastCard(
|
||||
itemId = itemId,
|
||||
mediaItem = mediaItem,
|
||||
type = type,
|
||||
appNavController = appNavController,
|
||||
windowSize = windowSize,
|
||||
type = type,
|
||||
mainViewModel = mainViewModel
|
||||
)
|
||||
|
||||
if (type == MediaViewType.TV) {
|
||||
SeasonCard(
|
||||
itemId = itemId,
|
||||
mediaItem = mediaItem,
|
||||
mainViewModel = mainViewModel,
|
||||
appNavController = appNavController
|
||||
)
|
||||
}
|
||||
|
||||
SimilarContentCard(
|
||||
itemId = itemId,
|
||||
mediaType = type,
|
||||
appNavController = appNavController,
|
||||
mainViewModel = mainViewModel
|
||||
)
|
||||
|
||||
VideosCard(
|
||||
itemId = itemId,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
mainViewModel = mainViewModel,
|
||||
type = type
|
||||
)
|
||||
|
||||
AdditionalDetailsCard(mediaItem = mediaItem, type = type)
|
||||
|
||||
WatchProvidersCard(itemId = itemId, type = type, mainViewModel = mainViewModel)
|
||||
|
||||
if (windowSize != WindowSizeClass.Expanded) {
|
||||
ReviewsCard(itemId = itemId, type = type, mainViewModel = mainViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,165 +268,6 @@ private fun MediaViewContent(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
private fun TvSeriesTabs(
|
||||
pagerState: PagerState,
|
||||
tabs: List<TabNavItem>
|
||||
) {
|
||||
Tabs(
|
||||
modifier = Modifier.offset(y = (-16).dp),
|
||||
pagerState = pagerState,
|
||||
tabs = tabs
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SeasonsTab(
|
||||
itemId: Int,
|
||||
mediaItem: DetailedItem?,
|
||||
mainViewModel: MainViewModel
|
||||
) {
|
||||
LaunchedEffect(mediaItem) {
|
||||
val numSeasons = (mediaItem as DetailedTv?)?.numberOfSeasons ?: 0
|
||||
for (i in 0..numSeasons) {
|
||||
mainViewModel.getSeason(itemId, i)
|
||||
}
|
||||
}
|
||||
|
||||
val seasonsMap = remember { mainViewModel.tvSeasons }
|
||||
val seasons = seasonsMap[itemId]
|
||||
|
||||
seasons?.forEach { season ->
|
||||
SeasonSection(season = season)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SeasonSection(season: Season) {
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(color = MaterialTheme.colorScheme.surfaceVariant)
|
||||
.clickable {
|
||||
isExpanded = !isExpanded
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 24.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = season.name,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
var currentRotation by remember { mutableStateOf(0f) }
|
||||
val rotation = remember { androidx.compose.animation.core.Animatable(currentRotation) }
|
||||
LaunchedEffect(isExpanded) {
|
||||
rotation.animateTo(
|
||||
targetValue = if (isExpanded) 180f else 0f,
|
||||
animationSpec = tween(200, easing = LinearEasing)
|
||||
) {
|
||||
currentRotation = value
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.ExpandMore,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.rotate(currentRotation),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
enter = expandVertically(),
|
||||
exit = shrinkVertically()
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
season.episodes.forEachIndexed { index, episode ->
|
||||
EpisodeItem(episode = episode)
|
||||
if (index != season.episodes.size - 1) {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EpisodeItem(episode: Episode) {
|
||||
val height = 170.dp
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.height(height)
|
||||
) {
|
||||
AsyncImage(
|
||||
model = TmdbUtils.getFullEpisodeStillPath(episode),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(width = 100.dp, height = height)
|
||||
.clip(RoundedCornerShape(10.dp)),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "S${episode.seasonNumber}E${episode.episodeNumber} • ${episode.name}",
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
TmdbUtils.convertEpisodeDate(episode.airDate)?.let {
|
||||
Text(
|
||||
text = it,
|
||||
fontStyle = FontStyle.Italic,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = episode.overview,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainContent(
|
||||
itemId: Int,
|
||||
mediaItem: DetailedItem?,
|
||||
type: MediaViewType,
|
||||
appNavController: NavController,
|
||||
windowSize: WindowSizeClass,
|
||||
mainViewModel: MainViewModel
|
||||
) {
|
||||
OverviewCard(itemId = itemId, mediaItem = mediaItem, type = type, mainViewModel = mainViewModel, appNavController = appNavController)
|
||||
|
||||
CastCard(itemId = itemId, appNavController = appNavController, type = type, mainViewModel = mainViewModel)
|
||||
|
||||
SimilarContentCard(itemId = itemId, mediaType = type, appNavController = appNavController, mainViewModel = mainViewModel)
|
||||
|
||||
VideosCard(itemId = itemId, modifier = Modifier.fillMaxWidth(), mainViewModel = mainViewModel, type = type)
|
||||
|
||||
AdditionalDetailsCard(mediaItem = mediaItem, type = type)
|
||||
|
||||
WatchProvidersCard(itemId = itemId, type = type, mainViewModel = mainViewModel)
|
||||
|
||||
if (windowSize != WindowSizeClass.Expanded) {
|
||||
ReviewsCard(itemId = itemId, type = type, mainViewModel = mainViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MiscTvDetails(
|
||||
itemId: Int,
|
||||
@@ -778,6 +644,77 @@ private fun CastCrewCard(appNavController: NavController, person: Person) {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SeasonCard(
|
||||
itemId: Int,
|
||||
mediaItem: DetailedItem?,
|
||||
mainViewModel: MainViewModel,
|
||||
appNavController: NavController
|
||||
) {
|
||||
LaunchedEffect(mediaItem) {
|
||||
val lastSeason = (mediaItem as DetailedTv?)?.numberOfSeasons ?: 0
|
||||
if (lastSeason > 0) {
|
||||
mainViewModel.getSeason(itemId, lastSeason)
|
||||
}
|
||||
}
|
||||
|
||||
val seasonsMap = remember { mainViewModel.tvSeasons }
|
||||
val lastSeason = seasonsMap[itemId]?.lastOrNull()
|
||||
|
||||
lastSeason?.let {
|
||||
ContentCard(
|
||||
title = "Latest Season"
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(all = 12.dp)
|
||||
) {
|
||||
PosterItem(
|
||||
url = TmdbUtils.getFullPosterPath(it.posterPath),
|
||||
title = it.name,
|
||||
overrideShowTitle = false,
|
||||
enabled = false
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = it.name,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(
|
||||
text = "${it.airDate?.getCalendarYear()} | ${it.episodes.size} Episodes"
|
||||
)
|
||||
Text(
|
||||
text = it.overview,
|
||||
fontSize = 14.sp,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, bottom = 12.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.clickable {
|
||||
appNavController.navigate(AppNavItem.SeasonListView.withArgs(id = itemId))
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = "See all seasons",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimilarContentCard(
|
||||
itemId: Int,
|
||||
@@ -914,7 +851,6 @@ private fun VideoGroup(results: List<Video>, type: Video.Type, title: String) {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun WatchProvidersCard(
|
||||
itemId: Int,
|
||||
@@ -945,7 +881,7 @@ private fun WatchProvidersCard(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
var selectedIndex by remember { mutableStateOf(
|
||||
var selectedIndex by remember { mutableIntStateOf(
|
||||
when {
|
||||
providers.flaterate != null -> 0
|
||||
providers.rent != null -> 1
|
||||
@@ -1192,83 +1128,4 @@ fun DetailsFor(
|
||||
mainViewModel = mainViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainContentMovie(
|
||||
type: MediaViewType,
|
||||
itemId: Int,
|
||||
mediaItem: DetailedItem?,
|
||||
appNavController: NavController,
|
||||
windowSize: WindowSizeClass,
|
||||
mainViewModel: MainViewModel
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
) {
|
||||
MainContent(
|
||||
itemId = itemId,
|
||||
mediaItem = mediaItem,
|
||||
type = type,
|
||||
appNavController = appNavController,
|
||||
windowSize = windowSize,
|
||||
mainViewModel = mainViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
fun MainContentTv(
|
||||
type: MediaViewType,
|
||||
itemId: Int,
|
||||
mediaItem: DetailedItem?,
|
||||
mainViewModel: MainViewModel,
|
||||
appNavController: NavController,
|
||||
windowSize: WindowSizeClass
|
||||
) {
|
||||
val tabState = rememberPagerState()
|
||||
val tabs = listOf(DetailsTab, SeasonsTab)
|
||||
TvSeriesTabs(pagerState = tabState, tabs = tabs)
|
||||
HorizontalPager(
|
||||
count = tabs.size,
|
||||
state = tabState,
|
||||
userScrollEnabled = false
|
||||
) { page ->
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
) {
|
||||
when (tabs[page]) {
|
||||
is DetailsTab -> {
|
||||
MainContent(
|
||||
itemId = itemId,
|
||||
mediaItem = mediaItem,
|
||||
type = type,
|
||||
appNavController = appNavController,
|
||||
windowSize = windowSize,
|
||||
mainViewModel = mainViewModel
|
||||
)
|
||||
}
|
||||
|
||||
is SeasonsTab -> {
|
||||
SeasonsTab(
|
||||
itemId = itemId,
|
||||
mediaItem = mediaItem,
|
||||
mainViewModel = mainViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object DetailsTab: TabNavItem("details_route") {
|
||||
override val name: String
|
||||
get() = "Details"
|
||||
}
|
||||
object SeasonsTab: TabNavItem("seasons_route") {
|
||||
override val name: String
|
||||
get() = "Seasons"
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package com.owenlejeune.tvtime.ui.screens
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.Animatable
|
||||
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.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.filled.ArrowBack
|
||||
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.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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.res.stringResource
|
||||
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 com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
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.ui.viewmodel.MainViewModel
|
||||
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||
import com.skydoves.cloudy.Cloudy
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SeasonListScreen(
|
||||
id: Int,
|
||||
appNavController: NavController
|
||||
) {
|
||||
val mainViewModel = viewModel<MainViewModel>()
|
||||
|
||||
val systemUiController = rememberSystemUiController()
|
||||
systemUiController.setStatusBarColor(color = MaterialTheme.colorScheme.background)
|
||||
systemUiController.setNavigationBarColor(color = MaterialTheme.colorScheme.background)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val numSeasons = mainViewModel.detailedTv[id]!!.numberOfSeasons
|
||||
for (i in 0..numSeasons) {
|
||||
mainViewModel.getSeason(id, i)
|
||||
}
|
||||
}
|
||||
|
||||
val topAppBarScrollState = rememberTopAppBarState()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(topAppBarScrollState)
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TopAppBarDefaults
|
||||
.topAppBarColors(
|
||||
scrolledContainerColor = MaterialTheme.colorScheme.background,
|
||||
titleContentColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
title = { },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = { appNavController.popBackStack() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = stringResource(id = R.string.content_description_back_button),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Box(modifier = Modifier.padding(innerPadding)) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.verticalScroll(state = rememberScrollState())
|
||||
) {
|
||||
val seasonsMap = remember { mainViewModel.tvSeasons }
|
||||
val seasons = seasonsMap[id] ?: emptyList()
|
||||
|
||||
seasons.sortedBy { it.seasonNumber }.forEach { season ->
|
||||
SeasonSection(season = season)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SeasonSection(season: Season) {
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.clickable {
|
||||
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = season.name,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
.padding(vertical = 12.dp, horizontal = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
var currentRotation by remember { mutableFloatStateOf(0f) }
|
||||
val rotation = remember { Animatable(currentRotation) }
|
||||
LaunchedEffect(isExpanded) {
|
||||
rotation.animateTo(
|
||||
targetValue = if (isExpanded) 180f else 0f,
|
||||
animationSpec = tween(200, easing = LinearEasing)
|
||||
) {
|
||||
currentRotation = value
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.ExpandMore,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.rotate(currentRotation)
|
||||
.clickable {
|
||||
isExpanded = !isExpanded
|
||||
},
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
enter = expandVertically(),
|
||||
exit = shrinkVertically()
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
season.episodes.forEach { episode ->
|
||||
EpisodeItem(episode = episode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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))
|
||||
// ) {
|
||||
AsyncImage(
|
||||
model = TmdbUtils.getFullEpisodeStillPath(it),
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,8 +33,8 @@ sealed class MediaTabNavItem(
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val MovieItems = listOf(NowPlaying, Popular, Trending, Upcoming, TopRated)
|
||||
private val TvItems = listOf(OnTheAir, Popular, Trending, AiringToday, TopRated)
|
||||
private val MovieItems = listOf(Trending, NowPlaying, Popular, Upcoming, TopRated)
|
||||
private val TvItems = listOf(Trending, OnTheAir, Popular, AiringToday, TopRated)
|
||||
|
||||
fun itemsForType(type: MediaViewType): List<MediaTabNavItem> {
|
||||
return when (type) {
|
||||
|
||||
@@ -242,15 +242,12 @@ object TmdbUtils {
|
||||
return "$${thousands}"
|
||||
}
|
||||
|
||||
fun convertEpisodeDate(inDate: String?): String? {
|
||||
fun convertEpisodeDate(inDate: Date?): String? {
|
||||
if (inDate == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val origFormat = SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault())
|
||||
val outFormat = SimpleDateFormat("MMMM dd, yyyy", java.util.Locale.getDefault())
|
||||
|
||||
return origFormat.parse(inDate)?.let { outFormat.format(it) }
|
||||
return outFormat.format(inDate)
|
||||
}
|
||||
|
||||
fun toDate(releaseDate: String): Date {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.owenlejeune.tvtime.utils.types
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
enum class Gender(val rawValue: Int) {
|
||||
@SerializedName("1")
|
||||
MALE(1),
|
||||
@SerializedName("2")
|
||||
FEMALE(2)
|
||||
}
|
||||
Reference in New Issue
Block a user