add video card to details screen

This commit is contained in:
Owen LeJeune
2022-02-22 22:37:20 -05:00
parent d13e31d77f
commit 4b5ca49982
6 changed files with 209 additions and 129 deletions

View File

@@ -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
)
) {
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)
}
}

View File

@@ -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,

View File

@@ -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
)
}
}
}
}

View File

@@ -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
// }
// )
// }
}

View File

@@ -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<VideoResponse?>(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<Video>, type: Video.Type, title: String) {
val videos = results.filter { it.isOfficial && it.type == type }
if (videos.isNotEmpty()) {
Text(
text = title,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 12.dp, top = 8.dp)
)
StaticGrid(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp),
cells = StaticGridCells.Dynamic(110.dp),
itemCount = videos.size
) { i ->
val video = videos[i]
FullScreenThumbnailVideoPlayer(
key = video.key,
modifier = Modifier.size(110.dp, 80.dp)
)
}
}
}
private fun fetchMediaItem(id: Int, service: DetailService, mediaItem: MutableState<DetailedItem?>) {
CoroutineScope(Dispatchers.IO).launch {
val response = service.getById(id)