mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-22 11:40:54 -05:00
add video card to details screen
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
|
||||
<!-- -->
|
||||
<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 -->
|
||||
<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_subtitle">Keep search bar expanded at all times</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>
|
||||
Reference in New Issue
Block a user