package org.mosad.teapod.ui.activity.main.fragments import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.viewpager2.adapter.FragmentStateAdapter import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions import com.google.android.material.appbar.AppBarLayout import com.google.android.material.tabs.TabLayoutMediator import jp.wasabeef.glide.transformations.BlurTransformation import kotlinx.coroutines.launch import org.mosad.teapod.R import org.mosad.teapod.databinding.FragmentMediaBinding import org.mosad.teapod.parser.crunchyroll.NoneUpNextSeriesList import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel import org.mosad.teapod.util.playerIntent import org.mosad.teapod.util.tmdb.TMDBApiController import org.mosad.teapod.util.tmdb.TMDBMovie import org.mosad.teapod.util.tmdb.TMDBTVShow import org.mosad.teapod.util.toItemMediaList /** * The media detail fragment. * Note: the fragment is created only once, when selecting a similar title etc. * therefore fragments may be not empty and model may be the old one */ class MediaFragment(private val mediaIdStr: String) : Fragment() { private lateinit var binding: FragmentMediaBinding private lateinit var pagerAdapter: FragmentStateAdapter private val model: MediaFragmentViewModel by viewModels() private val fragments = arrayListOf() private var watchlistJobRunning = false private val playerResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { playerFinishedCallback() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = FragmentMediaBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.frameLoading.visibility = View.VISIBLE // tab layout and pager pagerAdapter = ScreenSlidePagerAdapter(this) // fix material components issue #1878, if more tabs are added increase binding.pagerEpisodesSimilar.offscreenPageLimit = 2 binding.pagerEpisodesSimilar.adapter = pagerAdapter // TODO is position 0 always episodes? (and 1 always similar titles) TabLayoutMediator(binding.tabEpisodesSimilar, binding.pagerEpisodesSimilar) { tab, position -> tab.text = when(position) { 0 -> getString(R.string.episodes) 1 -> getString(R.string.similar_titles) else -> "" } }.attach() lifecycleScope.launch { model.loadCrunchy(mediaIdStr) updateGUI() initActions() } } /** * if tmdb data is present, use it, else use the aod data */ private fun updateGUI() = with(model) { // generic gui val backdropUrl = tmdbResult.backdropPath?.let { TMDBApiController.imageUrl + it } ?: seriesCrunchy.images.poster_wide[0][2].source val posterUrl = tmdbResult.posterPath?.let { TMDBApiController.imageUrl + it } ?: seriesCrunchy.images.poster_tall[0][2].source // load poster and backdrop Glide.with(requireContext()).load(posterUrl) .apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY))) .into(binding.imagePoster) Glide.with(requireContext()).load(backdropUrl) .apply(RequestOptions.placeholderOf(ColorDrawable(Color.DKGRAY))) .apply(RequestOptions.bitmapTransform(BlurTransformation(20, 3))) .into(binding.imageBackdrop) binding.textYear.text = when(tmdbResult) { is TMDBTVShow -> (tmdbResult as TMDBTVShow).firstAirDate?.substring(0, 4) is TMDBMovie -> (tmdbResult as TMDBMovie).releaseDate.substring(0, 4) else -> "" } binding.textAge.text = seriesCrunchy.maturityRatings.firstOrNull() binding.textTitle.text = if (upNextSeries != NoneUpNextSeriesList) { upNextSeries.data.first().panel.title } else seriesCrunchy.title binding.textOverview.text = seriesCrunchy.description // set "watchlist" indicator val watchlistIcon = if (isWatchlist) R.drawable.ic_baseline_check_24 else R.drawable.ic_baseline_add_24 Glide.with(requireContext()).load(watchlistIcon).into(binding.imageMyListAction) /** * clear fragments, since it lives in onCreate scope, * don't do this in onPause/onStop -> FragmentManager transaction * (will be called on similar -> new MediaFragment -> onBackPressed) */ val fragmentsSize = fragments.size fragments.clear() pagerAdapter.notifyItemRangeRemoved(0, fragmentsSize) MediaFragmentEpisodes().also { fragments.add(it) pagerAdapter.notifyItemInserted(fragments.indexOf(it)) } // if has similar titles if (model.similarTo.total > 0) { MediaFragmentSimilar(model.similarTo.toItemMediaList()).also { fragments.add(it) pagerAdapter.notifyItemInserted(fragments.indexOf(it)) } } // disable scrolling on appbar, if no tabs where added if(fragments.isEmpty()) { val params = binding.linearMedia.layoutParams as AppBarLayout.LayoutParams params.scrollFlags = 0 // clear all scroll flags } // specific gui (via tmdb) when (tmdbResult) { is TMDBTVShow -> { // episodes count binding.textEpisodesOrRuntime.text = resources.getQuantityString( R.plurals.text_episodes_count, seriesCrunchy.episodeCount, seriesCrunchy.episodeCount ) } is TMDBMovie -> { val tmdbMovie = tmdbResult as TMDBMovie if (tmdbMovie.runtime != null) { binding.textEpisodesOrRuntime.text = resources.getQuantityString( R.plurals.text_runtime, tmdbMovie.runtime, tmdbMovie.runtime ) } else { binding.textEpisodesOrRuntime.visibility = View.GONE } } else -> { binding.textEpisodesOrRuntime.visibility = View.GONE } } binding.frameLoading.visibility = View.GONE // hide loading indicator } private fun initActions() = with(model) { binding.buttonPlay.setOnClickListener { if (upNextSeries != NoneUpNextSeriesList) { val panel = upNextSeries.data.first().panel playEpisode(panel.episodeMetadata.seasonId, panel.id) } } // add or remove media from myList binding.linearMyListAction.setOnClickListener { // don't allow parallel execution if (!watchlistJobRunning) { watchlistJobRunning = true lifecycleScope.launch { setWatchlist() // update "watchlist" indicator val watchlistIcon = if (isWatchlist) R.drawable.ic_baseline_check_24 else R.drawable.ic_baseline_add_24 Glide.with(requireContext()).load(watchlistIcon).into(binding.imageMyListAction) watchlistJobRunning = false } } } } private fun playerFinishedCallback() = lifecycleScope.launch { model.updateOnResume() if (model.upNextSeries != NoneUpNextSeriesList) { binding.textTitle.text = model.upNextSeries.data.first().panel.title } // needs to be called after model.updateOnResume() (fragments.elementAtOrNull(0) as? MediaFragmentEpisodes)?.updateWatchedState() Log.d(javaClass.name, "Updated model and gui after player closed") } /** * play a episode, also runs callback on player result return */ fun playEpisode(seasonId: String, episodeId: String) { playerResult.launch(playerIntent(seasonId, episodeId)) Log.d(javaClass.name, "Started Player with episodeId: $episodeId") } /** * A simple pager adapter */ private inner class ScreenSlidePagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { override fun getItemCount(): Int = fragments.size override fun createFragment(position: Int): Fragment = fragments[position] } }