mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-08 12:42:44 -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"
|
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"
|
||||||
|
|||||||
@@ -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>?
|
||||||
)
|
)
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
@@ -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))
|
||||||
}
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
}
|
||||||
@@ -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 {
|
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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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