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 package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import com.owenlejeune.tvtime.R
class Video( class Video(
@SerializedName("iso_639_1") val language: String, @SerializedName("iso_639_1") val language: String,
@@ -9,6 +10,19 @@ class Video(
@SerializedName("key") val key: String, @SerializedName("key") val key: String,
@SerializedName("site") val site: String, @SerializedName("site") val site: String,
@SerializedName("size") val size: Int, @SerializedName("size") val size: Int,
@SerializedName("type") val type: String, @SerializedName("type") val type: Type,
@SerializedName("official") val isOfficial: Boolean @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 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.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card import androidx.compose.material.Card
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.rememberImagePainter import coil.compose.rememberImagePainter
import coil.transform.RoundedCornersTransformation import coil.transform.RoundedCornersTransformation
import com.owenlejeune.tvtime.R 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 @Composable
fun ImageTextCard( fun ImageTextCard(
title: String, 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 package com.owenlejeune.tvtime.ui.components
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.util.SparseArray
import android.widget.Toast import android.widget.Toast
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas 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.material.icons.filled.Search
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
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.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties 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 coil.compose.rememberImagePainter
import com.google.accompanist.flowlayout.FlowRow 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.owenlejeune.tvtime.R
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.YouTubePlayerFullScreenListener
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views.YouTubePlayerView import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views.YouTubePlayerView
@Composable @Composable
@@ -441,9 +430,13 @@ fun FullScreenThumbnailVideoPlayer(
onDismissRequest = { showFullscreenView.value = false }, onDismissRequest = { showFullscreenView.value = false },
properties = DialogProperties(usePlatformDefaultWidth = false) properties = DialogProperties(usePlatformDefaultWidth = false)
) { ) {
Surface(modifier = Modifier.fillMaxWidth().wrapContentHeight()) { Surface(modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()) {
AndroidView( AndroidView(
modifier = Modifier.wrapContentHeight().fillMaxWidth(), modifier = Modifier
.wrapContentHeight()
.fillMaxWidth(),
factory = { factory = {
YouTubePlayerView(context).apply { YouTubePlayerView(context).apply {
addYouTubePlayerListener(object : AbstractYouTubePlayerListener() { 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 package com.owenlejeune.tvtime.ui.screens
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
@@ -348,7 +349,7 @@ fun SimilarContentCard(itemId: Int?, service: DetailService, modifier: Modifier
ContentCard( ContentCard(
modifier = modifier, modifier = modifier,
title = "Recommended" title = stringResource(id = R.string.recommended_label)
) { ) {
LazyRow(modifier = Modifier LazyRow(modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -370,6 +371,7 @@ fun SimilarContentCard(itemId: Int?, service: DetailService, modifier: Modifier
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun VideosCard(itemId: Int?, service: DetailService, modifier: Modifier = Modifier) { fun VideosCard(itemId: Int?, service: DetailService, modifier: Modifier = Modifier) {
val videoResponse = remember { mutableStateOf<VideoResponse?>(null) } val videoResponse = remember { mutableStateOf<VideoResponse?>(null) }
@@ -379,45 +381,58 @@ fun VideosCard(itemId: Int?, service: DetailService, modifier: Modifier = Modifi
} }
} }
ContentCard( if (videoResponse.value != null) {
modifier = modifier, val results = videoResponse.value!!.results
title = "Trailers" ExpandableContentCard(
) { modifier = modifier,
LazyRow( title = {
modifier = Modifier Text(
.fillMaxWidth() text = stringResource(id = R.string.videos_label),
.wrapContentHeight() style = MaterialTheme.typography.titleLarge,
.padding(vertical = 12.dp, horizontal = 8.dp) modifier = Modifier.padding(start = 12.dp, top = 8.dp),
) { color = MaterialTheme.colorScheme.onSurfaceVariant
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 -> toggleTextColor = MaterialTheme.colorScheme.primary
val video = videos!![i] ) { isExpanded ->
VideoGroup(results = results, type = Video.Type.TRAILER, title = stringResource(id = Video.Type.TRAILER.stringRes))
Column( if (isExpanded) {
modifier = Modifier.wrapContentHeight().width(152.dp) Video.Type.values().filter { it != Video.Type.TRAILER}.forEach { type ->
) { VideoGroup(results = results, type = type, title = stringResource(id = type.stringRes))
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
)
} }
} }
} }
} }
} }
@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?>) { private fun fetchMediaItem(id: Int, service: DetailService, mediaItem: MutableState<DetailedItem?>) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val response = service.getById(id) val response = service.getById(id)

View File

@@ -15,6 +15,11 @@
<!-- --> <!-- -->
<string name="cast_label">Cast</string> <string name="cast_label">Cast</string>
<string name="recommended_label">Recommended</string>
<string name="videos_label">Videos</string>
<string name="expandable_see_more">See more</string>
<string name="expandable_see_less">See less</string>
<!-- preferences --> <!-- preferences -->
<string name="preference_heading_search">Search</string> <string name="preference_heading_search">Search</string>
@@ -23,4 +28,11 @@
<string name="preferences_hide_heading_title">Expanded search bar</string> <string name="preferences_hide_heading_title">Expanded search bar</string>
<string name="preferences_hide_heading_subtitle">Keep search bar expanded at all times</string> <string name="preferences_hide_heading_subtitle">Keep search bar expanded at all times</string>
<string name="preferences_debug_title">Developer options</string> <string name="preferences_debug_title">Developer options</string>
<!-- video type -->
<string name="video_type_clip">Clips</string>
<string name="video_type_trailer">Trailers</string>
<string name="video_type_teaser">Teasers</string>
<string name="video_type_behind_the_scenes">Behind the Scenes</string>
<string name="video_type_featureette">Featurettes</string>
</resources> </resources>