add support for crunchyroll media playback in player
This commit is contained in:
parent
236ca9a6c9
commit
7dc41da13c
@ -83,8 +83,8 @@ data class Episode(
|
||||
@SerialName("episode") val episode: String,
|
||||
@SerialName("episode_number") val episodeNumber: Int,
|
||||
@SerialName("description") val description: String,
|
||||
@SerialName("next_episode_id") val nextEpisodeId: String = "", // use default value since the field is optional
|
||||
@SerialName("next_episode_title") val nextEpisodeTitle: 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? = null, // default/nullable value since optional
|
||||
@SerialName("is_subbed") val isSubbed: Boolean,
|
||||
@SerialName("is_dubbed") val isDubbed: Boolean,
|
||||
@SerialName("images") val images: Thumbnail,
|
||||
|
@ -29,6 +29,7 @@ import kotlinx.android.synthetic.main.activity_player.*
|
||||
import kotlinx.android.synthetic.main.player_controls.*
|
||||
import kotlinx.coroutines.launch
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.parser.crunchyroll.NoneEpisode
|
||||
import org.mosad.teapod.preferences.Preferences
|
||||
import org.mosad.teapod.ui.components.EpisodesListPlayer
|
||||
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_episode_id)) ?: ""
|
||||
)
|
||||
model.playEpisode(model.currentEpisode.mediaId, replace = true)
|
||||
model.playCurrentMedia()
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,7 +172,7 @@ class PlayerActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun initPlayer() {
|
||||
if (model.media.aodId < 0) {
|
||||
if (model.currentEpisode.equals(NoneEpisode)) {
|
||||
Log.e(javaClass.name, "No media was set.")
|
||||
this.finish()
|
||||
}
|
||||
@ -206,14 +207,14 @@ class PlayerActivity : AppCompatActivity() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// start playing the current episode, after all needed player components have been initialized
|
||||
model.playEpisode(model.currentEpisode.mediaId, true)
|
||||
model.playCurrentMedia()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@ -251,9 +252,10 @@ class PlayerActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun initGUI() {
|
||||
if (model.media.type == DataTypes.MediaType.MOVIE) {
|
||||
button_episodes.visibility = View.GONE
|
||||
}
|
||||
// TODO reimplement for cr
|
||||
// if (model.media.type == DataTypes.MediaType.MOVIE) {
|
||||
// button_episodes.visibility = View.GONE
|
||||
// }
|
||||
}
|
||||
|
||||
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:
|
||||
// show next ep button
|
||||
if (remainingTime in 1..20000) {
|
||||
if (!btnNextEpIsVisible && model.nextEpisodeId != null && Preferences.autoplay && !isInPiPMode()) {
|
||||
if (!btnNextEpIsVisible && model.currentEpisodeCr.nextEpisodeId != null && Preferences.autoplay && !isInPiPMode()) {
|
||||
showButtonNextEp()
|
||||
}
|
||||
} else if (btnNextEpIsVisible) {
|
||||
@ -335,18 +337,19 @@ class PlayerActivity : AppCompatActivity() {
|
||||
exo_text_title.text = model.getMediaTitle()
|
||||
|
||||
// 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
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
|
||||
// TODO reimplement for cr
|
||||
// hide the episodes button, if the media type changed
|
||||
button_episodes.visibility = if (model.media.type == DataTypes.MediaType.MOVIE) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
// button_episodes.visibility = if (model.media.type == DataTypes.MediaType.MOVIE) {
|
||||
// View.GONE
|
||||
// } else {
|
||||
// View.VISIBLE
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -5,26 +5,23 @@ import android.net.Uri
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.util.Log
|
||||
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.SimpleExoPlayer
|
||||
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.upstream.DefaultDataSourceFactory
|
||||
import com.google.android.exoplayer2.util.Util
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.parser.AoDParser
|
||||
import org.mosad.teapod.parser.crunchyroll.Crunchyroll
|
||||
import org.mosad.teapod.parser.crunchyroll.NoneEpisode
|
||||
import org.mosad.teapod.parser.crunchyroll.NoneEpisodes
|
||||
import org.mosad.teapod.parser.crunchyroll.NonePlayback
|
||||
import org.mosad.teapod.preferences.Preferences
|
||||
import org.mosad.teapod.util.*
|
||||
import org.mosad.teapod.util.tmdb.TMDBApiController
|
||||
import org.mosad.teapod.util.AoDEpisodeNone
|
||||
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 java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
@ -43,8 +40,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
val currentEpisodeChangedListener = ArrayList<() -> Unit>()
|
||||
private val preferredLanguage = if (Preferences.preferSecondary) Locale.JAPANESE else Locale.GERMAN
|
||||
|
||||
var media: AoDMedia = AoDMediaNone
|
||||
internal set
|
||||
// var media: AoDMedia = AoDMediaNone
|
||||
// internal set
|
||||
var mediaMeta: Meta? = null
|
||||
internal set
|
||||
var tmdbTVSeason: TMDBTVSeason? =null
|
||||
@ -53,8 +50,8 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
internal set
|
||||
var currentEpisodeMeta: EpisodeMeta? = null
|
||||
internal set
|
||||
var nextEpisodeId: Int? = null
|
||||
internal set
|
||||
// var nextEpisodeId: Int? = null
|
||||
// internal set
|
||||
var currentLanguage: Locale = Locale.ROOT
|
||||
internal set
|
||||
|
||||
@ -62,8 +59,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
internal set
|
||||
var currentEpisodeCr = NoneEpisode
|
||||
internal set
|
||||
var currentPlaybackCr = NonePlayback
|
||||
internal set
|
||||
private var currentPlaybackCr = NonePlayback
|
||||
|
||||
init {
|
||||
initMediaSession()
|
||||
@ -94,6 +90,7 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
episodesCrunchy = Crunchyroll.episodes(seasonId)
|
||||
//mediaMeta = loadMediaMeta(media.aodId) // can be done blocking, since it should be cached
|
||||
|
||||
// TODO replace this with setCurrentEpisode
|
||||
currentEpisodeCr = episodesCrunchy.items.find { episode ->
|
||||
episode.id == episodeId
|
||||
} ?: NoneEpisode
|
||||
@ -102,14 +99,15 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
currentPlaybackCr = Crunchyroll.playback(currentEpisodeCr.playback)
|
||||
}
|
||||
|
||||
// TODO reimplement for cr
|
||||
// run async as it should be loaded by the time the episodes a
|
||||
viewModelScope.launch {
|
||||
// get season info, if metaDB knows the tv show
|
||||
if (media.type == DataTypes.MediaType.TVSHOW && mediaMeta != null) {
|
||||
val tvShowMeta = mediaMeta as TVShowMeta
|
||||
tmdbTVSeason = TMDBApiController().getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber)
|
||||
}
|
||||
}
|
||||
// viewModelScope.launch {
|
||||
// // get tmdb season info, if metaDB knows the tv show
|
||||
// if (media.type == DataTypes.MediaType.TVSHOW && mediaMeta != null) {
|
||||
// val tvShowMeta = mediaMeta as TVShowMeta
|
||||
// tmdbTVSeason = TMDBApiController().getTVSeasonDetails(tvShowMeta.tmdbId, tvShowMeta.tmdbSeasonNumber)
|
||||
// }
|
||||
// }
|
||||
|
||||
currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisode.mediaId)
|
||||
currentLanguage = currentEpisode.getPreferredStream(preferredLanguage).language
|
||||
@ -117,12 +115,12 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
|
||||
fun setLanguage(language: Locale) {
|
||||
currentLanguage = language
|
||||
playCurrentMedia(player.currentPosition)
|
||||
|
||||
val seekTime = player.currentPosition
|
||||
val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(
|
||||
MediaItem.fromUri(Uri.parse(currentEpisode.getPreferredStream(language).url))
|
||||
)
|
||||
playMedia(mediaSource, true, seekTime)
|
||||
// val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(
|
||||
// MediaItem.fromUri(Uri.parse(currentEpisode.getPreferredStream(language).url))
|
||||
// )
|
||||
// playMedia(mediaSource, seekTime)
|
||||
}
|
||||
|
||||
// player actions
|
||||
@ -138,62 +136,70 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
|
||||
/**
|
||||
* play the next episode, if nextEpisode is not null
|
||||
*/
|
||||
fun playNextEpisode() = nextEpisodeId?.let { it ->
|
||||
playEpisode(it, replace = true)
|
||||
fun playNextEpisode() = currentEpisodeCr.nextEpisodeId?.let { nextEpisodeId ->
|
||||
setCurrentEpisode(nextEpisodeId, startPlayback = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set currentEpisode and start playing it.
|
||||
* Update nextEpisode to reflect the change and update
|
||||
* the watched state for the now playing episode.
|
||||
* Set currentEpisodeCr to the episode of the given ID
|
||||
* @param episodeId The ID of the episode you want to set currentEpisodeCr to
|
||||
*/
|
||||
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).
|
||||
*/
|
||||
fun playEpisode(episodeId: Int, replace: Boolean = false, seekPosition: Long = 0) {
|
||||
currentEpisode = media.getEpisodeById(episodeId)
|
||||
currentLanguage = currentEpisode.getPreferredStream(currentLanguage).language
|
||||
currentEpisodeMeta = getEpisodeMetaByAoDMediaId(currentEpisode.mediaId)
|
||||
nextEpisodeId = selectNextEpisode()
|
||||
|
||||
fun playCurrentMedia(seekPosition: Long = 0) {
|
||||
// update player gui (title, next ep button) after nextEpisodeId has been set
|
||||
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(
|
||||
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
|
||||
if (!currentEpisode.watched) {
|
||||
viewModelScope.launch {
|
||||
AoDParser.markAsWatched(media.aodId, currentEpisode.mediaId)
|
||||
}
|
||||
}
|
||||
}
|
||||
// the actual player playback code
|
||||
player.setMediaSource(mediaSource)
|
||||
player.prepare()
|
||||
if (seekPosition > 0) player.seekTo(seekPosition)
|
||||
player.playWhenReady = true
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
// TODO reimplement mark as watched for cr, if needed
|
||||
}
|
||||
|
||||
fun getMediaTitle(): String {
|
||||
return if (media.type == DataTypes.MediaType.TVSHOW) {
|
||||
// TODO add tvshow/movie diff
|
||||
val isTVShow = true
|
||||
return if(isTVShow) {
|
||||
getApplication<Application>().getString(
|
||||
R.string.component_episode_title,
|
||||
currentEpisode.numberStr,
|
||||
currentEpisode.description
|
||||
currentEpisodeCr.episode,
|
||||
currentEpisodeCr.title
|
||||
)
|
||||
} 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? {
|
||||
return if (media.type == DataTypes.MediaType.TVSHOW) {
|
||||
MetaDBController().getTVShowMetadata(aodId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
// return if (media.type == DataTypes.MediaType.TVSHOW) {
|
||||
// MetaDBController().getTVShowMetadata(aodId)
|
||||
// } else {
|
||||
// null
|
||||
// }
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO reimplement for cr
|
||||
* Based on the current episodes index, get the next episode.
|
||||
* @return The next episode or null if there is none.
|
||||
*/
|
||||
private fun selectNextEpisode(): Int? {
|
||||
return media.playlist.firstOrNull {
|
||||
it.index > media.getEpisodeById(currentEpisode.mediaId).index
|
||||
}?.mediaId
|
||||
// return media.playlist.firstOrNull {
|
||||
// it.index > media.getEpisodeById(currentEpisode.mediaId).index
|
||||
// }?.mediaId
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
@ -28,12 +28,13 @@ class EpisodesListPlayer @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
model?.let {
|
||||
adapterRecEpisodes = PlayerEpisodeItemAdapter(model.media.playlist, model.tmdbTVSeason?.episodes)
|
||||
adapterRecEpisodes.onImageClick = { _, position ->
|
||||
adapterRecEpisodes = PlayerEpisodeItemAdapter(model.episodesCrunchy, model.tmdbTVSeason?.episodes)
|
||||
adapterRecEpisodes.onImageClick = {_, episodeId ->
|
||||
(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.scrollToPosition(model.currentEpisode.index)
|
||||
|
@ -5,13 +5,15 @@ import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import kotlinx.android.synthetic.main.button_fast_forward.view.*
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.ButtonFastForwardBinding
|
||||
|
||||
class FastForwardButton(context: Context, attrs: AttributeSet?): FrameLayout(context, attrs) {
|
||||
|
||||
private val binding = ButtonFastForwardBinding.inflate(LayoutInflater.from(context))
|
||||
private val animationDuration: Long = 800
|
||||
private val buttonAnimation: ObjectAnimator
|
||||
private val labelAnimation: ObjectAnimator
|
||||
@ -19,30 +21,30 @@ class FastForwardButton(context: Context, attrs: AttributeSet?): FrameLayout(con
|
||||
var onAnimationEndCallback: (() -> Unit)? = null
|
||||
|
||||
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
|
||||
repeatCount = 1
|
||||
repeatMode = ObjectAnimator.REVERSE
|
||||
addListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationStart(animation: Animator?) {
|
||||
imageButton.isEnabled = false // disable button
|
||||
imageButton.setBackgroundResource(R.drawable.ic_baseline_forward_24)
|
||||
binding.imageButton.isEnabled = false // disable button
|
||||
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
|
||||
addListener(object : AnimatorListenerAdapter() {
|
||||
// the label animation takes longer then the button animation, reset stuff in here
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
imageButton.isEnabled = true // enable button
|
||||
imageButton.setBackgroundResource(R.drawable.ic_baseline_forward_10_24)
|
||||
binding.imageButton.isEnabled = true // enable button
|
||||
binding.imageButton.setBackgroundResource(R.drawable.ic_baseline_forward_10_24)
|
||||
|
||||
textView.visibility = View.GONE
|
||||
textView.animate().translationX(0f)
|
||||
binding.textView.visibility = View.GONE
|
||||
binding.textView.animate().translationX(0f)
|
||||
|
||||
onAnimationEndCallback?.invoke()
|
||||
}
|
||||
@ -51,7 +53,7 @@ class FastForwardButton(context: Context, attrs: AttributeSet?): FrameLayout(con
|
||||
}
|
||||
|
||||
fun setOnButtonClickListener(func: FastForwardButton.() -> Unit) {
|
||||
imageButton.setOnClickListener {
|
||||
binding.imageButton.setOnClickListener {
|
||||
func()
|
||||
}
|
||||
}
|
||||
@ -61,7 +63,7 @@ class FastForwardButton(context: Context, attrs: AttributeSet?): FrameLayout(con
|
||||
buttonAnimation.start()
|
||||
|
||||
// run lbl animation
|
||||
textView.visibility = View.VISIBLE
|
||||
binding.textView.visibility = View.VISIBLE
|
||||
labelAnimation.start()
|
||||
}
|
||||
|
||||
|
@ -5,13 +5,15 @@ import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import kotlinx.android.synthetic.main.button_rewind.view.*
|
||||
import org.mosad.teapod.R
|
||||
import org.mosad.teapod.databinding.ButtonRewindBinding
|
||||
|
||||
class RewindButton(context: Context, attrs: AttributeSet): FrameLayout(context, attrs) {
|
||||
|
||||
private val binding = ButtonRewindBinding.inflate(LayoutInflater.from(context))
|
||||
private val animationDuration: Long = 800
|
||||
private val buttonAnimation: ObjectAnimator
|
||||
private val labelAnimation: ObjectAnimator
|
||||
@ -19,29 +21,29 @@ class RewindButton(context: Context, attrs: AttributeSet): FrameLayout(context,
|
||||
var onAnimationEndCallback: (() -> Unit)? = null
|
||||
|
||||
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
|
||||
repeatCount = 1
|
||||
repeatMode = ObjectAnimator.REVERSE
|
||||
addListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationStart(animation: Animator?) {
|
||||
imageButton.isEnabled = false // disable button
|
||||
imageButton.setBackgroundResource(R.drawable.ic_baseline_rewind_24)
|
||||
binding.imageButton.isEnabled = false // disable button
|
||||
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
|
||||
addListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
imageButton.isEnabled = true // enable button
|
||||
imageButton.setBackgroundResource(R.drawable.ic_baseline_rewind_10_24)
|
||||
binding.imageButton.isEnabled = true // enable button
|
||||
binding.imageButton.setBackgroundResource(R.drawable.ic_baseline_rewind_10_24)
|
||||
|
||||
textView.visibility = View.GONE
|
||||
textView.animate().translationX(0f)
|
||||
binding.textView.visibility = View.GONE
|
||||
binding.textView.animate().translationX(0f)
|
||||
|
||||
onAnimationEndCallback?.invoke()
|
||||
}
|
||||
@ -50,7 +52,7 @@ class RewindButton(context: Context, attrs: AttributeSet): FrameLayout(context,
|
||||
}
|
||||
|
||||
fun setOnButtonClickListener(func: RewindButton.() -> Unit) {
|
||||
imageButton.setOnClickListener {
|
||||
binding.imageButton.setOnClickListener {
|
||||
func()
|
||||
}
|
||||
}
|
||||
@ -60,7 +62,7 @@ class RewindButton(context: Context, attrs: AttributeSet): FrameLayout(context,
|
||||
buttonAnimation.start()
|
||||
|
||||
// run lbl animation
|
||||
textView.visibility = View.VISIBLE
|
||||
binding.textView.visibility = View.VISIBLE
|
||||
labelAnimation.start()
|
||||
}
|
||||
|
||||
|
@ -76,7 +76,7 @@ data class AoDEpisode(
|
||||
* @return the preferred stream, if not present use the first stream
|
||||
*/
|
||||
fun getPreferredStream(language: Locale) = streams.firstOrNull { it.language == language }
|
||||
?: streams.first()
|
||||
?: Stream("", Locale.ROOT)
|
||||
}
|
||||
|
||||
data class Stream(
|
||||
@ -112,7 +112,7 @@ val AoDEpisodeNone = AoDEpisode(
|
||||
"",
|
||||
"",
|
||||
-1,
|
||||
false,
|
||||
true,
|
||||
"",
|
||||
mutableListOf()
|
||||
)
|
||||
|
@ -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()) {
|
||||
Glide.with(context).load(ep.images.thumbnail[0][0].source)
|
||||
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
|
||||
|
@ -9,12 +9,12 @@ import com.bumptech.glide.request.RequestOptions
|
||||
import jp.wasabeef.glide.transformations.RoundedCornersTransformation
|
||||
import org.mosad.teapod.R
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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) {
|
||||
val context = holder.binding.root.context
|
||||
val ep = episodes[position]
|
||||
val ep = episodes.items[position]
|
||||
|
||||
val titleText = if (ep.hasDub()) {
|
||||
context.getString(R.string.component_episode_title, ep.numberStr, ep.description)
|
||||
val titleText = if (ep.isDubbed) {
|
||||
context.getString(R.string.component_episode_title, ep.episode, ep.title)
|
||||
} 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.textEpisodeDesc2.text = if (ep.shortDesc.isNotEmpty()) {
|
||||
ep.shortDesc
|
||||
holder.binding.textEpisodeDesc2.text = if (ep.description.isNotEmpty()) {
|
||||
ep.description
|
||||
} else if (tmdbEpisodes != null && position < tmdbEpisodes.size){
|
||||
tmdbEpisodes[position].overview
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
if (ep.imageURL.isNotEmpty()) {
|
||||
Glide.with(context).load(ep.imageURL)
|
||||
if (ep.images.thumbnail[0][0].source.isNotEmpty()) {
|
||||
Glide.with(context).load(ep.images.thumbnail[0][0].source)
|
||||
.apply(RequestOptions.bitmapTransform(RoundedCornersTransformation(10, 0)))
|
||||
.into(holder.binding.imageEpisode)
|
||||
}
|
||||
@ -55,15 +55,18 @@ class PlayerEpisodeItemAdapter(private val episodes: List<AoDEpisode>, private v
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return episodes.size
|
||||
return episodes.items.size
|
||||
}
|
||||
|
||||
inner class EpisodeViewHolder(val binding: ItemEpisodePlayerBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
binding.imageEpisode.setOnClickListener {
|
||||
// don't execute, if it's the current episode
|
||||
if (currentSelected != adapterPosition) {
|
||||
onImageClick?.invoke(episodes[adapterPosition].title, adapterPosition)
|
||||
if (currentSelected != bindingAdapterPosition) {
|
||||
onImageClick?.invoke(
|
||||
episodes.items[bindingAdapterPosition].seasonId,
|
||||
episodes.items[bindingAdapterPosition].id
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user