mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-08 04:32:43 -05:00
show trailers
This commit is contained in:
@@ -85,6 +85,11 @@ dependencies {
|
||||
|
||||
implementation "me.onebone:toolbar-compose:2.3.1"
|
||||
|
||||
implementation 'com.google.android.exoplayer:exoplayer:2.16.1'
|
||||
implementation 'com.github.HaarigerHarald:android-youtubeExtractor:v2.1.0'
|
||||
implementation 'com.pierfrancescosoffritti.androidyoutubeplayer:core:11.0.1'
|
||||
implementation 'com.pierfrancescosoffritti.androidyoutubeplayer:chromecast-sender:0.26'
|
||||
|
||||
testImplementation "junit:junit:${Versions.junit}"
|
||||
androidTestImplementation "androidx.test.ext:junit:${Versions.androidx_junit}"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:${Versions.espresso_core}"
|
||||
|
||||
@@ -11,12 +11,13 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.TVTime">
|
||||
android:theme="@style/Theme.TVTime"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.TVTime"
|
||||
android:screenOrientation="portrait">
|
||||
android:theme="@style/Theme.TVTime">
|
||||
<!-- android:screenOrientation="portrait">-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package com.owenlejeune.tvtime.api.tmdb
|
||||
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.CastAndCrew
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.ImageCollection
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.HomePageResponse
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.*
|
||||
import retrofit2.Response
|
||||
|
||||
interface DetailService {
|
||||
@@ -16,4 +13,6 @@ interface DetailService {
|
||||
|
||||
suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse>
|
||||
|
||||
suspend fun getVideos(id: Int): Response<VideoResponse>
|
||||
|
||||
}
|
||||
@@ -35,4 +35,7 @@ interface MoviesApi {
|
||||
@GET("movie/{id}/recommendations")
|
||||
suspend fun getSimilarMovies(@Path("id") id: Int, @Query("page") page: Int = 1): Response<HomePageMoviesResponse>
|
||||
|
||||
@GET("movie/{id}/videos")
|
||||
suspend fun getVideos(@Path("id") id: Int): Response<VideoResponse>
|
||||
|
||||
}
|
||||
@@ -44,4 +44,8 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
|
||||
return service.getSimilarMovies(id, page)
|
||||
}
|
||||
|
||||
override suspend fun getVideos(id: Int): Response<VideoResponse> {
|
||||
return service.getVideos(id)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -35,4 +35,7 @@ interface TvApi {
|
||||
@GET("tv/{id}/similar")
|
||||
suspend fun getSimilarTvShows(@Path("id") id: Int, @Query("page") page: Int = 1): Response<HomePageTvResponse>
|
||||
|
||||
@GET("tv/{id}/videos")
|
||||
suspend fun getVideos(@Path("id") id: Int): Response<VideoResponse>
|
||||
|
||||
}
|
||||
@@ -43,4 +43,8 @@ class TvService: KoinComponent, DetailService, HomePageService {
|
||||
override suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse> {
|
||||
return service.getSimilarTvShows(id, page)
|
||||
}
|
||||
|
||||
override suspend fun getVideos(id: Int): Response<VideoResponse> {
|
||||
return service.getVideos(id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.owenlejeune.tvtime.api.tmdb.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class Video(
|
||||
@SerializedName("iso_639_1") val language: String,
|
||||
@SerializedName("iso_3166_1") val region: String,
|
||||
@SerializedName("name") val name: String,
|
||||
@SerializedName("key") val key: String,
|
||||
@SerializedName("site") val site: String,
|
||||
@SerializedName("size") val size: Int,
|
||||
@SerializedName("type") val type: String,
|
||||
@SerializedName("official") val isOfficial: Boolean
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.owenlejeune.tvtime.api.tmdb.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class VideoResponse(
|
||||
@SerializedName("results") val results: List<Video>
|
||||
)
|
||||
@@ -1,9 +1,12 @@
|
||||
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
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -17,8 +20,11 @@ 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
|
||||
@@ -42,7 +48,23 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
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
|
||||
fun TopLevelSwitch(
|
||||
@@ -386,4 +408,133 @@ fun RoundedTextField(
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun FullScreenThumbnailVideoPlayer(
|
||||
key: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val showFullscreenView = remember { mutableStateOf(false) }
|
||||
|
||||
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 = ""
|
||||
)
|
||||
|
||||
if (showFullscreenView.value) {
|
||||
Dialog(
|
||||
onDismissRequest = { showFullscreenView.value = false },
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
) {
|
||||
Surface(modifier = Modifier.fillMaxWidth().wrapContentHeight()) {
|
||||
AndroidView(
|
||||
modifier = Modifier.wrapContentHeight().fillMaxWidth(),
|
||||
factory = {
|
||||
YouTubePlayerView(context).apply {
|
||||
addYouTubePlayerListener(object : AbstractYouTubePlayerListener() {
|
||||
override fun onReady(youTubePlayer: YouTubePlayer) {
|
||||
youTubePlayer.loadVideo(key, 0f)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
// 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
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
}
|
||||
@@ -188,7 +188,9 @@ private fun ContentColumn(
|
||||
|
||||
CastCard(itemId = itemId, service = service, modifier = Modifier.padding(bottom = 16.dp))
|
||||
|
||||
SimilarContent(itemId = itemId, service = service)//, modifier = Modifier.padding(bottom = 16.dp))
|
||||
SimilarContentCard(itemId = itemId, service = service, modifier = Modifier.padding(bottom = 16.dp))
|
||||
|
||||
VideosCard(itemId = itemId, service = service)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,7 +338,7 @@ private fun CastCrewCard(person: Person) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimilarContent(itemId: Int?, service: DetailService, modifier: Modifier = Modifier) {
|
||||
fun SimilarContentCard(itemId: Int?, service: DetailService, modifier: Modifier = Modifier) {
|
||||
val similarContent = remember { mutableStateOf<HomePageResponse?>(null) }
|
||||
itemId?.let {
|
||||
if (similarContent.value == null) {
|
||||
@@ -368,6 +370,54 @@ fun SimilarContent(itemId: Int?, service: DetailService, modifier: Modifier = Mo
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VideosCard(itemId: Int?, service: DetailService, modifier: Modifier = Modifier) {
|
||||
val videoResponse = remember { mutableStateOf<VideoResponse?>(null) }
|
||||
itemId?.let {
|
||||
if (videoResponse.value == null) {
|
||||
fetchVideos(itemId, service, videoResponse)
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchMediaItem(id: Int, service: DetailService, mediaItem: MutableState<DetailedItem?>) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val response = service.getById(id)
|
||||
@@ -434,4 +484,15 @@ private fun fetchSimilarContent(id: Int, service: DetailService, similarContent:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchVideos(id: Int, service: DetailService, videoResponse: MutableState<VideoResponse?>) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val results = service.getVideos(id)
|
||||
if (results.isSuccessful) {
|
||||
withContext(Dispatchers.Main) {
|
||||
videoResponse.value = results.body()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
package com.owenlejeune.tvtime.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.util.SparseArray
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import at.huber.youtubeExtractor.VideoMeta
|
||||
import at.huber.youtubeExtractor.YouTubeExtractor
|
||||
import at.huber.youtubeExtractor.YtFile
|
||||
import com.owenlejeune.tvtime.api.tmdb.model.*
|
||||
|
||||
object TmdbUtils {
|
||||
@@ -133,4 +138,20 @@ object TmdbUtils {
|
||||
return detailItem.voteAverage / 10f
|
||||
}
|
||||
|
||||
fun getFullVideoUrl(video: Video): String {
|
||||
// object: YouTubeExtractor(context) {
|
||||
// override fun onExtractionComplete(
|
||||
// ytFiles: SparseArray<YtFile>?,
|
||||
// videoMeta: VideoMeta?
|
||||
// ) {
|
||||
// if (ytFiles != null) {
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
if (video.site == "YouTube") {
|
||||
return "http://www.youtube.com/watch?v=${video.key}"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url "https://jitpack.io" }
|
||||
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
|
||||
@@ -14,6 +15,7 @@ subprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ pluginManagement {
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
@@ -10,6 +11,7 @@ dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
}
|
||||
rootProject.name = "TVTime"
|
||||
|
||||
Reference in New Issue
Block a user