implement season selection in MediaFragment
This commit is contained in:
		| @ -80,23 +80,23 @@ data class Seasons( | ||||
|     val total: Int, | ||||
|     val items: List<Season> | ||||
| ) { | ||||
|     fun getPreferredSeasonId(local: Locale): String { | ||||
|     fun getPreferredSeason(local: Locale): Season { | ||||
|         // try to get the the first seasons which matches the preferred local | ||||
|         items.forEach { season -> | ||||
|             if (season.title.startsWith("(${local.language})", true)) { | ||||
|                 return season.id | ||||
|                 return season | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // if there is no season with the preferred local, try to find a subbed season | ||||
|         items.forEach { season -> | ||||
|             if (season.isSubbed) { | ||||
|                 return season.id | ||||
|                 return season | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // if there is no preferred language season and no sub, use the first season | ||||
|         return items.first().id | ||||
|         return items.first() | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -111,6 +111,8 @@ data class Season( | ||||
| ) | ||||
|  | ||||
| val NoneSeasons = Seasons(0, listOf()) | ||||
| val NoneSeason = Season("", "", "", 0, false, false) | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * Episodes data type | ||||
|  | ||||
| @ -1,15 +1,19 @@ | ||||
| package org.mosad.teapod.ui.activity.main.fragments | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.os.Bundle | ||||
| import android.util.Log | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.appcompat.widget.PopupMenu | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.fragment.app.activityViewModels | ||||
| import androidx.lifecycle.lifecycleScope | ||||
| import kotlinx.coroutines.launch | ||||
| import org.mosad.teapod.databinding.FragmentMediaEpisodesBinding | ||||
| import org.mosad.teapod.ui.activity.main.MainActivity | ||||
| import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel | ||||
| import org.mosad.teapod.databinding.FragmentMediaEpisodesBinding | ||||
| import org.mosad.teapod.util.adapter.EpisodeItemAdapter | ||||
|  | ||||
| class MediaFragmentEpisodes : Fragment() { | ||||
| @ -27,15 +31,17 @@ class MediaFragmentEpisodes : Fragment() { | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|  | ||||
|         adapterRecEpisodes = EpisodeItemAdapter(model.episodesCrunchy, model.tmdbTVSeason?.episodes) | ||||
|         adapterRecEpisodes = EpisodeItemAdapter(model.currentEpisodesCrunchy, model.tmdbTVSeason?.episodes) | ||||
|         binding.recyclerEpisodes.adapter = adapterRecEpisodes | ||||
|  | ||||
|         // set onItemClick only in adapter is initialized | ||||
|         if (this::adapterRecEpisodes.isInitialized) { | ||||
|             adapterRecEpisodes.onImageClick = { seasonId, episodeId -> | ||||
|                 println("TODO playback episode $episodeId (season: $seasonId)") | ||||
|                 playEpisode(seasonId, episodeId) | ||||
|             } | ||||
|         // set onItemClick, adapter is initialized | ||||
|         adapterRecEpisodes.onImageClick = { seasonId, episodeId -> | ||||
|             playEpisode(seasonId, episodeId) | ||||
|         } | ||||
|  | ||||
|         binding.buttonSeasonSelection.text = model.currentSeasonCrunchy.title | ||||
|         binding.buttonSeasonSelection.setOnClickListener { v -> | ||||
|             showSeasonSelection(v) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -52,6 +58,37 @@ class MediaFragmentEpisodes : Fragment() { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun showSeasonSelection(v: View) { | ||||
|         // TODO replace with Exposed dropdown menu: https://material.io/components/menus/android#exposed-dropdown-menus | ||||
|         val popup = PopupMenu(requireContext(), v) | ||||
|         model.seasonsCrunchy.items.forEach { season -> | ||||
|             popup.menu.add(season.title).also { | ||||
|                 it.setOnMenuItemClickListener { | ||||
|                     onSeasonSelected(season.id) | ||||
|                     false | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         popup.show() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Call model to load a new season. | ||||
|      * Once loaded update buttonSeasonSelection text and adapterRecEpisodes. | ||||
|      * | ||||
|      * Suppress waring since invalid. | ||||
|      */ | ||||
|     @SuppressLint("NotifyDataSetChanged") | ||||
|     private fun onSeasonSelected(seasonId: String) { | ||||
|         // load the new season | ||||
|         lifecycleScope.launch { | ||||
|             model.setCurrentSeason(seasonId) | ||||
|             binding.buttonSeasonSelection.text = model.currentSeasonCrunchy.title | ||||
|             adapterRecEpisodes.notifyDataSetChanged() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun playEpisode(seasonId: String, episodeId: String) { | ||||
|         (activity as MainActivity).startPlayer(seasonId, episodeId) | ||||
|         Log.d(javaClass.name, "Started Player with  episodeId: $episodeId") | ||||
|  | ||||
| @ -6,10 +6,7 @@ import androidx.lifecycle.AndroidViewModel | ||||
| import androidx.lifecycle.viewModelScope | ||||
| import kotlinx.coroutines.joinAll | ||||
| import kotlinx.coroutines.launch | ||||
| import org.mosad.teapod.parser.crunchyroll.Crunchyroll | ||||
| import org.mosad.teapod.parser.crunchyroll.NoneEpisodes | ||||
| import org.mosad.teapod.parser.crunchyroll.NoneSeasons | ||||
| import org.mosad.teapod.parser.crunchyroll.NoneSeries | ||||
| import org.mosad.teapod.parser.crunchyroll.* | ||||
| import org.mosad.teapod.preferences.Preferences | ||||
| import org.mosad.teapod.util.DataTypes.MediaType | ||||
| import org.mosad.teapod.util.Meta | ||||
| @ -29,8 +26,11 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic | ||||
|         internal set | ||||
|     var seasonsCrunchy = NoneSeasons | ||||
|         internal set | ||||
|     var currentSeasonCrunchy = NoneSeason | ||||
|         internal set | ||||
|     var episodesCrunchy = NoneEpisodes | ||||
|         internal set | ||||
|     val currentEpisodesCrunchy = arrayListOf<Episode>() | ||||
|  | ||||
|     var tmdbResult: TMDBResult? = null // TODO rename | ||||
|         internal set | ||||
| @ -55,8 +55,9 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic | ||||
|         println("seasons: $seasonsCrunchy") | ||||
|  | ||||
|         // load the preferred season (preferred language, language per season, not per stream) | ||||
|         val preferredSeasonId = seasonsCrunchy.getPreferredSeasonId(Preferences.preferredLocal) | ||||
|         episodesCrunchy = Crunchyroll.episodes(preferredSeasonId) | ||||
|         currentSeasonCrunchy = seasonsCrunchy.getPreferredSeason(Preferences.preferredLocal) | ||||
|         episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id) | ||||
|         currentEpisodesCrunchy.addAll(episodesCrunchy.items) | ||||
|         println("episodes: $episodesCrunchy") | ||||
|  | ||||
|         // TODO check if metaDB knows the title | ||||
| @ -72,6 +73,21 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun setCurrentSeason(seasonId: String) { | ||||
|         // return if the id hasn't changed (performance) | ||||
|         if (currentSeasonCrunchy.id == seasonId) return | ||||
|  | ||||
|         // set currentSeasonCrunchy to the new season with id == seasonId, if the id isn't found, | ||||
|         // don't change the current season (this should/can never happen) | ||||
|         currentSeasonCrunchy = seasonsCrunchy.items.firstOrNull { | ||||
|             it.id == seasonId | ||||
|         } ?: currentSeasonCrunchy | ||||
|  | ||||
|         episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id) | ||||
|         currentEpisodesCrunchy.clear() | ||||
|         currentEpisodesCrunchy.addAll(episodesCrunchy.items) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * set media, tmdb and nextEpisode | ||||
|      * TODO run aod and tmdb load parallel | ||||
|  | ||||
| @ -10,10 +10,10 @@ import com.bumptech.glide.request.RequestOptions | ||||
| import jp.wasabeef.glide.transformations.RoundedCornersTransformation | ||||
| import org.mosad.teapod.R | ||||
| import org.mosad.teapod.databinding.ItemEpisodeBinding | ||||
| import org.mosad.teapod.parser.crunchyroll.Episodes | ||||
| import org.mosad.teapod.parser.crunchyroll.Episode | ||||
| import org.mosad.teapod.util.tmdb.TMDBTVEpisode | ||||
|  | ||||
| class EpisodeItemAdapter(private val episodes: Episodes, private val tmdbEpisodes: List<TMDBTVEpisode>?) : RecyclerView.Adapter<EpisodeItemAdapter.EpisodeViewHolder>() { | ||||
| class EpisodeItemAdapter(private val episodes: List<Episode>, private val tmdbEpisodes: List<TMDBTVEpisode>?) : RecyclerView.Adapter<EpisodeItemAdapter.EpisodeViewHolder>() { | ||||
|  | ||||
|     var onImageClick: ((seasonId: String, episodeId: String) -> Unit)? = null | ||||
|  | ||||
| @ -23,7 +23,7 @@ class EpisodeItemAdapter(private val episodes: Episodes, private val tmdbEpisode | ||||
|  | ||||
|     override fun onBindViewHolder(holder: EpisodeViewHolder, position: Int) { | ||||
|         val context = holder.binding.root.context | ||||
|         val ep = episodes.items[position] | ||||
|         val ep = episodes[position] | ||||
|  | ||||
|         val titleText = if (ep.isDubbed) { | ||||
|             context.getString(R.string.component_episode_title, ep.episode, ep.title) | ||||
| @ -61,7 +61,7 @@ class EpisodeItemAdapter(private val episodes: Episodes, private val tmdbEpisode | ||||
|     } | ||||
|  | ||||
|     override fun getItemCount(): Int { | ||||
|         return episodes.items.size | ||||
|         return episodes.size | ||||
|     } | ||||
|  | ||||
|     fun updateWatchedState(watched: Boolean, position: Int) { | ||||
| @ -77,8 +77,8 @@ class EpisodeItemAdapter(private val episodes: Episodes, private val tmdbEpisode | ||||
|             // on image click return the episode id and index (within the adapter) | ||||
|             binding.imageEpisode.setOnClickListener { | ||||
|                 onImageClick?.invoke( | ||||
|                     episodes.items[bindingAdapterPosition].seasonId, | ||||
|                     episodes.items[bindingAdapterPosition].id | ||||
|                     episodes[bindingAdapterPosition].seasonId, | ||||
|                     episodes[bindingAdapterPosition].id | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @ -0,0 +1,5 @@ | ||||
| <vector android:height="24dp" android:tint="#FFFFFF" | ||||
|     android:viewportHeight="24" android:viewportWidth="24" | ||||
|     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <path android:fillColor="@android:color/white" android:pathData="M7,10l5,5 5,-5z"/> | ||||
| </vector> | ||||
| @ -1,10 +1,24 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <FrameLayout | ||||
| <LinearLayout | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent"> | ||||
|     android:layout_height="match_parent" | ||||
|     android:orientation="vertical"> | ||||
|  | ||||
|     <Button | ||||
|         android:id="@+id/button_season_selection" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_marginStart="7dp" | ||||
|         android:layout_marginTop="6dp" | ||||
|         android:layout_marginEnd="7dp" | ||||
|         android:layout_marginBottom="6dp" | ||||
|         android:singleLine="true" | ||||
|         android:text="@string/text_title_ex" | ||||
|         app:icon="@drawable/ic_baseline_arrow_drop_down_24" | ||||
|         app:iconGravity="end" /> | ||||
|  | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|         android:id="@+id/recycler_episodes" | ||||
| @ -16,4 +30,4 @@ | ||||
|         tools:layout_editor_absoluteY="298dp" | ||||
|         tools:listitem="@layout/item_episode" /> | ||||
|  | ||||
| </FrameLayout> | ||||
| </LinearLayout> | ||||
| @ -133,4 +133,5 @@ | ||||
|     <string name="intent_media_id" translatable="false">intent_media_id</string> | ||||
|     <string name="intent_season_id" translatable="false">intent_season_id</string> | ||||
|     <string name="intent_episode_id" translatable="false">intent_episode_id</string> | ||||
|  | ||||
| </resources> | ||||
| @ -4,6 +4,7 @@ | ||||
|         <item name="colorPrimary">@color/colorPrimary</item> | ||||
|         <item name="colorPrimaryDark">@color/colorPrimaryDark</item> | ||||
|         <item name="colorAccent">@color/colorAccent</item> | ||||
|         <item name="popupMenuStyle">@style/Widget.App.PopupMenu</item> | ||||
|     </style> | ||||
|  | ||||
|     <style name="AppTheme.Light" parent="AppTheme"> | ||||
| @ -65,4 +66,9 @@ | ||||
|         <item name="cornerSize">5dp</item> | ||||
|     </style> | ||||
|  | ||||
|     <!-- popup menus --> | ||||
|     <style name="Widget.App.PopupMenu" parent="Widget.MaterialComponents.PopupMenu"> | ||||
|         <item name="android:popupBackground">?themeSecondary</item> | ||||
|     </style> | ||||
|  | ||||
| </resources> | ||||
		Reference in New Issue
	
	Block a user