diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/DetailService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/DetailService.kt index bac5452..f819260 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/DetailService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/DetailService.kt @@ -21,4 +21,6 @@ interface DetailService { suspend fun deleteRating(id: Int): Response + suspend fun getKeywords(id: Int): Response + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt index c31bfb0..0d4af9e 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt @@ -39,6 +39,9 @@ interface MoviesApi { @GET("movie/{id}/reviews") suspend fun getReviews(@Path("id") id: Int): Response + @GET("movie/{id}/keywords") + suspend fun getKeywords(@Path("id") id: Int): Response + @POST("movie/{id}/rating") suspend fun postMovieRatingAsGuest( @Path("id") id: Int, diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt index 0609099..212dffd 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt @@ -71,4 +71,8 @@ class MoviesService: KoinComponent, DetailService, HomePageService { } } + override suspend fun getKeywords(id: Int): Response { + return movieService.getKeywords(id) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvApi.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvApi.kt index 44d8754..d0a3c19 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvApi.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvApi.kt @@ -39,6 +39,9 @@ interface TvApi { @GET("tv/{id}/reviews") suspend fun getReviews(@Path("id") id: Int): Response + @GET("tv/{id}/keywords") + suspend fun getKeywords(@Path("id") id: Int): Response + @POST("tv/{id}/rating") suspend fun postTvRatingAsGuest( @Path("id") id: Int, diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvService.kt index 9db10a2..e769517 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvService.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TvService.kt @@ -71,4 +71,8 @@ class TvService: KoinComponent, DetailService, HomePageService { } } + override suspend fun getKeywords(id: Int): Response { + return service.getKeywords(id) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Keyword.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Keyword.kt new file mode 100644 index 0000000..117d05d --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Keyword.kt @@ -0,0 +1,8 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +class Keyword( + @SerializedName("id") val id: Int, + @SerializedName("name") val name: String +) diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/KeywordsResponse.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/KeywordsResponse.kt new file mode 100644 index 0000000..e7f84df --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/KeywordsResponse.kt @@ -0,0 +1,8 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +class KeywordsResponse( + @SerializedName("id") val id: Int, + @SerializedName("keywords") val keywords: List? +) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt index bfa49bd..df344ec 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Widgets.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.painter.BrushPainter import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInput @@ -267,15 +268,21 @@ fun MinLinesText( ) } +sealed class ChipStyle(val mainAxisSpacing: Dp, val crossAxisSpacing: Dp) { + object Boxy: ChipStyle(8.dp, 4.dp) + object Rounded: ChipStyle(4.dp, 4.dp) + class Mixed(val predicate: (String) -> ChipStyle): ChipStyle(8.dp, 4.dp) +} + @Composable -fun Chip( +fun BoxyChip( text: String, style: TextStyle = MaterialTheme.typography.bodySmall, isSelected: Boolean = true, onSelectionChanged: (String) -> Unit = {} ) { Surface( - modifier = Modifier.padding(4.dp), +// modifier = Modifier.padding(4.dp), shadowElevation = 8.dp, shape = RoundedCornerShape(5.dp), color = if (isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.secondary @@ -299,20 +306,75 @@ fun Chip( } } +@Composable +fun RoundedChip( + text: String, + style: TextStyle = MaterialTheme.typography.bodySmall, + isSelected: Boolean = false, + onSelectionChanged: (String) -> Unit = {} +) { + val borderColor = if (isSelected) MaterialTheme.colorScheme.inverseSurface else MaterialTheme.colorScheme.onSurfaceVariant + val radius = style.fontSize.value.dp * 2 + Surface( + border = BorderStroke(width = 1.dp, borderColor), + shape = RoundedCornerShape(radius), + color = MaterialTheme.colorScheme.surfaceVariant + ) { + Row( + modifier = Modifier + .toggleable( + value = isSelected, + onValueChange = { + onSelectionChanged(text) + } + ) + .padding(8.dp) + ) { + Text( + text = text, + style = style, + color = if (isSelected) MaterialTheme.colorScheme.inverseSurface else MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + @Composable fun ChipGroup( modifier: Modifier = Modifier, chips: List = emptyList(), onSelectedChanged: (String) -> Unit = {}, + chipStyle: ChipStyle = ChipStyle.Boxy ) { + + @Composable + fun DrawChip(chipStyle: ChipStyle, chip: String) { + when (chipStyle) { + ChipStyle.Boxy -> { + BoxyChip( + text = chip, + onSelectionChanged = onSelectedChanged + ) + } + ChipStyle.Rounded -> { + RoundedChip( + text = chip, + onSelectionChanged = onSelectedChanged + ) + } + is ChipStyle.Mixed -> { + DrawChip(chipStyle = chipStyle.predicate(chip), chip = chip) + } + } + } + FlowRow( - modifier = modifier + modifier = modifier, + crossAxisSpacing = 4.dp, + mainAxisSpacing = chipStyle.mainAxisSpacing ) { chips.forEach { chip -> - Chip( - text = chip, - onSelectionChanged = onSelectedChanged - ) + DrawChip(chipStyle = chipStyle, chip = chip) } } } @@ -320,7 +382,7 @@ fun ChipGroup( @Preview @Composable fun ChipPreview() { - Chip("Test Chip") + BoxyChip("Test Chip") } /** @@ -558,7 +620,9 @@ fun CircleBackgroundColorImage( .background(color = backgroundColor) ) { val mod = if (imageHeight != null) { - Modifier.align(imageAlignment).height(height = imageHeight) + Modifier + .align(imageAlignment) + .height(height = imageHeight) } else { Modifier.align(imageAlignment) } @@ -596,7 +660,9 @@ fun AvatarImage( .background(color = MaterialTheme.colorScheme.tertiary) ) { Text( - modifier = Modifier.fillMaxSize().padding(top = size/5), + modifier = Modifier + .fillMaxSize() + .padding(top = size / 5), text = if (author.name.isNotEmpty()) author.name[0].uppercase() else author.username[0].toString(), color = MaterialTheme.colorScheme.onTertiary, textAlign = TextAlign.Center, diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/DetailView.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/DetailView.kt index c54fbcc..3dbc732 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/DetailView.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/DetailView.kt @@ -401,10 +401,8 @@ private fun ContentColumn( MiscTvDetails(mediaItem = mediaItem, service as TvService) } - ActionsView(itemId = itemId, type = mediaType, service = service) - if (mediaItem.value?.overview?.isNotEmpty() == true) { - OverviewCard(mediaItem = mediaItem) + OverviewCard(itemId = itemId!!, mediaItem.value!!.overview!!, service) } CastCard(itemId = itemId, service = service, appNavController = appNavController) @@ -412,6 +410,8 @@ private fun ContentColumn( SimilarContentCard(itemId = itemId, service = service, mediaType = mediaType, appNavController = appNavController) VideosCard(itemId = itemId, service = service) + + ActionsView(itemId = itemId, type = mediaType, service = service) ReviewsCard(itemId = itemId, service = service) } @@ -653,19 +653,43 @@ private fun RatingDialog(showDialog: MutableState, onValueConfirmed: (F } @Composable -private fun OverviewCard(mediaItem: MutableState, modifier: Modifier = Modifier) { +private fun OverviewCard(itemId: Int, overview: String, service: DetailService, modifier: Modifier = Modifier) { + val keywordResponse = remember { mutableStateOf(null) } + if (keywordResponse.value == null) { + fetchKeywords(itemId, service, keywordResponse) + } + val context = LocalContext.current + ContentCard( modifier = modifier ) { - Text( + Column( modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding(vertical = 12.dp, horizontal = 16.dp), - text = mediaItem.value?.overview ?: "", - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium - ) + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = overview, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium + ) + + + keywordResponse.value?.keywords?.let { keywords -> + val names = keywords.map { it.name } + ChipGroup( + chips = names, + chipStyle = ChipStyle.Rounded, + onSelectedChanged = { chip -> + if (service is MoviesService) { +// Toast.makeText(context, chip, Toast.LENGTH_SHORT).show() + } + } + ) + } + } } } @@ -1085,4 +1109,15 @@ private fun fetchReviews(id: Int, service: DetailService, reviewResponse: Mutabl } } } +} + +private fun fetchKeywords(id: Int, service: DetailService, keywordsResponse: MutableState) { + CoroutineScope(Dispatchers.IO).launch { + val result = service.getKeywords(id) + if (result.isSuccessful) { + withContext(Dispatchers.Main) { + keywordsResponse.value = result.body() + } + } + } } \ No newline at end of file