mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-12-29 10:51:20 -05:00
add screen for movies/tv by keyword from details page
This commit is contained in:
@@ -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>>
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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>>
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>>
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user