add screen for movies/tv by keyword from details page

This commit is contained in:
Owen LeJeune
2023-06-23 21:15:21 -04:00
parent 90969247f9
commit 62324d8de7
15 changed files with 221 additions and 35 deletions

View File

@@ -29,4 +29,6 @@ interface DetailService {
suspend fun getAccountStates(id: Int) suspend fun getAccountStates(id: Int)
suspend fun discover(keywords: String? = null, page: Int): Response<out SearchResult<out SearchResultMedia>>
} }

View File

@@ -64,4 +64,7 @@ interface MoviesApi {
@GET("movie/{id}/account_states") @GET("movie/{id}/account_states")
suspend fun getAccountStates(@Path("id") id: Int, @Query("session_id") sessionId: String): Response<AccountStates> suspend fun getAccountStates(@Path("id") id: Int, @Query("session_id") sessionId: String): Response<AccountStates>
@GET("discover/movie")
suspend fun discover(@Query("with_keywords") keywords: String? = null, @Query("page") page: Int): Response<SearchResult<SearchResultMovie>>
} }

View File

@@ -19,6 +19,8 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MovieReleaseResults
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.RatingBody import com.owenlejeune.tvtime.api.tmdb.api.v3.model.RatingBody
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Review import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Review
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResult import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResult
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultMedia
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultMovie
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Searchable import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Searchable
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SortableSearchResult import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SortableSearchResult
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.StatusResponse import com.owenlejeune.tvtime.api.tmdb.api.v3.model.StatusResponse
@@ -53,6 +55,7 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
val releaseDates = Collections.synchronizedMap(mutableStateMapOf<Int, List<MovieReleaseResults.ReleaseDateResult>>()) val releaseDates = Collections.synchronizedMap(mutableStateMapOf<Int, List<MovieReleaseResults.ReleaseDateResult>>())
val similar = Collections.synchronizedMap(mutableStateMapOf<Int, Flow<PagingData<TmdbItem>>>()) val similar = Collections.synchronizedMap(mutableStateMapOf<Int, Flow<PagingData<TmdbItem>>>())
val accountStates = Collections.synchronizedMap(mutableStateMapOf<Int, AccountStates>()) val accountStates = Collections.synchronizedMap(mutableStateMapOf<Int, AccountStates>())
val keywordResults = Collections.synchronizedMap(mutableStateMapOf<Int, Flow<PagingData<SearchResultMedia>>>())
override suspend fun getById(id: Int) { override suspend fun getById(id: Int) {
@@ -134,6 +137,9 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
} }
} }
override suspend fun discover(keywords: String?, page: Int): Response<out SearchResult<out SearchResultMedia>> {
return movieService.discover(keywords, page)
}
override suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse> { override suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse> {
return movieService.getSimilarMovies(id, page) return movieService.getSimilarMovies(id, page)

View File

@@ -67,4 +67,6 @@ interface TvApi {
@GET("tv/{id}/account_states") @GET("tv/{id}/account_states")
suspend fun getAccountStates(@Path("id") id: Int): Response<AccountStates> suspend fun getAccountStates(@Path("id") id: Int): Response<AccountStates>
@GET("discover/tv")
suspend fun discover(@Query("page") page: Int, @Query("with_keywords") keywords: String? = null): Response<SearchResult<SearchResultTv>>
} }

View File

@@ -21,6 +21,8 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.KeywordsResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.RatingBody import com.owenlejeune.tvtime.api.tmdb.api.v3.model.RatingBody
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Review import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Review
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ReviewResponse import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ReviewResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResult
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultMedia
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Season import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Season
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.StatusResponse import com.owenlejeune.tvtime.api.tmdb.api.v3.model.StatusResponse
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TmdbItem import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TmdbItem
@@ -57,6 +59,7 @@ class TvService: KoinComponent, DetailService, HomePageService {
val contentRatings = Collections.synchronizedMap(mutableStateMapOf<Int, List<TvContentRatings.TvContentRating>>()) val contentRatings = Collections.synchronizedMap(mutableStateMapOf<Int, List<TvContentRatings.TvContentRating>>())
val similar = Collections.synchronizedMap(mutableStateMapOf<Int, Flow<PagingData<TmdbItem>>>()) val similar = Collections.synchronizedMap(mutableStateMapOf<Int, Flow<PagingData<TmdbItem>>>())
val accountStates = Collections.synchronizedMap(mutableStateMapOf<Int, AccountStates>()) val accountStates = Collections.synchronizedMap(mutableStateMapOf<Int, AccountStates>())
val keywordResults = Collections.synchronizedMap(mutableStateMapOf<Int, Flow<PagingData<SearchResultMedia>>>())
private val _seasons = Collections.synchronizedMap(mutableStateMapOf<Int, MutableSet<Season>>()) private val _seasons = Collections.synchronizedMap(mutableStateMapOf<Int, MutableSet<Season>>())
val seasons: MutableMap<Int, out Set<Season>> val seasons: MutableMap<Int, out Set<Season>>
@@ -137,6 +140,10 @@ class TvService: KoinComponent, DetailService, HomePageService {
} }
} }
override suspend fun discover(keywords: String?, page: Int): Response<out SearchResult<out SearchResultMedia>> {
return service.discover(page, keywords)
}
override suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse> { override suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse> {
return service.getSimilarTvShows(id, page) return service.getSimilarTvShows(id, page)
} }

View File

@@ -0,0 +1,14 @@
package com.owenlejeune.tvtime.extensions
import android.os.Build
import android.os.Bundle
import java.io.Serializable
import kotlin.reflect.KClass
fun <T: Serializable> Bundle.safeGetSerializable(key: String, clazz: Class<T>): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getSerializable(key, clazz)
} else {
getSerializable(key) as? T
}
}

View File

@@ -130,7 +130,7 @@ fun ActionButton(
.clip(CircleShape) .clip(CircleShape)
.height(40.dp) .height(40.dp)
.requiredWidthIn(min = 40.dp) .requiredWidthIn(min = 40.dp)
.background(color = MaterialTheme.colorScheme.actionButtonColor) .background(color = MaterialTheme.colorScheme.tertiary)
.clickable(onClick = onClick) .clickable(onClick = onClick)
) { ) {
Icon( Icon(

View File

@@ -222,6 +222,8 @@ fun SearchView(
val homeScreenViewModel = viewModel<HomeScreenViewModel>() val homeScreenViewModel = viewModel<HomeScreenViewModel>()
homeScreenViewModel.fab.value = @Composable { homeScreenViewModel.fab.value = @Composable {
FloatingActionButton( FloatingActionButton(
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
contentColor = MaterialTheme.colorScheme.tertiary,
onClick = { onClick = {
appNavController.navigate(route) appNavController.navigate(route)
} }
@@ -279,7 +281,8 @@ fun MinLinesText(
class ChipInfo( class ChipInfo(
val text: String, val text: String,
val enabled: Boolean = true val enabled: Boolean = true,
val id: Int? = null
) )
sealed class ChipStyle(val mainAxisSpacing: Dp, val crossAxisSpacing: Dp) { sealed class ChipStyle(val mainAxisSpacing: Dp, val crossAxisSpacing: Dp) {
@@ -329,11 +332,10 @@ object ChipDefaults {
@Composable @Composable
fun BoxyChip( fun BoxyChip(
text: String, chipInfo: ChipInfo,
style: TextStyle = MaterialTheme.typography.bodySmall, style: TextStyle = MaterialTheme.typography.bodySmall,
isSelected: Boolean = true, isSelected: Boolean = true,
onSelectionChanged: (String) -> Unit = {}, onSelectionChanged: (ChipInfo) -> Unit = {},
enabled: Boolean = true,
colors: ChipColors = ChipDefaults.boxyChipColors() colors: ChipColors = ChipDefaults.boxyChipColors()
) { ) {
Surface( Surface(
@@ -346,13 +348,13 @@ fun BoxyChip(
.toggleable( .toggleable(
value = isSelected, value = isSelected,
onValueChange = { onValueChange = {
onSelectionChanged(text) onSelectionChanged(chipInfo)
}, },
enabled = enabled enabled = chipInfo.enabled
) )
) { ) {
Text( Text(
text = text, text = chipInfo.text,
style = style, style = style,
color = if (isSelected) colors.selectedContentColor() else colors.unselectedContentColor(), color = if (isSelected) colors.selectedContentColor() else colors.unselectedContentColor(),
modifier = Modifier.padding(8.dp) modifier = Modifier.padding(8.dp)
@@ -363,11 +365,10 @@ fun BoxyChip(
@Composable @Composable
fun RoundedChip( fun RoundedChip(
text: String, chipInfo: ChipInfo,
style: TextStyle = MaterialTheme.typography.bodySmall, style: TextStyle = MaterialTheme.typography.bodySmall,
isSelected: Boolean = false, isSelected: Boolean = false,
onSelectionChanged: (String) -> Unit = {}, onSelectionChanged: (ChipInfo) -> Unit = {},
enabled: Boolean = true,
colors: ChipColors = ChipDefaults.roundedChipColors() colors: ChipColors = ChipDefaults.roundedChipColors()
) { ) {
val borderColor = if (isSelected) colors.selectedContainerColor() else colors.unselectedContainerColor() val borderColor = if (isSelected) colors.selectedContainerColor() else colors.unselectedContainerColor()
@@ -382,14 +383,14 @@ fun RoundedChip(
.toggleable( .toggleable(
value = isSelected, value = isSelected,
onValueChange = { onValueChange = {
onSelectionChanged(text) onSelectionChanged(chipInfo)
}, },
enabled = enabled enabled = chipInfo.enabled
) )
.padding(8.dp) .padding(8.dp)
) { ) {
Text( Text(
text = text, text = chipInfo.text,
style = style, style = style,
color = if (isSelected) colors.selectedContentColor() else colors.unselectedContentColor() color = if (isSelected) colors.selectedContentColor() else colors.unselectedContentColor()
) )
@@ -401,7 +402,7 @@ fun RoundedChip(
fun ChipGroup( fun ChipGroup(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
chips: List<ChipInfo> = emptyList(), chips: List<ChipInfo> = emptyList(),
onSelectedChanged: (String) -> Unit = {}, onSelectedChanged: (ChipInfo) -> Unit = {},
chipStyle: ChipStyle = ChipStyle.Boxy, chipStyle: ChipStyle = ChipStyle.Boxy,
roundedChipColors: ChipColors = ChipDefaults.roundedChipColors(), roundedChipColors: ChipColors = ChipDefaults.roundedChipColors(),
boxyChipColors: ChipColors = ChipDefaults.boxyChipColors() boxyChipColors: ChipColors = ChipDefaults.boxyChipColors()
@@ -412,17 +413,15 @@ fun ChipGroup(
when (chipStyle) { when (chipStyle) {
ChipStyle.Boxy -> { ChipStyle.Boxy -> {
BoxyChip( BoxyChip(
text = chip.text, chipInfo = chip,
onSelectionChanged = onSelectedChanged, onSelectionChanged = onSelectedChanged,
enabled = chip.enabled,
colors = boxyChipColors colors = boxyChipColors
) )
} }
ChipStyle.Rounded -> { ChipStyle.Rounded -> {
RoundedChip( RoundedChip(
text = chip.text, chipInfo = chip,
onSelectionChanged = onSelectedChanged, onSelectionChanged = onSelectedChanged,
enabled = chip.enabled,
colors = roundedChipColors colors = roundedChipColors
) )
} }
@@ -453,6 +452,7 @@ fun RatingRing(
size: Dp = 60.dp, size: Dp = 60.dp,
ringStrokeWidth: Dp = 4.dp, ringStrokeWidth: Dp = 4.dp,
ringColor: Color = MaterialTheme.colorScheme.primary, ringColor: Color = MaterialTheme.colorScheme.primary,
trackColor: Color = MaterialTheme.colorScheme.tertiaryContainer,
textColor: Color = Color.White, textColor: Color = Color.White,
textSize: TextUnit = 14.sp textSize: TextUnit = 14.sp
) { ) {
@@ -464,7 +464,9 @@ fun RatingRing(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
progress = progress, progress = progress,
strokeWidth = ringStrokeWidth, strokeWidth = ringStrokeWidth,
color = ringColor color = ringColor,
trackColor = trackColor,
strokeCap = StrokeCap.Round
) )
Text( Text(
@@ -688,7 +690,7 @@ fun UserInitials(
modifier = Modifier modifier = Modifier
.clip(CircleShape) .clip(CircleShape)
.size(size) .size(size)
.background(color = MaterialTheme.colorScheme.secondary) .background(color = MaterialTheme.colorScheme.tertiary)
) { ) {
Text( Text(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),

View File

@@ -1,5 +1,6 @@
package com.owenlejeune.tvtime.ui.navigation package com.owenlejeune.tvtime.ui.navigation
import android.os.Build
import android.util.Log import android.util.Log
import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
@@ -15,10 +16,12 @@ import androidx.navigation.navArgument
import androidx.navigation.navDeepLink import androidx.navigation.navDeepLink
import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.extensions.WindowSizeClass import com.owenlejeune.tvtime.extensions.WindowSizeClass
import com.owenlejeune.tvtime.extensions.safeGetSerializable
import com.owenlejeune.tvtime.preferences.AppPreferences import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.screens.AboutScreen import com.owenlejeune.tvtime.ui.screens.AboutScreen
import com.owenlejeune.tvtime.ui.screens.AccountScreen import com.owenlejeune.tvtime.ui.screens.AccountScreen
import com.owenlejeune.tvtime.ui.screens.HomeScreen import com.owenlejeune.tvtime.ui.screens.HomeScreen
import com.owenlejeune.tvtime.ui.screens.KeywordResultsScreen
import com.owenlejeune.tvtime.ui.screens.ListDetailScreen 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
@@ -116,7 +119,7 @@ fun AppNavigationHost(
) )
} else { } else {
Pair( Pair(
arguments.getSerializable(NavConstants.SEARCH_ID_KEY) as MediaViewType, arguments.safeGetSerializable(NavConstants.SEARCH_ID_KEY, MediaViewType::class.java)!!,
arguments.getString(NavConstants.SEARCH_TITLE_KEY) ?: "" arguments.getString(NavConstants.SEARCH_TITLE_KEY) ?: ""
) )
} }
@@ -154,6 +157,25 @@ fun AppNavigationHost(
composable(route = AppNavItem.AboutView.route) { composable(route = AppNavItem.AboutView.route) {
AboutScreen(appNavController = appNavController) AboutScreen(appNavController = appNavController)
} }
composable(
route = AppNavItem.KeywordsView.route.plus("/{${NavConstants.KEYWORD_TYPE_KEY}}?keyword={${NavConstants.KEYWORD_NAME_KEY}}&keywordId={${NavConstants.KEYWORD_ID_KEY}}"),
arguments = listOf(
navArgument(NavConstants.KEYWORD_TYPE_KEY) { type = NavType.EnumType(MediaViewType::class.java) },
navArgument(NavConstants.KEYWORD_NAME_KEY) { type = NavType.StringType },
navArgument(NavConstants.KEYWORD_ID_KEY) { type = NavType.IntType }
)
) { navBackStackEntry ->
val type = navBackStackEntry.arguments?.safeGetSerializable(NavConstants.KEYWORD_TYPE_KEY, MediaViewType::class.java)!!
val keywords = navBackStackEntry.arguments?.getString(NavConstants.KEYWORD_NAME_KEY) ?: ""
val id = navBackStackEntry.arguments?.getInt(NavConstants.KEYWORD_ID_KEY)!!
KeywordResultsScreen(
type = type,
keyword = keywords,
id = id,
appNavController = appNavController
)
}
} }
} }
@@ -178,5 +200,8 @@ sealed class AppNavItem(val route: String) {
} }
object AccountView: AppNavItem("account_route") object AccountView: AppNavItem("account_route")
object AboutView: AppNavItem("about_route") object AboutView: AppNavItem("about_route")
object KeywordsView: AppNavItem("keywords_route") {
fun withArgs(type: MediaViewType, keyword: String, id: Int) = route.plus("/$type?keyword=$keyword&keywordId=$id")
}
} }

View File

@@ -1,5 +1,7 @@
package com.owenlejeune.tvtime.ui.navigation package com.owenlejeune.tvtime.ui.navigation
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Face import androidx.compose.material.icons.outlined.Face
import androidx.compose.material.icons.outlined.Movie import androidx.compose.material.icons.outlined.Movie
@@ -30,7 +32,12 @@ fun HomeScreenNavHost(
) { ) {
val homeScreenViewModel = viewModel<HomeScreenViewModel>() val homeScreenViewModel = viewModel<HomeScreenViewModel>()
NavHost(navController = navController, startDestination = startDestination) { NavHost(
navController = navController,
startDestination = startDestination,
enterTransition = { fadeIn() },
exitTransition = { fadeOut() }
) {
composable(HomeScreenNavItem.Movies.route) { composable(HomeScreenNavItem.Movies.route) {
homeScreenViewModel.appBarActions.value = {} homeScreenViewModel.appBarActions.value = {}
MediaTab( MediaTab(

View File

@@ -0,0 +1,91 @@
package com.owenlejeune.tvtime.ui.screens
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.paging.compose.collectAsLazyPagingItems
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.extensions.lazyPagingItems
import com.owenlejeune.tvtime.ui.components.MediaResultCard
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.utils.TmdbUtils
import com.owenlejeune.tvtime.utils.types.MediaViewType
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun KeywordResultsScreen(
type: MediaViewType,
keyword: String,
id: Int,
appNavController: NavController
) {
val mainViewModel = viewModel<MainViewModel>()
LaunchedEffect(Unit) {
mainViewModel.getKeywordResults(id, keyword, type)
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = keyword.capitalize(Locale.current)) },
navigationIcon = {
IconButton(
onClick = { appNavController.popBackStack() }
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = null
)
}
}
)
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
val keywordResultsMap = remember { mainViewModel.produceKeywordsResultsFor(type) }
val keywordResults = keywordResultsMap[id]
val pagingResults = keywordResults?.collectAsLazyPagingItems()
LazyColumn(
modifier = Modifier.padding(all = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
pagingResults?.let {
lazyPagingItems(it) { result ->
result?.let {
MediaResultCard(
appNavController = appNavController,
mediaViewType = type,
id = result.id,
backdropPath = TmdbUtils.getFullBackdropPath(result.backdropPath),
posterPath = TmdbUtils.getFullPosterPath(result.posterPath),
title = result.name,
additionalDetails = listOf(result.overview)
)
}
}
}
}
}
}
}

View File

@@ -385,7 +385,7 @@ private fun MainContent(
windowSize: WindowSizeClass, windowSize: WindowSizeClass,
mainViewModel: MainViewModel mainViewModel: MainViewModel
) { ) {
OverviewCard(itemId = itemId, mediaItem = mediaItem, type = type, mainViewModel = mainViewModel) OverviewCard(itemId = itemId, mediaItem = mediaItem, type = type, mainViewModel = mainViewModel, appNavController = appNavController)
CastCard(itemId = itemId, appNavController = appNavController, type = type, mainViewModel = mainViewModel) CastCard(itemId = itemId, appNavController = appNavController, type = type, mainViewModel = mainViewModel)
@@ -500,11 +500,12 @@ private fun MiscDetails(
@Composable @Composable
private fun OverviewCard( private fun OverviewCard(
modifier: Modifier = Modifier,
itemId: Int, itemId: Int,
mediaItem: DetailedItem?, mediaItem: DetailedItem?,
type: MediaViewType, type: MediaViewType,
mainViewModel: MainViewModel mainViewModel: MainViewModel,
appNavController: NavController,
modifier: Modifier = Modifier
) { ) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
mainViewModel.getKeywords(itemId, type) mainViewModel.getKeywords(itemId, type)
@@ -530,7 +531,8 @@ private fun OverviewCard(
Text( Text(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
text = tagline, text = tagline,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.tertiary
,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
fontStyle = FontStyle.Italic, fontStyle = FontStyle.Italic,
) )
@@ -545,7 +547,7 @@ private fun OverviewCard(
keywords?.let { keywords -> keywords?.let { keywords ->
val keywordsChipInfo = keywords.map { ChipInfo(it.name, false) } val keywordsChipInfo = keywords.map { ChipInfo(it.name, true, it.id) }
Row( Row(
modifier = Modifier.horizontalScroll(rememberScrollState()), modifier = Modifier.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(ChipStyle.Rounded.mainAxisSpacing) horizontalArrangement = Arrangement.spacedBy(ChipStyle.Rounded.mainAxisSpacing)
@@ -553,16 +555,13 @@ private fun OverviewCard(
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
keywordsChipInfo.forEach { keywordChipInfo -> keywordsChipInfo.forEach { keywordChipInfo ->
RoundedChip( RoundedChip(
text = keywordChipInfo.text, chipInfo = keywordChipInfo,
enabled = keywordChipInfo.enabled,
colors = ChipDefaults.roundedChipColors( colors = ChipDefaults.roundedChipColors(
unselectedContainerColor = MaterialTheme.colorScheme.primary, unselectedContainerColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.primary unselectedContentColor = MaterialTheme.colorScheme.primary
), ),
onSelectionChanged = { chip -> onSelectionChanged = { chip ->
// if (service is MoviesService) { appNavController.navigate(AppNavItem.KeywordsView.withArgs(type, chip.text, chip.id!!))
// // Toast.makeText(context, chip, Toast.LENGTH_SHORT).show()
// }
} }
) )
} }

View File

@@ -61,7 +61,7 @@ fun MediaTabContent(
mediaTabItem: MediaTabNavItem mediaTabItem: MediaTabNavItem
) { ) {
val viewModel = viewModel<MainViewModel>() val viewModel = viewModel<MainViewModel>()
val mediaListItems = viewModel.produceFlowFor(mediaType, mediaTabItem.type).collectAsLazyPagingItems() val mediaListItems = viewModel.produceMediaTabFlowFor(mediaType, mediaTabItem.type).collectAsLazyPagingItems()
PagingPosterGrid( PagingPosterGrid(
lazyPagingItems = mediaListItems, lazyPagingItems = mediaListItems,

View File

@@ -15,6 +15,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.ImageCollection
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Keyword import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Keyword
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.RatingBody import com.owenlejeune.tvtime.api.tmdb.api.v3.model.RatingBody
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Review import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Review
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.SearchResultMedia
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TmdbItem import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TmdbItem
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video import com.owenlejeune.tvtime.api.tmdb.api.v3.model.Video
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviders import com.owenlejeune.tvtime.api.tmdb.api.v3.model.WatchProviders
@@ -43,6 +44,7 @@ class MainViewModel: ViewModel(), KoinComponent {
val movieReleaseDates = movieService.releaseDates val movieReleaseDates = movieService.releaseDates
val similarMovies = movieService.similar val similarMovies = movieService.similar
val movieAccountStates = movieService.accountStates val movieAccountStates = movieService.accountStates
val movieKeywordResults = movieService.keywordResults
val popularMovies = createPagingFlow( val popularMovies = createPagingFlow(
fetcher = { p -> movieService.getPopular(p) }, fetcher = { p -> movieService.getPopular(p) },
@@ -74,6 +76,7 @@ class MainViewModel: ViewModel(), KoinComponent {
val tvSeasons = tvService.seasons val tvSeasons = tvService.seasons
val similarTv = tvService.similar val similarTv = tvService.similar
val tvAccountStates = tvService.accountStates val tvAccountStates = tvService.accountStates
val tvKeywordResults = tvService.keywordResults
val popularTv = createPagingFlow( val popularTv = createPagingFlow(
fetcher = { p -> tvService.getPopular(p) }, fetcher = { p -> tvService.getPopular(p) },
@@ -155,7 +158,7 @@ class MainViewModel: ViewModel(), KoinComponent {
return providesForType(type, { movieAccountStates }, { tvAccountStates} ) return providesForType(type, { movieAccountStates }, { tvAccountStates} )
} }
fun produceFlowFor(mediaType: MediaViewType, contentType: MediaTabNavItem.Type): Flow<PagingData<TmdbItem>> { fun produceMediaTabFlowFor(mediaType: MediaViewType, contentType: MediaTabNavItem.Type): Flow<PagingData<TmdbItem>> {
return providesForType( return providesForType(
mediaType, mediaType,
{ {
@@ -177,6 +180,10 @@ class MainViewModel: ViewModel(), KoinComponent {
) )
} }
fun produceKeywordsResultsFor(mediaType: MediaViewType): Map<Int, Flow<PagingData<SearchResultMedia>>> {
return providesForType(mediaType, { movieKeywordResults }, { tvKeywordResults })
}
suspend fun getById(id: Int, type: MediaViewType) { suspend fun getById(id: Int, type: MediaViewType) {
when (type) { when (type) {
MediaViewType.MOVIE -> movieService.getById(id) MediaViewType.MOVIE -> movieService.getById(id)
@@ -287,6 +294,24 @@ class MainViewModel: ViewModel(), KoinComponent {
} }
} }
fun getKeywordResults(id: Int, keyword: String, type: MediaViewType) {
when (type) {
MediaViewType.MOVIE -> {
movieKeywordResults[id] = createPagingFlow(
fetcher = { p -> movieService.discover(keyword, p) },
processor = { it.results }
)
}
MediaViewType.TV -> {
tvKeywordResults[id] = createPagingFlow(
fetcher = { p -> tvService.discover(keyword, p) },
processor = { it.results }
)
}
else -> {}
}
}
suspend fun getReleaseDates(id: Int) { suspend fun getReleaseDates(id: Int) {
movieService.getReleaseDates(id) movieService.getReleaseDates(id)
} }

View File

@@ -9,4 +9,7 @@ object NavConstants {
const val ACCOUNT_KEY = "account_key" const val ACCOUNT_KEY = "account_key"
const val AUTH_REDIRECT_PAGE = "return" const val AUTH_REDIRECT_PAGE = "return"
const val WEB_LINK_KEY = "web_link_key" const val WEB_LINK_KEY = "web_link_key"
const val KEYWORD_TYPE_KEY = "keyword_type_key"
const val KEYWORD_NAME_KEY = "keyword_name_key"
const val KEYWORD_ID_KEY = "keyword_id_key"
} }