add support for crunchyroll media playback in player

This commit is contained in:
Jannik 2021-12-26 20:22:00 +01:00
parent 919bce65e9
commit 6dac929550
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
9 changed files with 153 additions and 130 deletions

View File

@ -83,8 +83,8 @@ data class Episode(
@SerialName("episode") val episode: String, @SerialName("episode") val episode: String,
@SerialName("episode_number") val episodeNumber: Int, @SerialName("episode_number") val episodeNumber: Int,
@SerialName("description") val description: String, @SerialName("description") val description: String,
@SerialName("next_episode_id") val nextEpisodeId: String = "", // use default value since the field is optional @SerialName("next_episode_id") val nextEpisodeId: String? = null, // default/nullable value since optional
@SerialName("next_episode_title") val nextEpisodeTitle: String = "", // use default value since the field is optional @SerialName("next_episode_title") val nextEpisodeTitle: String? = null, // default/nullable value since optional
@SerialName("is_subbed") val isSubbed: Boolean, @SerialName("is_subbed") val isSubbed: Boolean,
@SerialName("is_dubbed") val isDubbed: Boolean, @SerialName("is_dubbed") val isDubbed: Boolean,
@SerialName("images") val images: Thumbnail, @SerialName("images") val images: Thumbnail,

View File

@ -29,6 +29,7 @@ import kotlinx.android.synthetic.main.activity_player.*
import kotlinx.android.synthetic.main.player_controls.* import kotlinx.android.synthetic.main.player_controls.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.parser.crunchyroll.NoneEpisode
import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.ui.components.EpisodesListPlayer import org.mosad.teapod.ui.components.EpisodesListPlayer
import org.mosad.teapod.ui.components.LanguageSettingsPlayer import org.mosad.teapod.ui.components.LanguageSettingsPlayer
@ -124,7 +125,7 @@ class PlayerActivity : AppCompatActivity() {
it.getStringExtra(getString(R.string.intent_season_id)) ?: "", it.getStringExtra(getString(R.string.intent_season_id)) ?: "",
it.getStringExtra(getString(R.string.intent_episode_id)) ?: "" it.getStringExtra(getString(R.string.intent_episode_id)) ?: ""
) )
model.playEpisode(model.currentEpisode.mediaId, replace = true) model.playCurrentMedia()
} }
} }
@ -171,7 +172,7 @@ class PlayerActivity : AppCompatActivity() {
} }
private fun initPlayer() { private fun initPlayer() {
if (model.media.aodId < 0) { if (model.currentEpisode.equals(NoneEpisode)) {
Log.e(javaClass.name, "No media was set.") Log.e(javaClass.name, "No media was set.")
this.finish() this.finish()
} }
@ -206,14 +207,14 @@ class PlayerActivity : AppCompatActivity() {
else -> View.VISIBLE else -> View.VISIBLE
} }
if (state == ExoPlayer.STATE_ENDED && model.nextEpisodeId != null && Preferences.autoplay) { if (state == ExoPlayer.STATE_ENDED && model.currentEpisodeCr.nextEpisodeId != null && Preferences.autoplay) {
playNextEpisode() playNextEpisode()
} }
} }
}) })
// start playing the current episode, after all needed player components have been initialized // start playing the current episode, after all needed player components have been initialized
model.playEpisode(model.currentEpisode.mediaId, true) model.playCurrentMedia()
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
@ -251,9 +252,10 @@ class PlayerActivity : AppCompatActivity() {
} }
private fun initGUI() { private fun initGUI() {
if (model.media.type == DataTypes.MediaType.MOVIE) { // TODO reimplement for cr
button_episodes.visibility = View.GONE // if (model.media.type == DataTypes.MediaType.MOVIE) {
} // button_episodes.visibility = View.GONE
// }
} }
private fun initTimeUpdates() { private fun initTimeUpdates() {
@ -277,7 +279,7 @@ class PlayerActivity : AppCompatActivity() {
// if remaining time < 20 sec, a next ep is set, autoplay is enabled and not in pip: // if remaining time < 20 sec, a next ep is set, autoplay is enabled and not in pip:
// show next ep button // show next ep button
if (remainingTime in 1..20000) { if (remainingTime in 1..20000) {
if (!btnNextEpIsVisible && model.nextEpisodeId != null && Preferences.autoplay && !isInPiPMode()) { if (!btnNextEpIsVisible && model.currentEpisodeCr.nextEpisodeId != null && Preferences.autoplay && !isInPiPMode()) {
showButtonNextEp() showButtonNextEp()
} }
} else if (btnNextEpIsVisible) { } else if (btnNextEpIsVisible) {
@ -335,18 +337,19 @@ class PlayerActivity : AppCompatActivity() {
exo_text_title.text = model.getMediaTitle() exo_text_title.text = model.getMediaTitle()
// hide the next ep button, if there is none // hide the next ep button, if there is none
button_next_ep_c.visibility = if (model.nextEpisodeId == null) { button_next_ep_c.visibility = if (model.currentEpisodeCr.nextEpisodeId == null) {
View.GONE View.GONE
} else { } else {
View.VISIBLE View.VISIBLE
} }
// TODO reimplement for cr
// hide the episodes button, if the media type changed // hide the episodes button, if the media type changed
button_episodes.visibility = if (model.media.type == DataTypes.MediaType.MOVIE) { // button_episodes.visibility = if (model.media.type == DataTypes.MediaType.MOVIE) {
View.GONE // View.GONE
} else { // } else {
View.VISIBLE // View.VISIBLE
} // }
} }
/** /**

View File

@ -5,26 +5,23 @@ import android.net.Uri
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.util.Log import android.util.Log
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.SimpleExoPlayer import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.util.Util import com.google.android.exoplayer2.util.Util
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.parser.crunchyroll.Crunchyroll import org.mosad.teapod.parser.crunchyroll.Crunchyroll
import org.mosad.teapod.parser.crunchyroll.NoneEpisode import org.mosad.teapod.parser.crunchyroll.NoneEpisode
import org.mosad.teapod.parser.crunchyroll.NoneEpisodes import org.mosad.teapod.parser.crunchyroll.NoneEpisodes
import org.mosad.teapod.parser.crunchyroll.NonePlayback import org.mosad.teapod.parser.crunchyroll.NonePlayback
import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.util.* import org.mosad.teapod.util.AoDEpisodeNone
import org.mosad.teapod.util.tmdb.TMDBApiController import org.mosad.teapod.util.EpisodeMeta
import org.mosad.teapod.util.Meta
import org.mosad.teapod.util.TVShowMeta
import org.mosad.teapod.util.tmdb.TMDBTVSeason import org.mosad.teapod.util.tmdb.TMDBTVSeason
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@ -43,8 +40,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
val currentEpisodeChangedListener = ArrayList<() -> Unit>() val currentEpisodeChangedListener = ArrayList<() -> Unit>()
private val preferredLanguage = if (Preferences.preferSecondary) Locale.JAPANESE else Locale.GERMAN private val preferredLanguage = if (Preferences.preferSecondary) Locale.JAPANESE else Locale.GERMAN
var media: AoDMedia = AoDMediaNone // var media: AoDMedia = AoDMediaNone
internal set // internal set
var mediaMeta: Meta? = null var mediaMeta: Meta? = null
internal set internal set
var tmdbTVSeason: TMDBTVSeason? =null var tmdbTVSeason: TMDBTVSeason? =null
@ -53,8 +50,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
internal set internal set
var currentEpisodeMeta: EpisodeMeta? = null var currentEpisodeMeta: EpisodeMeta? = null
internal set internal set
var nextEpisodeId: Int? = null // var nextEpisodeId: Int? = null
internal set // internal set
var currentLanguage: Locale = Locale.ROOT var currentLanguage: Locale = Locale.ROOT
internal set internal set
@ -62,8 +59,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
internal set internal set
var currentEpisodeCr = NoneEpisode var currentEpisodeCr = NoneEpisode
internal set internal set
var currentPlaybackCr = NonePlayback private var currentPlaybackCr = NonePlayback
internal set
init { init {
initMediaSession() initMediaSession()
@ -94,6 +90,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
episodesCrunchy = Crunchyroll.episodes(seasonId) episodesCrunchy = Crunchyroll.episodes(seasonId)
//mediaMeta = loadMediaMeta(media.aodId) // can be done blocking, since it should be cached //mediaMeta = loadMediaMeta(media.aodId) // can be done blocking, since it should be cached
// TODO replace this with setCurrentEpisode
currentEpisodeCr = episodesCrunchy.items.find { episode -> currentEpisodeCr = episodesCrunchy.items.find { episode ->
episode.id == episodeId episode.id == episodeId
} ?: NoneEpisode } ?: NoneEpisode
@ -102,14 +99,15 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
currentPlaybackCr = Crunchyroll.playback(currentEpisodeCr.playback) currentPlaybackCr = Crunchyroll.playback(currentEpisodeCr.playback)
} }
// TODO reimplement for cr
// run async as it should be loaded by the time the episodes a // run async as it should be loaded by the time the episodes a
viewModelScope.launch { // viewModelScope.launch {
// get season info, if metaDB knows the tv show // // get tmdb season info, if metaDB knows the tv show
if (media.type == DataTypes.MediaType.TVSHOW && mediaMeta != null) { // if (media.type == DataTypes.MediaType.TVSHOW && mediaMeta != null) {
val tvShowMeta = mediaMeta as TVShowMeta // val tvShowMeta = mediaMeta as TVShowMeta
tmdbTVSeason = TMDBApiController().getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber) // tmdbTVSeason = TMDBApiController().getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber)
} // }
} // }
currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisode.mediaId) currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisode.mediaId)
currentLanguage = currentEpisode.getPreferredStream(preferredLanguage).language currentLanguage = currentEpisode.getPreferredStream(preferredLanguage).language
@ -117,12 +115,12 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
fun setLanguage(language: Locale) { fun setLanguage(language: Locale) {
currentLanguage = language currentLanguage = language
playCurrentMedia(player.currentPosition)
val seekTime = player.currentPosition // val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(
val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource( // MediaItem.fromUri(Uri.parse(currentEpisode.getPreferredStream(language).url))
MediaItem.fromUri(Uri.parse(currentEpisode.getPreferredStream(language).url)) // )
) // playMedia(mediaSource, seekTime)
playMedia(mediaSource, true, seekTime)
} }
// player actions // player actions
@ -138,62 +136,70 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
/** /**
* play the next episode, if nextEpisode is not null * play the next episode, if nextEpisode is not null
*/ */
fun playNextEpisode() = nextEpisodeId?.let { it -> fun playNextEpisode() = currentEpisodeCr.nextEpisodeId?.let { nextEpisodeId ->
playEpisode(it, replace = true) setCurrentEpisode(nextEpisodeId, startPlayback = true)
} }
/** /**
* Set currentEpisode and start playing it. * Set currentEpisodeCr to the episode of the given ID
* Update nextEpisode to reflect the change and update * @param episodeId The ID of the episode you want to set currentEpisodeCr to
* the watched state for the now playing episode. */
fun setCurrentEpisode(episodeId: String, startPlayback: Boolean = false) {
currentEpisodeCr = episodesCrunchy.items.find { episode ->
episode.id == episodeId
} ?: NoneEpisode
// TODO don't run blocking
runBlocking {
currentPlaybackCr = Crunchyroll.playback(currentEpisodeCr.playback)
}
// TODO update metadata and language (it should not be needed to update the language here!)
if (startPlayback) {
playCurrentMedia()
}
}
/**
* Play the current media from currentPlaybackCr.
* *
* @param episodeId The aod media id of the episode to play.
* @param replace (default = false)
* @param seekPosition The seek position for the episode (default = 0). * @param seekPosition The seek position for the episode (default = 0).
*/ */
fun playEpisode(episodeId: Int, replace: Boolean = false, seekPosition: Long = 0) { fun playCurrentMedia(seekPosition: Long = 0) {
currentEpisode = media.getEpisodeById(episodeId)
currentLanguage = currentEpisode.getPreferredStream(currentLanguage).language
currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisode.mediaId)
nextEpisodeId = selectNextEpisode()
// update player gui (title, next ep button) after nextEpisodeId has been set // update player gui (title, next ep button) after nextEpisodeId has been set
currentEpisodeChangedListener.forEach { it() } currentEpisodeChangedListener.forEach { it() }
// get preferred stream url TODO implement
val url = currentPlaybackCr.streams.adaptive_hls["en-US"]?.url ?: ""
println("stream url: $url")
// create the media source object
val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource( val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(
MediaItem.fromUri(Uri.parse(currentEpisode.getPreferredStream(currentLanguage).url)) MediaItem.fromUri(Uri.parse(url))
) )
playMedia(mediaSource, replace, seekPosition)
// if episodes has not been watched, mark as watched // the actual player playback code
if (!currentEpisode.watched) { player.setMediaSource(mediaSource)
viewModelScope.launch { player.prepare()
AoDParser.markAsWatched(media.aodId, currentEpisode.mediaId) if (seekPosition > 0) player.seekTo(seekPosition)
} player.playWhenReady = true
}
}
/** // TODO reimplement mark as watched for cr, if needed
* change the players media source and start playback
*/
fun playMedia(source: MediaSource, replace: Boolean = false, seekPosition: Long = 0) {
if (replace || player.contentDuration == C.TIME_UNSET) {
player.setMediaSource(source)
player.prepare()
if (seekPosition > 0) player.seekTo(seekPosition)
player.playWhenReady = true
}
} }
fun getMediaTitle(): String { fun getMediaTitle(): String {
return if (media.type == DataTypes.MediaType.TVSHOW) { // TODO add tvshow/movie diff
val isTVShow = true
return if(isTVShow) {
getApplication<Application>().getString( getApplication<Application>().getString(
R.string.component_episode_title, R.string.component_episode_title,
currentEpisode.numberStr, currentEpisodeCr.episode,
currentEpisode.description currentEpisodeCr.title
) )
} else { } else {
currentEpisode.title // TODO movie
currentEpisodeCr.title
} }
} }
@ -206,22 +212,28 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
} }
} }
// TODO reimplement for cr
private suspend fun loadMediaMeta(aodId: Int): Meta? { private suspend fun loadMediaMeta(aodId: Int): Meta? {
return if (media.type == DataTypes.MediaType.TVSHOW) { // return if (media.type == DataTypes.MediaType.TVSHOW) {
MetaDBController().getTVShowMetadata(aodId) // MetaDBController().getTVShowMetadata(aodId)
} else { // } else {
null // null
} // }
return null
} }
/** /**
* TODO reimplement for cr
* Based on the current episodes index, get the next episode. * Based on the current episodes index, get the next episode.
* @return The next episode or null if there is none. * @return The next episode or null if there is none.
*/ */
private fun selectNextEpisode(): Int? { private fun selectNextEpisode(): Int? {
return media.playlist.firstOrNull { // return media.playlist.firstOrNull {
it.index > media.getEpisodeById(currentEpisode.mediaId).index // it.index > media.getEpisodeById(currentEpisode.mediaId).index
}?.mediaId // }?.mediaId
return null
} }
} }

View File

@ -28,12 +28,13 @@ class EpisodesListPlayer @JvmOverloads constructor(
} }
model?.let { model?.let {
adapterRecEpisodes = PlayerEpisodeItemAdapter(model.media.playlist, model.tmdbTVSeason?.episodes) adapterRecEpisodes = PlayerEpisodeItemAdapter(model.episodesCrunchy, model.tmdbTVSeason?.episodes)
adapterRecEpisodes.onImageClick = { _, position -> adapterRecEpisodes.onImageClick = {_, episodeId ->
(this.parent as ViewGroup).removeView(this) (this.parent as ViewGroup).removeView(this)
model.playEpisode(model.media.playlist[position].mediaId, replace = true) model.setCurrentEpisode(episodeId, startPlayback = true)
} }
adapterRecEpisodes.currentSelected = model.currentEpisode.index // episodeNumber starts at 1, we need the episode index -> - 1
adapterRecEpisodes.currentSelected = (model.currentEpisodeCr.episodeNumber - 1)
binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes binding.recyclerEpisodesPlayer.adapter = adapterRecEpisodes
binding.recyclerEpisodesPlayer.scrollToPosition(model.currentEpisode.index) binding.recyclerEpisodesPlayer.scrollToPosition(model.currentEpisode.index)

View File

@ -5,13 +5,15 @@ import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import kotlinx.android.synthetic.main.button_fast_forward.view.*
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.ButtonFastForwardBinding
class FastForwardButton(context: Context, attrs: AttributeSet?): FrameLayout(context, attrs) { class FastForwardButton(context: Context, attrs: AttributeSet?): FrameLayout(context, attrs) {
private val binding = ButtonFastForwardBinding.inflate(LayoutInflater.from(context))
private val animationDuration: Long = 800 private val animationDuration: Long = 800
private val buttonAnimation: ObjectAnimator private val buttonAnimation: ObjectAnimator
private val labelAnimation: ObjectAnimator private val labelAnimation: ObjectAnimator
@ -19,30 +21,30 @@ class FastForwardButton(context: Context, attrs: AttributeSet?): FrameLayout(con
var onAnimationEndCallback: (() -> Unit)? = null var onAnimationEndCallback: (() -> Unit)? = null
init { init {
inflate(context, R.layout.button_fast_forward, this) addView(binding.root)
buttonAnimation = ObjectAnimator.ofFloat(imageButton, View.ROTATION, 0f, 50f).apply { buttonAnimation = ObjectAnimator.ofFloat(binding.imageButton, View.ROTATION, 0f, 50f).apply {
duration = animationDuration / 4 duration = animationDuration / 4
repeatCount = 1 repeatCount = 1
repeatMode = ObjectAnimator.REVERSE repeatMode = ObjectAnimator.REVERSE
addListener(object : AnimatorListenerAdapter() { addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) { override fun onAnimationStart(animation: Animator?) {
imageButton.isEnabled = false // disable button binding.imageButton.isEnabled = false // disable button
imageButton.setBackgroundResource(R.drawable.ic_baseline_forward_24) binding.imageButton.setBackgroundResource(R.drawable.ic_baseline_forward_24)
} }
}) })
} }
labelAnimation = ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, 35f).apply { labelAnimation = ObjectAnimator.ofFloat(binding.textView, View.TRANSLATION_X, 35f).apply {
duration = animationDuration duration = animationDuration
addListener(object : AnimatorListenerAdapter() { addListener(object : AnimatorListenerAdapter() {
// the label animation takes longer then the button animation, reset stuff in here // the label animation takes longer then the button animation, reset stuff in here
override fun onAnimationEnd(animation: Animator?) { override fun onAnimationEnd(animation: Animator?) {
imageButton.isEnabled = true // enable button binding.imageButton.isEnabled = true // enable button
imageButton.setBackgroundResource(R.drawable.ic_baseline_forward_10_24) binding.imageButton.setBackgroundResource(R.drawable.ic_baseline_forward_10_24)
textView.visibility = View.GONE binding.textView.visibility = View.GONE
textView.animate().translationX(0f) binding.textView.animate().translationX(0f)
onAnimationEndCallback?.invoke() onAnimationEndCallback?.invoke()
} }
@ -51,7 +53,7 @@ class FastForwardButton(context: Context, attrs: AttributeSet?): FrameLayout(con
} }
fun setOnButtonClickListener(func: FastForwardButton.() -> Unit) { fun setOnButtonClickListener(func: FastForwardButton.() -> Unit) {
imageButton.setOnClickListener { binding.imageButton.setOnClickListener {
func() func()
} }
} }
@ -61,7 +63,7 @@ class FastForwardButton(context: Context, attrs: AttributeSet?): FrameLayout(con
buttonAnimation.start() buttonAnimation.start()
// run lbl animation // run lbl animation
textView.visibility = View.VISIBLE binding.textView.visibility = View.VISIBLE
labelAnimation.start() labelAnimation.start()
} }

View File

@ -5,13 +5,15 @@ import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import kotlinx.android.synthetic.main.button_rewind.view.*
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.ButtonRewindBinding
class RewindButton(context: Context, attrs: AttributeSet): FrameLayout(context, attrs) { class RewindButton(context: Context, attrs: AttributeSet): FrameLayout(context, attrs) {
private val binding = ButtonRewindBinding.inflate(LayoutInflater.from(context))
private val animationDuration: Long = 800 private val animationDuration: Long = 800
private val buttonAnimation: ObjectAnimator private val buttonAnimation: ObjectAnimator
private val labelAnimation: ObjectAnimator private val labelAnimation: ObjectAnimator
@ -19,29 +21,29 @@ class RewindButton(context: Context, attrs: AttributeSet): FrameLayout(context,
var onAnimationEndCallback: (() -> Unit)? = null var onAnimationEndCallback: (() -> Unit)? = null
init { init {
inflate(context, R.layout.button_rewind, this) addView(binding.root)
buttonAnimation = ObjectAnimator.ofFloat(imageButton, View.ROTATION, 0f, -50f).apply { buttonAnimation = ObjectAnimator.ofFloat(binding.imageButton, View.ROTATION, 0f, -50f).apply {
duration = animationDuration / 4 duration = animationDuration / 4
repeatCount = 1 repeatCount = 1
repeatMode = ObjectAnimator.REVERSE repeatMode = ObjectAnimator.REVERSE
addListener(object : AnimatorListenerAdapter() { addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) { override fun onAnimationStart(animation: Animator?) {
imageButton.isEnabled = false // disable button binding.imageButton.isEnabled = false // disable button
imageButton.setBackgroundResource(R.drawable.ic_baseline_rewind_24) binding.imageButton.setBackgroundResource(R.drawable.ic_baseline_rewind_24)
} }
}) })
} }
labelAnimation = ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, -35f).apply { labelAnimation = ObjectAnimator.ofFloat(binding.textView, View.TRANSLATION_X, -35f).apply {
duration = animationDuration duration = animationDuration
addListener(object : AnimatorListenerAdapter() { addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) { override fun onAnimationEnd(animation: Animator?) {
imageButton.isEnabled = true // enable button binding.imageButton.isEnabled = true // enable button
imageButton.setBackgroundResource(R.drawable.ic_baseline_rewind_10_24) binding.imageButton.setBackgroundResource(R.drawable.ic_baseline_rewind_10_24)
textView.visibility = View.GONE binding.textView.visibility = View.GONE
textView.animate().translationX(0f) binding.textView.animate().translationX(0f)
onAnimationEndCallback?.invoke() onAnimationEndCallback?.invoke()
} }
@ -50,7 +52,7 @@ class RewindButton(context: Context, attrs: AttributeSet): FrameLayout(context,
} }
fun setOnButtonClickListener(func: RewindButton.() -> Unit) { fun setOnButtonClickListener(func: RewindButton.() -> Unit) {
imageButton.setOnClickListener { binding.imageButton.setOnClickListener {
func() func()
} }
} }
@ -60,7 +62,7 @@ class RewindButton(context: Context, attrs: AttributeSet): FrameLayout(context,
buttonAnimation.start() buttonAnimation.start()
// run lbl animation // run lbl animation
textView.visibility = View.VISIBLE binding.textView.visibility = View.VISIBLE
labelAnimation.start() labelAnimation.start()
} }

View File

@ -76,7 +76,7 @@ data class AoDEpisode(
* @return the preferred stream, if not present use the first stream * @return the preferred stream, if not present use the first stream
*/ */
fun getPreferredStream(language: Locale) = streams.firstOrNull { it.language == language } fun getPreferredStream(language: Locale) = streams.firstOrNull { it.language == language }
?: streams.first() ?: Stream("", Locale.ROOT)
} }
data class Stream( data class Stream(
@ -112,7 +112,7 @@ val AoDEpisodeNone = AoDEpisode(
"", "",
"", "",
-1, -1,
false, true,
"", "",
mutableListOf() mutableListOf()
) )

View File

@ -40,7 +40,7 @@ class EpisodeItemAdapter(private val episodes: Episodes, private val tmdbEpisode
"" ""
} }
// TODO is isNotEmpty() needed? // TODO is isNotEmpty() needed? also in PlayerEpisodeItemAdapter
if (ep.images.thumbnail[0][0].source.isNotEmpty()) { if (ep.images.thumbnail[0][0].source.isNotEmpty()) {
Glide.with(context).load(ep.images.thumbnail[0][0].source) Glide.with(context).load(ep.images.thumbnail[0][0].source)
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY))) .apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))

View File

@ -9,12 +9,12 @@ import com.bumptech.glide.request.RequestOptions
import jp.wasabeef.glide.transformations.RoundedCornersTransformation import jp.wasabeef.glide.transformations.RoundedCornersTransformation
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.ItemEpisodePlayerBinding import org.mosad.teapod.databinding.ItemEpisodePlayerBinding
import org.mosad.teapod.util.AoDEpisode import org.mosad.teapod.parser.crunchyroll.Episodes
import org.mosad.teapod.util.tmdb.TMDBTVEpisode import org.mosad.teapod.util.tmdb.TMDBTVEpisode
class PlayerEpisodeItemAdapter(private val episodes: List<AoDEpisode>, private val tmdbEpisodes: List<TMDBTVEpisode>?) : RecyclerView.Adapter<PlayerEpisodeItemAdapter.EpisodeViewHolder>() { class PlayerEpisodeItemAdapter(private val episodes: Episodes, private val tmdbEpisodes: List<TMDBTVEpisode>?) : RecyclerView.Adapter<PlayerEpisodeItemAdapter.EpisodeViewHolder>() {
var onImageClick: ((String, Int) -> Unit)? = null var onImageClick: ((seasonId: String, episodeId: String) -> Unit)? = null
var currentSelected: Int = -1 // -1, since position should never be < 0 var currentSelected: Int = -1 // -1, since position should never be < 0
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder {
@ -23,25 +23,25 @@ class PlayerEpisodeItemAdapter(private val episodes: List<AoDEpisode>, private v
override fun onBindViewHolder(holder: EpisodeViewHolder, position: Int) { override fun onBindViewHolder(holder: EpisodeViewHolder, position: Int) {
val context = holder.binding.root.context val context = holder.binding.root.context
val ep = episodes[position] val ep = episodes.items[position]
val titleText = if (ep.hasDub()) { val titleText = if (ep.isDubbed) {
context.getString(R.string.component_episode_title, ep.numberStr, ep.description) context.getString(R.string.component_episode_title, ep.episode, ep.title)
} else { } else {
context.getString(R.string.component_episode_title_sub, ep.numberStr, ep.description) context.getString(R.string.component_episode_title_sub, ep.episode, ep.title)
} }
holder.binding.textEpisodeTitle2.text = titleText holder.binding.textEpisodeTitle2.text = titleText
holder.binding.textEpisodeDesc2.text = if (ep.shortDesc.isNotEmpty()) { holder.binding.textEpisodeDesc2.text = if (ep.description.isNotEmpty()) {
ep.shortDesc ep.description
} else if (tmdbEpisodes != null && position < tmdbEpisodes.size){ } else if (tmdbEpisodes != null && position < tmdbEpisodes.size){
tmdbEpisodes[position].overview tmdbEpisodes[position].overview
} else { } else {
"" ""
} }
if (ep.imageURL.isNotEmpty()) { if (ep.images.thumbnail[0][0].source.isNotEmpty()) {
Glide.with(context).load(ep.imageURL) Glide.with(context).load(ep.images.thumbnail[0][0].source)
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0))) .apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
.into(holder.binding.imageEpisode) .into(holder.binding.imageEpisode)
} }
@ -55,15 +55,18 @@ class PlayerEpisodeItemAdapter(private val episodes: List<AoDEpisode>, private v
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
return episodes.size return episodes.items.size
} }
inner class EpisodeViewHolder(val binding: ItemEpisodePlayerBinding) : RecyclerView.ViewHolder(binding.root) { inner class EpisodeViewHolder(val binding: ItemEpisodePlayerBinding) : RecyclerView.ViewHolder(binding.root) {
init { init {
binding.imageEpisode.setOnClickListener { binding.imageEpisode.setOnClickListener {
// don't execute, if it's the current episode // don't execute, if it's the current episode
if (currentSelected != adapterPosition) { if (currentSelected != bindingAdapterPosition) {
onImageClick?.invoke(episodes[adapterPosition].title, adapterPosition) onImageClick?.invoke(
episodes.items[bindingAdapterPosition].seasonId,
episodes.items[bindingAdapterPosition].id
)
} }
} }
} }