add support for crunchyroll media playback in player
This commit is contained in:
		| @ -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 | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user