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") 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, | ||||||
|  | |||||||
| @ -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 | ||||||
|         } | //        } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -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 | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
| @ -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) | ||||||
|  | |||||||
| @ -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() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | |||||||
| @ -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() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | |||||||
| @ -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() | ||||||
| ) | ) | ||||||
|  | |||||||
| @ -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))) | ||||||
|  | |||||||
| @ -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 | ||||||
|  |                     ) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user