implement season selection in MediaFragment
parent
ecbbc5db7b
commit
f97d07c2b8
|
@ -14,7 +14,7 @@ android {
|
|||
minSdkVersion 23
|
||||
targetSdkVersion 30
|
||||
versionCode 4200 //00.04.200
|
||||
versionName "1.0.0-alpha1"
|
||||
versionName "1.0.0-alpha2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resValue "string", "build_time", buildTime()
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue