implement season selection in MediaFragment
This commit is contained in:
parent
ecbbc5db7b
commit
f97d07c2b8
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"?>
|
<?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>
|
@ -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>
|
@ -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>
|
Loading…
Reference in New Issue
Block a user