implement season selection in MediaFragment

This commit is contained in:
Jannik 2021-12-29 19:36:33 +01:00
parent ecbbc5db7b
commit f97d07c2b8
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
9 changed files with 109 additions and 28 deletions

View File

@ -14,7 +14,7 @@ android {
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 30 targetSdkVersion 30
versionCode 4200 //00.04.200 versionCode 4200 //00.04.200
versionName "1.0.0-alpha1" versionName "1.0.0-alpha2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "build_time", buildTime() resValue "string", "build_time", buildTime()

View File

@ -80,23 +80,23 @@ data class Seasons(
val total: Int, val total: Int,
val items: List<Season> 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 // try to get the the first seasons which matches the preferred local
items.forEach { season -> items.forEach { season ->
if (season.title.startsWith("(${local.language})", true)) { 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 // if there is no season with the preferred local, try to find a subbed season
items.forEach { season -> items.forEach { season ->
if (season.isSubbed) { if (season.isSubbed) {
return season.id return season
} }
} }
// if there is no preferred language season and no sub, use the first 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 NoneSeasons = Seasons(0, listOf())
val NoneSeason = Season("", "", "", 0, false, false)
/** /**
* Episodes data type * Episodes data type

View File

@ -1,15 +1,19 @@
package org.mosad.teapod.ui.activity.main.fragments package org.mosad.teapod.ui.activity.main.fragments
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels 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.MainActivity
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
import org.mosad.teapod.databinding.FragmentMediaEpisodesBinding
import org.mosad.teapod.util.adapter.EpisodeItemAdapter import org.mosad.teapod.util.adapter.EpisodeItemAdapter
class MediaFragmentEpisodes : Fragment() { class MediaFragmentEpisodes : Fragment() {
@ -27,15 +31,17 @@ class MediaFragmentEpisodes : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
adapterRecEpisodes = EpisodeItemAdapter(model.episodesCrunchy, model.tmdbTVSeason?.episodes) adapterRecEpisodes = EpisodeItemAdapter(model.currentEpisodesCrunchy, model.tmdbTVSeason?.episodes)
binding.recyclerEpisodes.adapter = adapterRecEpisodes binding.recyclerEpisodes.adapter = adapterRecEpisodes
// set onItemClick only in adapter is initialized // set onItemClick, adapter is initialized
if (this::adapterRecEpisodes.isInitialized) { adapterRecEpisodes.onImageClick = { seasonId, episodeId ->
adapterRecEpisodes.onImageClick = { seasonId, episodeId -> playEpisode(seasonId, episodeId)
println("TODO playback episode $episodeId (season: $seasonId)") }
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) { private fun playEpisode(seasonId: String, episodeId: String) {
(activity as MainActivity).startPlayer(seasonId, episodeId) (activity as MainActivity).startPlayer(seasonId, episodeId)
Log.d(javaClass.name, "Started Player with episodeId: $episodeId") Log.d(javaClass.name, "Started Player with episodeId: $episodeId")

View File

@ -6,10 +6,7 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.joinAll import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.mosad.teapod.parser.crunchyroll.Crunchyroll import org.mosad.teapod.parser.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.preferences.Preferences import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.util.DataTypes.MediaType import org.mosad.teapod.util.DataTypes.MediaType
import org.mosad.teapod.util.Meta import org.mosad.teapod.util.Meta
@ -29,8 +26,11 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
internal set internal set
var seasonsCrunchy = NoneSeasons var seasonsCrunchy = NoneSeasons
internal set internal set
var currentSeasonCrunchy = NoneSeason
internal set
var episodesCrunchy = NoneEpisodes var episodesCrunchy = NoneEpisodes
internal set internal set
val currentEpisodesCrunchy = arrayListOf<Episode>()
var tmdbResult: TMDBResult? = null // TODO rename var tmdbResult: TMDBResult? = null // TODO rename
internal set internal set
@ -55,8 +55,9 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic
println("seasons: $seasonsCrunchy") println("seasons: $seasonsCrunchy")
// load the preferred season (preferred language, language per season, not per stream) // load the preferred season (preferred language, language per season, not per stream)
val preferredSeasonId = seasonsCrunchy.getPreferredSeasonId(Preferences.preferredLocal) currentSeasonCrunchy = seasonsCrunchy.getPreferredSeason(Preferences.preferredLocal)
episodesCrunchy = Crunchyroll.episodes(preferredSeasonId) episodesCrunchy = Crunchyroll.episodes(currentSeasonCrunchy.id)
currentEpisodesCrunchy.addAll(episodesCrunchy.items)
println("episodes: $episodesCrunchy") println("episodes: $episodesCrunchy")
// TODO check if metaDB knows the title // 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 * set media, tmdb and nextEpisode
* TODO run aod and tmdb load parallel * TODO run aod and tmdb load parallel

View File

@ -10,10 +10,10 @@ 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.ItemEpisodeBinding 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 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 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) { override fun onBindViewHolder(holder: EpisodeViewHolder, position: Int) {
val context = holder.binding.root.context val context = holder.binding.root.context
val ep = episodes.items[position] val ep = episodes[position]
val titleText = if (ep.isDubbed) { val titleText = if (ep.isDubbed) {
context.getString(R.string.component_episode_title, ep.episode, ep.title) 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 { override fun getItemCount(): Int {
return episodes.items.size return episodes.size
} }
fun updateWatchedState(watched: Boolean, position: Int) { 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) // on image click return the episode id and index (within the adapter)
binding.imageEpisode.setOnClickListener { binding.imageEpisode.setOnClickListener {
onImageClick?.invoke( onImageClick?.invoke(
episodes.items[bindingAdapterPosition].seasonId, episodes[bindingAdapterPosition].seasonId,
episodes.items[bindingAdapterPosition].id episodes[bindingAdapterPosition].id
) )
} }
} }

View File

@ -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>

View File

@ -1,10 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" 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 <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_episodes" android:id="@+id/recycler_episodes"
@ -16,4 +30,4 @@
tools:layout_editor_absoluteY="298dp" tools:layout_editor_absoluteY="298dp"
tools:listitem="@layout/item_episode" /> tools:listitem="@layout/item_episode" />
</FrameLayout> </LinearLayout>

View File

@ -133,4 +133,5 @@
<string name="intent_media_id" translatable="false">intent_media_id</string> <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_season_id" translatable="false">intent_season_id</string>
<string name="intent_episode_id" translatable="false">intent_episode_id</string> <string name="intent_episode_id" translatable="false">intent_episode_id</string>
</resources> </resources>

View File

@ -4,6 +4,7 @@
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/colorAccent</item>
<item name="popupMenuStyle">@style/Widget.App.PopupMenu</item>
</style> </style>
<style name="AppTheme.Light" parent="AppTheme"> <style name="AppTheme.Light" parent="AppTheme">
@ -65,4 +66,9 @@
<item name="cornerSize">5dp</item> <item name="cornerSize">5dp</item>
</style> </style>
<!-- popup menus -->
<style name="Widget.App.PopupMenu" parent="Widget.MaterialComponents.PopupMenu">
<item name="android:popupBackground">?themeSecondary</item>
</style>
</resources> </resources>