move seasons and episode list to separate screen

This commit is contained in:
Owen LeJeune
2023-06-27 17:17:01 -04:00
parent 3d3711cee9
commit f9aa5357dc
10 changed files with 435 additions and 280 deletions

View File

@@ -143,6 +143,9 @@ dependencies {
def compose_markdown = "0.3.3" def compose_markdown = "0.3.3"
implementation "com.github.jeziellago:compose-markdown:$compose_markdown" implementation "com.github.jeziellago:compose-markdown:$compose_markdown"
def cloudy = "0.1.2"
implementation "com.github.skydoves:cloudy:$cloudy"
// testing // testing
def junit = "4.13.2" def junit = "4.13.2"
def androidx_junit = "1.1.3" def androidx_junit = "1.1.3"

View File

@@ -1,18 +1,21 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import java.util.Date
data class Episode( data class Episode(
@SerializedName("name") @SerializedName("air_date") val airDate: Date?,
val name: String, @SerializedName("episode_number") val episodeNumber: Int,
@SerializedName("overview") @SerializedName("id") val id: Int,
val overview: String, @SerializedName("name") val name: String,
@SerializedName("episode_number") @SerializedName("overview") val overview: String,
val episodeNumber: Int, @SerializedName("production_code") val productionCode: String,
@SerializedName("season_number") @SerializedName("runtime") val runtime: Int,
val seasonNumber: Int, @SerializedName("season_number") val seasonNumber: Int,
@SerializedName("still_path") @SerializedName("show_id") val showId: Int,
val stillPath: String?, @SerializedName("still_path") val stillPath: String?,
@SerializedName("air_date") @SerializedName("vote_average") val voteAverage: Float,
val airDate: String? @SerializedName("vote_count") val voteCount: Int,
@SerializedName("crew") val crew: List<CrewMember>?,
@SerializedName("guest_starts") val guestStars: List<CastMember>?
) )

View File

@@ -1,10 +1,15 @@
package com.owenlejeune.tvtime.api.tmdb.api.v3.model package com.owenlejeune.tvtime.api.tmdb.api.v3.model
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import java.util.Date
data class Season( data class Season(
@SerializedName("name") @SerializedName("_id") val _id: String,
val name: String, @SerializedName("air_date") val airDate: Date?,
@SerializedName("episodes") @SerializedName("episodes") val episodes: List<Episode>,
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
) )

View File

@@ -18,4 +18,8 @@ fun <T> List<T>.bringToFront(predicate: (T) -> Boolean): List<T> {
} }
} }
return frontItems.plus(orig) 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))
} }

View File

@@ -28,6 +28,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.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
import com.owenlejeune.tvtime.utils.NavConstants import com.owenlejeune.tvtime.utils.NavConstants
@@ -200,6 +201,16 @@ fun AppNavigationHost(
GalleryView(id = id, type = type, appNavController = appNavController) 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") { object GalleryView: AppNavItem("gallery_view_route") {
fun withArgs(type: MediaViewType, id: Int) = route.plus("/$type/$id") fun withArgs(type: MediaViewType, id: Int) = route.plus("/$type/$id")
} }
object SeasonListView: AppNavItem("season_list_route") {
fun withArgs(id: Int) = route.plus("/$id")
}
} }

View File

@@ -4,8 +4,6 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.widget.Toast import android.widget.Toast
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow 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.ArrowBack
import androidx.compose.material.icons.filled.Movie import androidx.compose.material.icons.filled.Movie
import androidx.compose.material.icons.filled.Send import androidx.compose.material.icons.filled.Send
import androidx.compose.material.icons.outlined.ExpandMore
import androidx.compose.material3.* import androidx.compose.material3.*
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.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
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.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
@@ -38,13 +33,13 @@ import androidx.paging.compose.collectAsLazyPagingItems
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.PagerState import com.google.accompanist.pager.PagerState
import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.pager.rememberPagerState
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.* import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
import com.owenlejeune.tvtime.extensions.WindowSizeClass import com.owenlejeune.tvtime.extensions.WindowSizeClass
import com.owenlejeune.tvtime.extensions.getCalendarYear
import com.owenlejeune.tvtime.extensions.lazyPagingItems import com.owenlejeune.tvtime.extensions.lazyPagingItems
import com.owenlejeune.tvtime.extensions.listItems import com.owenlejeune.tvtime.extensions.listItems
import com.owenlejeune.tvtime.ui.components.* 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.SessionManager
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
import com.owenlejeune.tvtime.utils.types.TabNavItem
import kotlinx.coroutines.* import kotlinx.coroutines.*
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalPagerApi::class)
@@ -204,24 +198,55 @@ private fun MediaViewContent(
ActionsView(itemId = itemId, type = type, modifier = Modifier.padding(start = 20.dp)) ActionsView(itemId = itemId, type = type, modifier = Modifier.padding(start = 20.dp))
} }
if (type == MediaViewType.MOVIE) { Column(
MainContentMovie( verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(horizontal = 16.dp)
) {
OverviewCard(
itemId = itemId, itemId = itemId,
mediaItem = mediaItem, mediaItem = mediaItem,
type = type, type = type,
appNavController = appNavController, mainViewModel = mainViewModel,
windowSize = windowSize, appNavController = appNavController
mainViewModel = mainViewModel
) )
} else {
MainContentTv( CastCard(
itemId = itemId, itemId = itemId,
mediaItem = mediaItem,
type = type,
appNavController = appNavController, appNavController = appNavController,
windowSize = windowSize, type = type,
mainViewModel = mainViewModel 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 @Composable
private fun MiscTvDetails( private fun MiscTvDetails(
itemId: Int, 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 @Composable
fun SimilarContentCard( fun SimilarContentCard(
itemId: Int, itemId: Int,
@@ -914,7 +851,6 @@ private fun VideoGroup(results: List<Video>, type: Video.Type, title: String) {
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun WatchProvidersCard( private fun WatchProvidersCard(
itemId: Int, itemId: Int,
@@ -945,7 +881,7 @@ private fun WatchProvidersCard(
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
var selectedIndex by remember { mutableStateOf( var selectedIndex by remember { mutableIntStateOf(
when { when {
providers.flaterate != null -> 0 providers.flaterate != null -> 0
providers.rent != null -> 1 providers.rent != null -> 1
@@ -1192,83 +1128,4 @@ fun DetailsFor(
mainViewModel = mainViewModel 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"
} }

View File

@@ -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
)
}
}
}
}

View File

@@ -33,8 +33,8 @@ sealed class MediaTabNavItem(
} }
companion object { companion object {
private val MovieItems = listOf(NowPlaying, Popular, Trending, Upcoming, TopRated) private val MovieItems = listOf(Trending, NowPlaying, Popular, Upcoming, TopRated)
private val TvItems = listOf(OnTheAir, Popular, Trending, AiringToday, TopRated) private val TvItems = listOf(Trending, OnTheAir, Popular, AiringToday, TopRated)
fun itemsForType(type: MediaViewType): List<MediaTabNavItem> { fun itemsForType(type: MediaViewType): List<MediaTabNavItem> {
return when (type) { return when (type) {

View File

@@ -242,15 +242,12 @@ object TmdbUtils {
return "$${thousands}" return "$${thousands}"
} }
fun convertEpisodeDate(inDate: String?): String? { fun convertEpisodeDate(inDate: Date?): String? {
if (inDate == null) { if (inDate == null) {
return null return null
} }
val origFormat = SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault())
val outFormat = SimpleDateFormat("MMMM dd, yyyy", java.util.Locale.getDefault()) val outFormat = SimpleDateFormat("MMMM dd, yyyy", java.util.Locale.getDefault())
return outFormat.format(inDate)
return origFormat.parse(inDate)?.let { outFormat.format(it) }
} }
fun toDate(releaseDate: String): Date { fun toDate(releaseDate: String): Date {

View File

@@ -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)
}