diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Video.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Video.kt index ba0332b..e84fc1c 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Video.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/Video.kt @@ -1,6 +1,7 @@ package com.owenlejeune.tvtime.api.tmdb.model import com.google.gson.annotations.SerializedName +import com.owenlejeune.tvtime.R class Video( @SerializedName("iso_639_1") val language: String, @@ -9,6 +10,19 @@ class Video( @SerializedName("key") val key: String, @SerializedName("site") val site: String, @SerializedName("size") val size: Int, - @SerializedName("type") val type: String, + @SerializedName("type") val type: Type, @SerializedName("official") val isOfficial: Boolean -) \ No newline at end of file +) { + enum class Type(val stringRes: Int) { + @SerializedName("Clip") + CLIP(R.string.video_type_clip), + @SerializedName("Behind the Scenes") + BEHIND_THE_SCENES(R.string.video_type_behind_the_scenes), + @SerializedName("Featurette") + FEATURETTE(R.string.video_type_featureette), + @SerializedName("Teaser") + TEASER(R.string.video_type_teaser), + @SerializedName("Trailer") + TRAILER(R.string.video_type_trailer) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Cards.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Cards.kt index 0c28641..fa8a050 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Cards.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Cards.kt @@ -1,16 +1,22 @@ package com.owenlejeune.tvtime.ui.components +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.tween import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import coil.compose.rememberImagePainter import coil.transform.RoundedCornersTransformation import com.owenlejeune.tvtime.R @@ -46,6 +52,51 @@ fun ContentCard( } } +@Composable +fun ExpandableContentCard( + modifier: Modifier = Modifier, + backgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant, + title: @Composable () -> Unit = {}, + collapsedText: String = stringResource(id = R.string.expandable_see_more), + expandedText: String = stringResource(id = R.string.expandable_see_less), + toggleTextColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + content: @Composable (Boolean) -> Unit = {} +) { + var expandedState by remember { mutableStateOf(false) } + + Card( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight() + .animateContentSize( + animationSpec = tween( + durationMillis = 300, + easing = LinearOutSlowInEasing + ) + ), + shape = RoundedCornerShape(10.dp), + backgroundColor = backgroundColor, + elevation = 8.dp + ) { + Column(modifier = Modifier.fillMaxSize()) { + title() + content(expandedState) + Text( + text = if (expandedState) expandedText else collapsedText, + color = toggleTextColor, + fontSize = 12.sp, + modifier = Modifier + .padding(horizontal = 12.dp, vertical = 8.dp) + .clickable( + onClick = { + expandedState = !expandedState + } + ) + ) + } + } +} + @Composable fun ImageTextCard( title: String, diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/components/Grid.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Grid.kt new file mode 100644 index 0000000..c438461 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/components/Grid.kt @@ -0,0 +1,75 @@ +package com.owenlejeune.tvtime.ui.components + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.math.ceil + +@Composable +private fun StaticGridInternal( + columns: Int, + itemCount: Int, + modifier: Modifier = Modifier, + horizontalSpacing: Dp = 4.dp, + verticalSpacing: Dp = 4.dp, + content: @Composable (Int) -> Unit +) { + val numRows = ceil(itemCount.toFloat() / columns.toFloat()).toInt() + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(verticalSpacing) + ) { + for (i in 0 until numRows) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(horizontalSpacing) + ) { + for (j in 0 until columns) { + if ((columns*i)+j < itemCount) { + content((columns*i)+j) + } + } + } + } + } +} + +sealed class StaticGridCells { + class Fixed(val count: Int): StaticGridCells() + class Dynamic(val minSize: Dp): StaticGridCells() +} + +@Composable +fun StaticGrid( + cells: StaticGridCells, + itemCount: Int, + modifier: Modifier = Modifier, + content: @Composable (Int) -> Unit +) { + when (cells) { + is StaticGridCells.Fixed -> { + StaticGridInternal( + columns = cells.count, + itemCount = itemCount, + verticalSpacing = 4.dp, + horizontalSpacing = 4.dp, + content = content + ) + } + is StaticGridCells.Dynamic -> { + BoxWithConstraints(modifier = modifier) { + val nColumns = maxOf((maxWidth / cells.minSize).toInt(), 1) + val spacing = maxWidth - (cells.minSize * nColumns) + StaticGridInternal( + columns = nColumns, + itemCount = itemCount, + verticalSpacing = 4.dp, + horizontalSpacing = spacing, + content = content + ) + } + } + } +} \ 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 c042966..3cb2301 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 @@ -1,7 +1,6 @@ package com.owenlejeune.tvtime.ui.components import android.content.res.Configuration.UI_MODE_NIGHT_YES -import android.util.SparseArray import android.widget.Toast import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Canvas @@ -20,11 +19,9 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search import androidx.compose.material3.* import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.scale import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -51,19 +48,11 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import at.huber.youtubeExtractor.VideoMeta -import at.huber.youtubeExtractor.YouTubeExtractor -import at.huber.youtubeExtractor.YtFile import coil.compose.rememberImagePainter import com.google.accompanist.flowlayout.FlowRow -import com.google.android.exoplayer2.ExoPlayer -import com.google.android.exoplayer2.MediaItem -import com.google.android.exoplayer2.SimpleExoPlayer -import com.google.android.exoplayer2.ui.PlayerView import com.owenlejeune.tvtime.R import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener -import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.YouTubePlayerFullScreenListener import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views.YouTubePlayerView @Composable @@ -441,9 +430,13 @@ fun FullScreenThumbnailVideoPlayer( onDismissRequest = { showFullscreenView.value = false }, properties = DialogProperties(usePlatformDefaultWidth = false) ) { - Surface(modifier = Modifier.fillMaxWidth().wrapContentHeight()) { + Surface(modifier = Modifier + .fillMaxWidth() + .wrapContentHeight()) { AndroidView( - modifier = Modifier.wrapContentHeight().fillMaxWidth(), + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth(), factory = { YouTubePlayerView(context).apply { addYouTubePlayerListener(object : AbstractYouTubePlayerListener() { @@ -454,87 +447,7 @@ fun FullScreenThumbnailVideoPlayer( } } ) -// Text("big dialog") } } -// AndroidView( -// modifier = modifier, -// factory = { -// val player = YouTubePlayerView(context).apply { -// var ytPlayer: YouTubePlayer? = null -// addYouTubePlayerListener(object : AbstractYouTubePlayerListener() { -// override fun onReady(youTubePlayer: YouTubePlayer) { -// ytPlayer = youTubePlayer -// youTubePlayer.loadVideo(key, 0f) -// } -// }) -// addFullScreenListener(object : YouTubePlayerFullScreenListener { -// override fun onYouTubePlayerEnterFullScreen() { -// ytPlayer?.play() -// } -// -// override fun onYouTubePlayerExitFullScreen() { -// ytPlayer?.pause() -// } -// }) -// } -// -// player.enterFullScreen() -// -// player -// } -// ) } - -// val lifecyclerOwner = LocalLifecycleOwner.current -// val showFullscreenView = remember { mutableStateOf(false) } -// -// if (!showFullscreenView.value) { -// Image( -// modifier = modifier -// .clickable( -// onClick = { -// showFullscreenView.value = true -// } -// ), -// painter = rememberImagePainter( -// data = "https://img.youtube.com/vi/${key}/hqdefault.jpg", -// builder = { -// placeholder(R.drawable.placeholder) -// } -// ), -// contentDescription = "" -// ) -// } else { -// AndroidView( -// modifier = modifier, -// factory = { context -> -// val p = YouTubePlayerView(context).apply { -// var player: YouTubePlayer? = null -//// lifecyclerOwner.lifecycle.addObserver(this) -// addYouTubePlayerListener(object : AbstractYouTubePlayerListener() { -// override fun onReady(youTubePlayer: YouTubePlayer) { -// player = youTubePlayer -// youTubePlayer.loadVideo(key, 0f) -// } -// }) -// addFullScreenListener(object : YouTubePlayerFullScreenListener { -// override fun onYouTubePlayerEnterFullScreen() { -// player?.play() -// } -// -// override fun onYouTubePlayerExitFullScreen() { -// showFullscreenView.value = false -// player?.pause() -// } -// -// }) -// } -// -// p.enterFullScreen() -// -// p -// } -// ) -// } } \ No newline at end of file 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 fe19998..87379b3 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 @@ -1,5 +1,6 @@ package com.owenlejeune.tvtime.ui.screens +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow @@ -348,7 +349,7 @@ fun SimilarContentCard(itemId: Int?, service: DetailService, modifier: Modifier ContentCard( modifier = modifier, - title = "Recommended" + title = stringResource(id = R.string.recommended_label) ) { LazyRow(modifier = Modifier .fillMaxWidth() @@ -370,6 +371,7 @@ fun SimilarContentCard(itemId: Int?, service: DetailService, modifier: Modifier } } +@OptIn(ExperimentalFoundationApi::class) @Composable fun VideosCard(itemId: Int?, service: DetailService, modifier: Modifier = Modifier) { val videoResponse = remember { mutableStateOf(null) } @@ -379,45 +381,58 @@ fun VideosCard(itemId: Int?, service: DetailService, modifier: Modifier = Modifi } } - ContentCard( - modifier = modifier, - title = "Trailers" - ) { - LazyRow( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(vertical = 12.dp, horizontal = 8.dp) - ) { - val videos = videoResponse.value?.results?.filter { it.isOfficial && it.type.lowercase() in listOf("trailer"/*, "teaser"*/) } - val types = videos?.map { it.type } - items(videos?.size ?: 0) { i -> - val video = videos!![i] + if (videoResponse.value != null) { + val results = videoResponse.value!!.results + ExpandableContentCard( + modifier = modifier, + title = { + Text( + text = stringResource(id = R.string.videos_label), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(start = 12.dp, top = 8.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + toggleTextColor = MaterialTheme.colorScheme.primary + ) { isExpanded -> + VideoGroup(results = results, type = Video.Type.TRAILER, title = stringResource(id = Video.Type.TRAILER.stringRes)) - Column( - modifier = Modifier.wrapContentHeight().width(152.dp) - ) { - FullScreenThumbnailVideoPlayer( - key = video.key, - modifier = Modifier - .size(width = 152.dp, height = 108.dp) - .padding(end = 4.dp) - ) - MinLinesText( - modifier = Modifier - .fillMaxWidth() - .padding(top = 5.dp), - minLines = 2, - text = video.name, - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium - ) + if (isExpanded) { + Video.Type.values().filter { it != Video.Type.TRAILER}.forEach { type -> + VideoGroup(results = results, type = type, title = stringResource(id = type.stringRes)) } } } } } +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun VideoGroup(results: List