From d73f9882ff1f6cca5904ea9d2a9ad6f467ce0fc7 Mon Sep 17 00:00:00 2001 From: Jannik Date: Fri, 29 Jan 2021 21:55:01 +0100 Subject: [PATCH] add tab layout to media fragment this is a rough first implementation of the tab layout to switch between episodes and similar titles --- .../teapod/activity/main/MainActivity.kt | 6 + .../main/fragments/MediaEpisodesFragment.kt | 51 +++++++ .../activity/main/fragments/MediaFragment.kt | 124 +++++++++--------- .../main/fragments/MediaFragmentViewModel.kt | 63 +++++++++ .../main/fragments/MediaSimilarFragment.kt | 18 +++ .../activity/onboarding/OnboardingActivity.kt | 3 +- .../java/org/mosad/teapod/parser/AoDParser.kt | 2 +- app/src/main/res/layout/fragment_media.xml | 21 ++- .../res/layout/fragment_media_episodes.xml | 20 +++ .../res/layout/fragment_media_similar.xml | 18 +++ app/src/main/res/values-de-rDE/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 12 files changed, 254 insertions(+), 74 deletions(-) create mode 100644 app/src/main/java/org/mosad/teapod/activity/main/fragments/MediaEpisodesFragment.kt create mode 100644 app/src/main/java/org/mosad/teapod/activity/main/fragments/MediaFragmentViewModel.kt create mode 100644 app/src/main/java/org/mosad/teapod/activity/main/fragments/MediaSimilarFragment.kt create mode 100644 app/src/main/res/layout/fragment_media_episodes.xml create mode 100644 app/src/main/res/layout/fragment_media_similar.xml diff --git a/app/src/main/java/org/mosad/teapod/activity/main/MainActivity.kt b/app/src/main/java/org/mosad/teapod/activity/main/MainActivity.kt index c22a68b..1bd5b6d 100644 --- a/app/src/main/java/org/mosad/teapod/activity/main/MainActivity.kt +++ b/app/src/main/java/org/mosad/teapod/activity/main/MainActivity.kt @@ -31,6 +31,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.commit import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.callbacks.onDismiss +import com.afollestad.materialdialogs.utils.MDUtil.isLandscape import com.google.android.material.bottomnavigation.BottomNavigationView import kotlinx.coroutines.joinAll import kotlinx.coroutines.runBlocking @@ -60,6 +61,11 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS companion object { var wasInitialized = false + lateinit var instance: MainActivity + } + + init { + instance = this } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/org/mosad/teapod/activity/main/fragments/MediaEpisodesFragment.kt b/app/src/main/java/org/mosad/teapod/activity/main/fragments/MediaEpisodesFragment.kt new file mode 100644 index 0000000..9bbd93b --- /dev/null +++ b/app/src/main/java/org/mosad/teapod/activity/main/fragments/MediaEpisodesFragment.kt @@ -0,0 +1,51 @@ +package org.mosad.teapod.activity.main.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import org.mosad.teapod.databinding.FragmentMediaEpisodesBinding +import org.mosad.teapod.util.adapter.EpisodeItemAdapter + +class MediaEpisodesFragment : Fragment() { + + private lateinit var binding: FragmentMediaEpisodesBinding + private lateinit var adapterRecEpisodes: EpisodeItemAdapter + + private val model: MediaFragmentViewModel by activityViewModels() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = FragmentMediaEpisodesBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + adapterRecEpisodes = EpisodeItemAdapter(model.media.episodes) + binding.recyclerEpisodes.adapter = adapterRecEpisodes + + println(model.media.episodes) + + // set onItemClick only in adapter is initialized + if (this::adapterRecEpisodes.isInitialized) { + adapterRecEpisodes.onImageClick = { _, position -> + model.playEpisode(model.media.episodes[position]) + } + } + } + + override fun onResume() { + super.onResume() + + if (this::adapterRecEpisodes.isInitialized) { + model.media.episodes.forEachIndexed { index, episode -> + adapterRecEpisodes.updateWatchedState(episode.watched, index) + } + adapterRecEpisodes.notifyDataSetChanged() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/activity/main/fragments/MediaFragment.kt b/app/src/main/java/org/mosad/teapod/activity/main/fragments/MediaFragment.kt index ed3f42f..248d710 100644 --- a/app/src/main/java/org/mosad/teapod/activity/main/fragments/MediaFragment.kt +++ b/app/src/main/java/org/mosad/teapod/activity/main/fragments/MediaFragment.kt @@ -8,26 +8,30 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.activityViewModels +import androidx.viewpager2.adapter.FragmentStateAdapter import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions +import com.google.android.material.tabs.TabLayoutMediator import jp.wasabeef.glide.transformations.BlurTransformation import kotlinx.coroutines.* import org.mosad.teapod.R -import org.mosad.teapod.activity.main.MainActivity import org.mosad.teapod.databinding.FragmentMediaBinding -import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.util.* import org.mosad.teapod.util.DataTypes.MediaType -import org.mosad.teapod.util.adapter.EpisodeItemAdapter +/** + * TODO use a shared ViewModel for MediaFragment and it's sibling Fragments (episodes and similar) + */ class MediaFragment(private val mediaId: Int) : Fragment() { private lateinit var binding: FragmentMediaBinding - private lateinit var adapterRecEpisodes: EpisodeItemAdapter + private lateinit var pagerAdapter: FragmentStateAdapter - private lateinit var media: Media - private lateinit var tmdb: TMDBResponse - private lateinit var nextEpisode: Episode + private val fragments = arrayListOf() + + private val model: MediaFragmentViewModel by activityViewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = FragmentMediaBinding.inflate(inflater, container, false) @@ -38,10 +42,19 @@ class MediaFragment(private val mediaId: Int) : Fragment() { super.onViewCreated(view, savedInstanceState) binding.frameLoading.visibility = View.VISIBLE + // tab layout and pager TODO + pagerAdapter = ScreenSlidePagerAdapter(requireActivity()) + binding.pagerEpisodesSimilar.adapter = pagerAdapter + TabLayoutMediator(binding.tabEpisodesSimilar, binding.pagerEpisodesSimilar) { tab, position -> + tab.text = if (model.media.type == MediaType.TVSHOW && position == 0) { + getString(R.string.episodes) + } else { + getString(R.string.similar_titles) + } + }.attach() + GlobalScope.launch(Dispatchers.Main) { - // load the streams for the selected media - media = AoDParser.getMediaById(mediaId) - tmdb = TMDBApiController().search(media.info.title, media.type) + model.load(mediaId) // load the streams and tmdb for the selected media if (this@MediaFragment.isAdded) { updateGUI() @@ -53,20 +66,14 @@ class MediaFragment(private val mediaId: Int) : Fragment() { override fun onResume() { super.onResume() - // only notify adapter, if initialized - if (this::adapterRecEpisodes.isInitialized) { - // TODO find a better solution for this - media.episodes.forEachIndexed { index, episode -> - adapterRecEpisodes.updateWatchedState(episode.watched, index) - } - adapterRecEpisodes.notifyDataSetChanged() - } + // update the next ep text, since it may have changed + binding.textTitle.text = model.nextEpisode.title } /** * if tmdb data is present, use it, else use the aod data */ - private fun updateGUI() = with(binding) { + private fun updateGUI() = with(model) { // generic gui val backdropUrl = if (tmdb.backdropUrl.isNotEmpty()) tmdb.backdropUrl else media.info.posterUrl val posterUrl = if (tmdb.posterUrl.isNotEmpty()) tmdb.posterUrl else media.info.posterUrl @@ -74,33 +81,23 @@ class MediaFragment(private val mediaId: Int) : Fragment() { Glide.with(requireContext()).load(backdropUrl) .apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY))) .apply(RequestOptions.bitmapTransform(BlurTransformation(20, 3))) - .into(imageBackdrop) + .into(binding.imageBackdrop) Glide.with(requireContext()).load(posterUrl) - .into(imagePoster) + .into(binding.imagePoster) - textTitle.text = media.info.title - textYear.text = media.info.year.toString() - textAge.text = media.info.age.toString() - textOverview.text = media.info.shortDesc + binding.textTitle.text = media.info.title + binding.textYear.text = media.info.year.toString() + binding.textAge.text = media.info.age.toString() + binding.textOverview.text = media.info.shortDesc if (StorageController.myList.contains(media.id)) { - Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(imageMyListAction) + Glide.with(requireContext()).load(R.drawable.ic_baseline_check_24).into(binding.imageMyListAction) } else { - Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(imageMyListAction) + Glide.with(requireContext()).load(R.drawable.ic_baseline_add_24).into(binding.imageMyListAction) } // specific gui if (media.type == MediaType.TVSHOW) { - adapterRecEpisodes = EpisodeItemAdapter(media.episodes) - recyclerEpisodes.adapter = adapterRecEpisodes - - // episodes count - binding.textEpisodesOrRuntime.text = resources.getQuantityString( - R.plurals.text_episodes_count, - media.info.episodesCount, - media.info.episodesCount - ) - // get next episode nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) { media.episodes.first{ !it.watched } @@ -109,25 +106,39 @@ class MediaFragment(private val mediaId: Int) : Fragment() { } // title is the next episodes title - textTitle.text = nextEpisode.title + binding.textTitle.text = nextEpisode.title + + // episodes count + binding.textEpisodesOrRuntime.text = resources.getQuantityString( + R.plurals.text_episodes_count, + media.info.episodesCount, + media.info.episodesCount + ) + + // episodes + fragments.add(MediaEpisodesFragment()) + pagerAdapter.notifyDataSetChanged() } else if (media.type == MediaType.MOVIE) { - recyclerEpisodes.visibility = View.GONE if (tmdb.runtime > 0) { - textEpisodesOrRuntime.text = resources.getQuantityString( + binding.textEpisodesOrRuntime.text = resources.getQuantityString( R.plurals.text_runtime, tmdb.runtime, tmdb.runtime ) } else { - textEpisodesOrRuntime.visibility = View.GONE + binding.textEpisodesOrRuntime.visibility = View.GONE } } - frameLoading.visibility = View.GONE // hide loading indicator + // if has similar titles + fragments.add(MediaSimilarFragment()) + pagerAdapter.notifyDataSetChanged() + + binding.frameLoading.visibility = View.GONE // hide loading indicator } - private fun initActions() { + private fun initActions() = with(model) { binding.buttonPlay.setOnClickListener { when (media.type) { MediaType.MOVIE -> playStream(media.episodes.first()) @@ -152,30 +163,15 @@ class MediaFragment(private val mediaId: Int) : Fragment() { (it as HomeFragment).updateMyListMedia() } } - - // set onItemClick only in adapter is initialized - if (this::adapterRecEpisodes.isInitialized) { - adapterRecEpisodes.onImageClick = { _, position -> - playEpisode(media.episodes[position]) - } - } } - private fun playEpisode(ep: Episode) { - playStream(ep) + /** + * A simple pager adapter + */ + private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { + override fun getItemCount(): Int = fragments.size - // update nextEpisode - nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) { - media.episodes.first{ !it.watched } - } else { - media.episodes.first() - } - binding.textTitle.text = nextEpisode.title - } - - private fun playStream(ep: Episode) { - Log.d(javaClass.name, "Starting Player with mediaId: ${media.id}") - (activity as MainActivity).startPlayer(media.id, ep.id) + override fun createFragment(position: Int): Fragment = fragments[position] } } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/activity/main/fragments/MediaFragmentViewModel.kt b/app/src/main/java/org/mosad/teapod/activity/main/fragments/MediaFragmentViewModel.kt new file mode 100644 index 0000000..57cb71c --- /dev/null +++ b/app/src/main/java/org/mosad/teapod/activity/main/fragments/MediaFragmentViewModel.kt @@ -0,0 +1,63 @@ +package org.mosad.teapod.activity.main.fragments + +import android.app.Application +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import org.mosad.teapod.activity.main.MainActivity +import org.mosad.teapod.parser.AoDParser +import org.mosad.teapod.util.* + +class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) { + + var media = Media(-1, "", DataTypes.MediaType.OTHER) + internal set + var nextEpisode = Episode() + internal set + var tmdb = TMDBResponse() + internal set + + suspend fun load(mediaId: Int) { + media = AoDParser.getMediaById(mediaId) + tmdb = TMDBApiController().search(media.info.title, media.type) + + if (media.type == DataTypes.MediaType.TVSHOW) { + nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) { + media.episodes.first{ !it.watched } + } else { + media.episodes.first() + } + } + } + + fun playEpisode(ep: Episode) { + playStream(ep) + + // update nextEpisode + val currentEpNumber = nextEpisode.number + nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) { + media.episodes.first{ !it.watched && it.number > currentEpNumber } + } else { + media.episodes.first() + } + } + + fun playStream(ep: Episode) { + Log.d(javaClass.name, "Starting Player with mediaId: ${media.id}") + + // TODO somehow start the player ... + MainActivity.instance.startPlayer(media.id, ep.id) // this is just a workaround! + + // not working TODO once thsi is solved, use ViewModel instead of AndroidViewModel +// with(getApplication().baseContext) { +// val intent = Intent(this, PlayerActivity::class.java).apply { +// putExtra(getString(R.string.intent_media_id), media.id) +// putExtra(getString(R.string.intent_episode_id), ep.id) +// addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) +// } +// startActivity(intent) +// } + + //(activity as MainActivity).startPlayer(media.id, ep.id) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/activity/main/fragments/MediaSimilarFragment.kt b/app/src/main/java/org/mosad/teapod/activity/main/fragments/MediaSimilarFragment.kt new file mode 100644 index 0000000..8927642 --- /dev/null +++ b/app/src/main/java/org/mosad/teapod/activity/main/fragments/MediaSimilarFragment.kt @@ -0,0 +1,18 @@ +package org.mosad.teapod.activity.main.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import org.mosad.teapod.databinding.FragmentMediaSimilarBinding + +class MediaSimilarFragment : Fragment() { + + private lateinit var binding: FragmentMediaSimilarBinding + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = FragmentMediaSimilarBinding.inflate(inflater, container, false) + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/activity/onboarding/OnboardingActivity.kt b/app/src/main/java/org/mosad/teapod/activity/onboarding/OnboardingActivity.kt index 4493b82..3c4488b 100644 --- a/app/src/main/java/org/mosad/teapod/activity/onboarding/OnboardingActivity.kt +++ b/app/src/main/java/org/mosad/teapod/activity/onboarding/OnboardingActivity.kt @@ -67,8 +67,7 @@ class OnboardingActivity : AppCompatActivity() { } /** - * A simple pager adapter that represents 5 ScreenSlidePageFragment objects, in - * sequence. + * A simple pager adapter */ private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { override fun getItemCount(): Int = fragments.size diff --git a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt index 217708d..2362d9d 100644 --- a/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt +++ b/app/src/main/java/org/mosad/teapod/parser/AoDParser.kt @@ -41,7 +41,7 @@ object AoDParser { private const val loginPath = "/users/sign_in" private const val libraryPath = "/animes" - private const val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0" + private const val userAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0" private var sessionCookies = mutableMapOf() private var csrfToken: String = "" diff --git a/app/src/main/res/layout/fragment_media.xml b/app/src/main/res/layout/fragment_media.xml index ff4d7ad..ce972b9 100644 --- a/app/src/main/res/layout/fragment_media.xml +++ b/app/src/main/res/layout/fragment_media.xml @@ -148,17 +148,24 @@ - + app:tabMode="scrollable" + app:tabGravity="start" + app:tabSelectedTextColor="?textPrimary" + app:tabTextColor="?textSecondary" /> + + + diff --git a/app/src/main/res/layout/fragment_media_episodes.xml b/app/src/main/res/layout/fragment_media_episodes.xml new file mode 100644 index 0000000..21453fc --- /dev/null +++ b/app/src/main/res/layout/fragment_media_episodes.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_media_similar.xml b/app/src/main/res/layout/fragment_media_similar.xml new file mode 100644 index 0000000..e3e23a0 --- /dev/null +++ b/app/src/main/res/layout/fragment_media_similar.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index d710961..c958865 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -27,6 +27,7 @@ %d Minute %d Minuten + Ähnliche Titel Flg. %1$d %2$s Flg. %1$d %2$s (OmU) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1c6411c..fe2da5e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -34,6 +34,7 @@ %d Minute %d Minutes + Similar titles Ep. %1$d %2$s Ep. %1$d %2$s (Sub) episode poster