add tab layout to media fragment

this is a rough first implementation of the tab layout to switch between episodes and similar titles
This commit is contained in:
Jannik 2021-01-29 21:55:01 +01:00
parent 1e9e02c879
commit d73f9882ff
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
12 changed files with 254 additions and 74 deletions

View File

@ -31,6 +31,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.callbacks.onDismiss import com.afollestad.materialdialogs.callbacks.onDismiss
import com.afollestad.materialdialogs.utils.MDUtil.isLandscape
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.coroutines.joinAll import kotlinx.coroutines.joinAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -60,6 +61,11 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
companion object { companion object {
var wasInitialized = false var wasInitialized = false
lateinit var instance: MainActivity
}
init {
instance = this
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {

View File

@ -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()
}
}
}

View File

@ -8,26 +8,30 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment 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.Glide
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.google.android.material.tabs.TabLayoutMediator
import jp.wasabeef.glide.transformations.BlurTransformation import jp.wasabeef.glide.transformations.BlurTransformation
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.activity.main.MainActivity
import org.mosad.teapod.databinding.FragmentMediaBinding import org.mosad.teapod.databinding.FragmentMediaBinding
import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.util.* import org.mosad.teapod.util.*
import org.mosad.teapod.util.DataTypes.MediaType 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() { class MediaFragment(private val mediaId: Int) : Fragment() {
private lateinit var binding: FragmentMediaBinding private lateinit var binding: FragmentMediaBinding
private lateinit var adapterRecEpisodes: EpisodeItemAdapter private lateinit var pagerAdapter: FragmentStateAdapter
private lateinit var media: Media private val fragments = arrayListOf<Fragment>()
private lateinit var tmdb: TMDBResponse
private lateinit var nextEpisode: Episode private val model: MediaFragmentViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentMediaBinding.inflate(inflater, container, false) binding = FragmentMediaBinding.inflate(inflater, container, false)
@ -38,10 +42,19 @@ class MediaFragment(private val mediaId: Int) : Fragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.frameLoading.visibility = View.VISIBLE 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) { GlobalScope.launch(Dispatchers.Main) {
// load the streams for the selected media model.load(mediaId) // load the streams and tmdb for the selected media
media = AoDParser.getMediaById(mediaId)
tmdb = TMDBApiController().search(media.info.title, media.type)
if (this@MediaFragment.isAdded) { if (this@MediaFragment.isAdded) {
updateGUI() updateGUI()
@ -53,20 +66,14 @@ class MediaFragment(private val mediaId: Int) : Fragment() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// only notify adapter, if initialized // update the next ep text, since it may have changed
if (this::adapterRecEpisodes.isInitialized) { binding.textTitle.text = model.nextEpisode.title
// TODO find a better solution for this
media.episodes.forEachIndexed { index, episode ->
adapterRecEpisodes.updateWatchedState(episode.watched, index)
}
adapterRecEpisodes.notifyDataSetChanged()
}
} }
/** /**
* if tmdb data is present, use it, else use the aod data * if tmdb data is present, use it, else use the aod data
*/ */
private fun updateGUI() = with(binding) { private fun updateGUI() = with(model) {
// generic gui // generic gui
val backdropUrl = if (tmdb.backdropUrl.isNotEmpty()) tmdb.backdropUrl else media.info.posterUrl val backdropUrl = if (tmdb.backdropUrl.isNotEmpty()) tmdb.backdropUrl else media.info.posterUrl
val posterUrl = if (tmdb.posterUrl.isNotEmpty()) tmdb.posterUrl 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) Glide.with(requireContext()).load(backdropUrl)
.apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY))) .apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY)))
.apply(RequestOptions.bitmapTransform(BlurTransformation(20, 3))) .apply(RequestOptions.bitmapTransform(BlurTransformation(20, 3)))
.into(imageBackdrop) .into(binding.imageBackdrop)
Glide.with(requireContext()).load(posterUrl) Glide.with(requireContext()).load(posterUrl)
.into(imagePoster) .into(binding.imagePoster)
textTitle.text = media.info.title binding.textTitle.text = media.info.title
textYear.text = media.info.year.toString() binding.textYear.text = media.info.year.toString()
textAge.text = media.info.age.toString() binding.textAge.text = media.info.age.toString()
textOverview.text = media.info.shortDesc binding.textOverview.text = media.info.shortDesc
if (StorageController.myList.contains(media.id)) { 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 { } 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 // specific gui
if (media.type == MediaType.TVSHOW) { 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 // get next episode
nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) { nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) {
media.episodes.first{ !it.watched } media.episodes.first{ !it.watched }
@ -109,25 +106,39 @@ class MediaFragment(private val mediaId: Int) : Fragment() {
} }
// title is the next episodes title // 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) { } else if (media.type == MediaType.MOVIE) {
recyclerEpisodes.visibility = View.GONE
if (tmdb.runtime > 0) { if (tmdb.runtime > 0) {
textEpisodesOrRuntime.text = resources.getQuantityString( binding.textEpisodesOrRuntime.text = resources.getQuantityString(
R.plurals.text_runtime, R.plurals.text_runtime,
tmdb.runtime, tmdb.runtime,
tmdb.runtime tmdb.runtime
) )
} else { } 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 { binding.buttonPlay.setOnClickListener {
when (media.type) { when (media.type) {
MediaType.MOVIE -> playStream(media.episodes.first()) MediaType.MOVIE -> playStream(media.episodes.first())
@ -152,30 +163,15 @@ class MediaFragment(private val mediaId: Int) : Fragment() {
(it as HomeFragment).updateMyListMedia() (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 override fun createFragment(position: Int): Fragment = fragments[position]
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)
} }
} }

View File

@ -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<Application>().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)
}
}

View File

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

View File

@ -67,8 +67,7 @@ class OnboardingActivity : AppCompatActivity() {
} }
/** /**
* A simple pager adapter that represents 5 ScreenSlidePageFragment objects, in * A simple pager adapter
* sequence.
*/ */
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = fragments.size override fun getItemCount(): Int = fragments.size

View File

@ -41,7 +41,7 @@ object AoDParser {
private const val loginPath = "/users/sign_in" private const val loginPath = "/users/sign_in"
private const val libraryPath = "/animes" 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<String, String>() private var sessionCookies = mutableMapOf<String, String>()
private var csrfToken: String = "" private var csrfToken: String = ""

View File

@ -148,17 +148,24 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<androidx.recyclerview.widget.RecyclerView <com.google.android.material.tabs.TabLayout
android:id="@+id/recycler_episodes" android:id="@+id/tab_episodes_similar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:layout_marginStart="7dp" android:layout_marginStart="7dp"
android:layout_marginTop="17dp" android:layout_marginTop="12dp"
android:layout_marginEnd="7dp" android:layout_marginEnd="7dp"
android:nestedScrollingEnabled="false" app:tabMode="scrollable"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:tabGravity="start"
tools:layout_editor_absoluteY="298dp" app:tabSelectedTextColor="?textPrimary"
tools:listitem="@layout/item_episode" /> app:tabTextColor="?textSecondary" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager_episodes_similar"
android:layout_width="match_parent"
android:layout_height="wrap_content"></androidx.viewpager2.widget.ViewPager2>
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_episodes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:layout_marginEnd="7dp"
android:nestedScrollingEnabled="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:layout_editor_absoluteY="298dp"
tools:listitem="@layout/item_episode" />
</FrameLayout>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="7dp"
android:text="similar media"
android:textColor="?textPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -27,6 +27,7 @@
<item quantity="one">%d Minute</item> <item quantity="one">%d Minute</item>
<item quantity="other">%d Minuten</item> <item quantity="other">%d Minuten</item>
</plurals> </plurals>
<string name="similar_titles">Ähnliche Titel</string>
<string name="component_episode_title">Flg. %1$d %2$s</string> <string name="component_episode_title">Flg. %1$d %2$s</string>
<string name="component_episode_title_sub">Flg. %1$d %2$s (OmU)</string> <string name="component_episode_title_sub">Flg. %1$d %2$s (OmU)</string>

View File

@ -34,6 +34,7 @@
<item quantity="one">%d Minute</item> <item quantity="one">%d Minute</item>
<item quantity="other">%d Minutes</item> <item quantity="other">%d Minutes</item>
</plurals> </plurals>
<string name="similar_titles">Similar titles</string>
<string name="component_episode_title">Ep. %1$d %2$s</string> <string name="component_episode_title">Ep. %1$d %2$s</string>
<string name="component_episode_title_sub">Ep. %1$d %2$s (Sub)</string> <string name="component_episode_title_sub">Ep. %1$d %2$s (Sub)</string>
<string name="component_poster_desc" translatable="false">episode poster</string> <string name="component_poster_desc" translatable="false">episode poster</string>