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 '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}"

View File

@@ -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" />

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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