show trailers

This commit is contained in:
Owen LeJeune
2022-02-21 00:05:31 -05:00
parent ac469b081c
commit d13e31d77f
14 changed files with 286 additions and 9 deletions

View File

@@ -85,6 +85,11 @@ dependencies {
implementation "me.onebone:toolbar-compose:2.3.1" 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}" testImplementation "junit:junit:${Versions.junit}"
androidTestImplementation "androidx.test.ext:junit:${Versions.androidx_junit}" androidTestImplementation "androidx.test.ext:junit:${Versions.androidx_junit}"
androidTestImplementation "androidx.test.espresso:espresso-core:${Versions.espresso_core}" androidTestImplementation "androidx.test.espresso:espresso-core:${Versions.espresso_core}"

View File

@@ -11,12 +11,13 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.TVTime"> android:theme="@style/Theme.TVTime"
android:usesCleartextTraffic="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.TVTime" android:theme="@style/Theme.TVTime">
android:screenOrientation="portrait"> <!-- android:screenOrientation="portrait">-->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@@ -1,9 +1,6 @@
package com.owenlejeune.tvtime.api.tmdb package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.CastAndCrew import com.owenlejeune.tvtime.api.tmdb.model.*
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 retrofit2.Response import retrofit2.Response
interface DetailService { interface DetailService {
@@ -16,4 +13,6 @@ interface DetailService {
suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse> suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse>
suspend fun getVideos(id: Int): Response<VideoResponse>
} }

View File

@@ -35,4 +35,7 @@ interface MoviesApi {
@GET("movie/{id}/recommendations") @GET("movie/{id}/recommendations")
suspend fun getSimilarMovies(@Path("id") id: Int, @Query("page") page: Int = 1): Response<HomePageMoviesResponse> 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>
} }

View File

@@ -44,4 +44,8 @@ class MoviesService: KoinComponent, DetailService, HomePageService {
return service.getSimilarMovies(id, page) return service.getSimilarMovies(id, page)
} }
override suspend fun getVideos(id: Int): Response<VideoResponse> {
return service.getVideos(id)
}
} }

View File

@@ -35,4 +35,7 @@ interface TvApi {
@GET("tv/{id}/similar") @GET("tv/{id}/similar")
suspend fun getSimilarTvShows(@Path("id") id: Int, @Query("page") page: Int = 1): Response<HomePageTvResponse> 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>
} }

View File

@@ -43,4 +43,8 @@ class TvService: KoinComponent, DetailService, HomePageService {
override suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse> { override suspend fun getSimilar(id: Int, page: Int): Response<out HomePageResponse> {
return service.getSimilarTvShows(id, page) return service.getSimilarTvShows(id, page)
} }
override suspend fun getVideos(id: Int): Response<VideoResponse> {
return service.getVideos(id)
}
} }

View File

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

View File

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

View File

@@ -1,9 +1,12 @@
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
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.* 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.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.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
@@ -42,7 +48,23 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp 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.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 @Composable
fun TopLevelSwitch( fun TopLevelSwitch(
@@ -387,3 +409,132 @@ fun RoundedTextField(
} }
} }
} }
@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
// }
// )
// }
}

View File

@@ -188,7 +188,9 @@ private fun ContentColumn(
CastCard(itemId = itemId, service = service, modifier = Modifier.padding(bottom = 16.dp)) 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 @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) } val similarContent = remember { mutableStateOf<HomePageResponse?>(null) }
itemId?.let { itemId?.let {
if (similarContent.value == null) { 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?>) { 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)
@@ -435,3 +485,14 @@ 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()
}
}
}
}

View File

@@ -1,6 +1,11 @@
package com.owenlejeune.tvtime.utils package com.owenlejeune.tvtime.utils
import android.content.Context
import android.util.SparseArray
import androidx.compose.ui.text.intl.Locale 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.* import com.owenlejeune.tvtime.api.tmdb.model.*
object TmdbUtils { object TmdbUtils {
@@ -133,4 +138,20 @@ object TmdbUtils {
return detailItem.voteAverage / 10f 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 ""
}
} }

View File

@@ -2,6 +2,7 @@ buildscript {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven { url "https://jitpack.io" }
dependencies { dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
@@ -14,6 +15,7 @@ subprojects {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven { url "https://jitpack.io" }
} }
} }

View File

@@ -3,6 +3,7 @@ pluginManagement {
gradlePluginPortal() gradlePluginPortal()
google() google()
mavenCentral() mavenCentral()
maven { url "https://jitpack.io" }
} }
} }
dependencyResolutionManagement { dependencyResolutionManagement {
@@ -10,6 +11,7 @@ dependencyResolutionManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven { url "https://jitpack.io" }
} }
} }
rootProject.name = "TVTime" rootProject.name = "TVTime"