Add similar titles to media fragment (#28)
* update androidx navigation libraries * add similar media to MediaFragment * parse similar media in AoDParser Reviewed-on: #28 Co-Authored-By: Jannik <seil0@mosad.xyz> Co-Committed-By: Jannik <seil0@mosad.xyz>
This commit is contained in:
		| @ -13,7 +13,7 @@ | ||||
|         android:supportsRtl="true" | ||||
|         android:theme="@style/AppTheme.Dark"> | ||||
|         <activity | ||||
|             android:name=".activity.SplashActivity" | ||||
|             android:name="org.mosad.teapod.ui.activity.SplashActivity" | ||||
|             android:label="@string/app_name" | ||||
|             android:theme="@style/SplashTheme" | ||||
|             android:screenOrientation="portrait"> | ||||
| @ -23,24 +23,24 @@ | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".activity.onboarding.OnboardingActivity" | ||||
|             android:name="org.mosad.teapod.ui.activity.onboarding.OnboardingActivity" | ||||
|             android:label="@string/app_name" | ||||
|             android:screenOrientation="portrait" | ||||
|             android:launchMode="singleTop" | ||||
|             android:windowSoftInputMode="adjustPan"> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".activity.main.MainActivity" | ||||
|             android:name="org.mosad.teapod.ui.activity.main.MainActivity" | ||||
|             android:label="@string/app_name" | ||||
|             android:screenOrientation="portrait"> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name="org.mosad.teapod.activity.player.PlayerActivity" | ||||
|             android:name="org.mosad.teapod.ui.activity.player.PlayerActivity" | ||||
|             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|layoutDirection" | ||||
|             android:excludeFromRecents="true" | ||||
|             android:label="@string/app_name" | ||||
|             android:launchMode="singleTask" | ||||
|             android:parentActivityName=".activity.main.MainActivity" | ||||
|             android:parentActivityName="org.mosad.teapod.ui.activity.main.MainActivity" | ||||
|             android:supportsPictureInPicture="true" | ||||
|             android:taskAffinity=".player.PlayerActivity" | ||||
|             android:theme="@style/PlayerTheme" | ||||
|  | ||||
| @ -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<String, String>() | ||||
|     private var csrfToken: String = "" | ||||
| @ -323,7 +323,7 @@ object AoDParser { | ||||
|         } | ||||
|         Log.i(javaClass.name, "Loaded playlists successfully") | ||||
|  | ||||
|         // parse additional info from the media page | ||||
|         // additional info from the media page | ||||
|         res.select("table.vertical-table").select("tr").forEach { row -> | ||||
|             when (row.select("th").text().toLowerCase(Locale.ROOT)) { | ||||
|                 "produktionsjahr" -> media.info.year = row.select("td").text().toInt() | ||||
| @ -337,7 +337,21 @@ object AoDParser { | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // parse additional information for tv shows the episode title (description) is loaded from the "api" | ||||
|         // similar titles from media page | ||||
|         media.info.similar = res.select("h2:contains(Ähnliche Animes)").next().select("li").mapNotNull { | ||||
|             val mediaId = it.select("a.thumbs").attr("href") | ||||
|                 .substringAfterLast("/").toIntOrNull() | ||||
|             val mediaImage = it.select("a.thumbs > img").attr("src") | ||||
|             val mediaTitle = it.select("a").text() | ||||
|  | ||||
|             if (mediaId != null) { | ||||
|                 ItemMedia(mediaId, mediaTitle, mediaImage) | ||||
|             } else { | ||||
|                 null | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // additional information for tv shows the episode title (description) is loaded from the "api" | ||||
|         if (media.type == MediaType.TVSHOW) { | ||||
|             res.select("div.three-box-container > div.episodebox").forEach { episodebox -> | ||||
|                 // make sure the episode has a streaming link | ||||
|  | ||||
| @ -1,9 +1,9 @@ | ||||
| package org.mosad.teapod.activity | ||||
| package org.mosad.teapod.ui.activity | ||||
| 
 | ||||
| import android.content.Intent | ||||
| import android.os.Bundle | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| import org.mosad.teapod.activity.main.MainActivity | ||||
| import org.mosad.teapod.ui.activity.main.MainActivity | ||||
| 
 | ||||
| 
 | ||||
| class SplashActivity : AppCompatActivity() { | ||||
| @ -20,7 +20,7 @@ | ||||
|  * | ||||
|  */ | ||||
| 
 | ||||
| package org.mosad.teapod.activity.main | ||||
| package org.mosad.teapod.ui.activity.main | ||||
| 
 | ||||
| import android.content.Intent | ||||
| import android.os.Bundle | ||||
| @ -37,20 +37,19 @@ import kotlinx.coroutines.runBlocking | ||||
| import org.mosad.teapod.R | ||||
| import org.mosad.teapod.databinding.ActivityMainBinding | ||||
| import org.mosad.teapod.parser.AoDParser | ||||
| import org.mosad.teapod.activity.player.PlayerActivity | ||||
| import org.mosad.teapod.ui.activity.player.PlayerActivity | ||||
| import org.mosad.teapod.preferences.EncryptedPreferences | ||||
| import org.mosad.teapod.preferences.Preferences | ||||
| import org.mosad.teapod.ui.components.LoginDialog | ||||
| import org.mosad.teapod.activity.main.fragments.AccountFragment | ||||
| import org.mosad.teapod.activity.main.fragments.HomeFragment | ||||
| import org.mosad.teapod.activity.main.fragments.LibraryFragment | ||||
| import org.mosad.teapod.activity.main.fragments.SearchFragment | ||||
| import org.mosad.teapod.activity.onboarding.OnboardingActivity | ||||
| import org.mosad.teapod.ui.activity.main.fragments.AccountFragment | ||||
| import org.mosad.teapod.ui.activity.main.fragments.HomeFragment | ||||
| import org.mosad.teapod.ui.activity.main.fragments.LibraryFragment | ||||
| import org.mosad.teapod.ui.activity.main.fragments.SearchFragment | ||||
| import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity | ||||
| import org.mosad.teapod.util.DataTypes | ||||
| import org.mosad.teapod.util.StorageController | ||||
| import org.mosad.teapod.util.exitAndRemoveTask | ||||
| import java.net.SocketTimeoutException | ||||
| import kotlin.system.exitProcess | ||||
| import kotlin.system.measureTimeMillis | ||||
| 
 | ||||
| class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener { | ||||
| @ -60,6 +59,11 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS | ||||
| 
 | ||||
|     companion object { | ||||
|         var wasInitialized = false | ||||
|         lateinit var instance: MainActivity | ||||
|     } | ||||
| 
 | ||||
|     init { | ||||
|         instance = this | ||||
|     } | ||||
| 
 | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
| @ -1,4 +1,4 @@ | ||||
| package org.mosad.teapod.activity.main.fragments | ||||
| package org.mosad.teapod.ui.activity.main.fragments | ||||
| 
 | ||||
| import android.content.Intent | ||||
| import android.net.Uri | ||||
| @ -1,4 +1,4 @@ | ||||
| package org.mosad.teapod.activity.main.fragments | ||||
| package org.mosad.teapod.ui.activity.main.fragments | ||||
| 
 | ||||
| import android.os.Bundle | ||||
| import android.util.Log | ||||
| @ -9,7 +9,7 @@ import androidx.fragment.app.Fragment | ||||
| import com.afollestad.materialdialogs.MaterialDialog | ||||
| import com.afollestad.materialdialogs.list.listItemsSingleChoice | ||||
| import org.mosad.teapod.BuildConfig | ||||
| import org.mosad.teapod.activity.main.MainActivity | ||||
| import org.mosad.teapod.ui.activity.main.MainActivity | ||||
| import org.mosad.teapod.R | ||||
| import org.mosad.teapod.databinding.FragmentAccountBinding | ||||
| import org.mosad.teapod.parser.AoDParser | ||||
| @ -1,4 +1,4 @@ | ||||
| package org.mosad.teapod.activity.main.fragments | ||||
| package org.mosad.teapod.ui.activity.main.fragments | ||||
| 
 | ||||
| import android.os.Bundle | ||||
| import android.util.Log | ||||
| @ -11,7 +11,7 @@ import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.GlobalScope | ||||
| import kotlinx.coroutines.launch | ||||
| import org.mosad.teapod.R | ||||
| import org.mosad.teapod.activity.main.MainActivity | ||||
| import org.mosad.teapod.ui.activity.main.MainActivity | ||||
| import org.mosad.teapod.databinding.FragmentHomeBinding | ||||
| import org.mosad.teapod.parser.AoDParser | ||||
| import org.mosad.teapod.util.ItemMedia | ||||
| @ -1,4 +1,4 @@ | ||||
| package org.mosad.teapod.activity.main.fragments | ||||
| package org.mosad.teapod.ui.activity.main.fragments | ||||
| 
 | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| @ -9,7 +9,6 @@ import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.GlobalScope | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.withContext | ||||
| import org.mosad.teapod.activity.main.MainActivity | ||||
| import org.mosad.teapod.databinding.FragmentLibraryBinding | ||||
| import org.mosad.teapod.parser.AoDParser | ||||
| import org.mosad.teapod.util.adapter.MediaItemAdapter | ||||
| @ -1,4 +1,4 @@ | ||||
| package org.mosad.teapod.activity.main.fragments | ||||
| package org.mosad.teapod.ui.activity.main.fragments | ||||
| 
 | ||||
| import android.graphics.Color | ||||
| import android.graphics.drawable.ColorDrawable | ||||
| @ -8,26 +8,34 @@ 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.ui.activity.main.MainActivity | ||||
| import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel | ||||
| 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 | ||||
| 
 | ||||
| /** | ||||
|  * 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 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<Fragment>() | ||||
| 
 | ||||
|     private val model: MediaFragmentViewModel by activityViewModels() | ||||
| 
 | ||||
|     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { | ||||
|         binding = FragmentMediaBinding.inflate(inflater, container, false) | ||||
| @ -38,10 +46,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 +70,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 +85,27 @@ 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) | ||||
|         } | ||||
| 
 | ||||
|         // clear fragments, since it lives in onCreate scope (don't do this in onPause/onStop -> FragmentManager transaction) | ||||
|         fragments.clear() | ||||
|         pagerAdapter.notifyDataSetChanged() | ||||
| 
 | ||||
|         // 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,28 +114,44 @@ 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(MediaFragmentEpisodes()) | ||||
|             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 | ||||
|         if (media.info.similar.isNotEmpty()) { | ||||
|             fragments.add(MediaFragmentSimilar()) | ||||
|             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()) | ||||
|                 MediaType.MOVIE -> playEpisode(media.episodes.first()) | ||||
|                 MediaType.TVSHOW -> playEpisode(nextEpisode) | ||||
|                 else -> Log.e(javaClass.name, "Wrong Type: $media.type") | ||||
|             } | ||||
| @ -152,30 +173,26 @@ 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]) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * play the current episode | ||||
|      * TODO this is also used in MediaFragmentEpisode, we should only have on implementation | ||||
|      */ | ||||
|     private fun playEpisode(ep: Episode) { | ||||
|         playStream(ep) | ||||
|         (activity as MainActivity).startPlayer(model.media.id, ep.id) | ||||
|         Log.d(javaClass.name, "Started Player with  episodeId: ${ep.id}") | ||||
| 
 | ||||
|         // update nextEpisode | ||||
|         nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) { | ||||
|             media.episodes.first{ !it.watched } | ||||
|         } else { | ||||
|             media.episodes.first() | ||||
|         } | ||||
|         binding.textTitle.text = nextEpisode.title | ||||
|         model.updateNextEpisode(ep) // set the correct next episode | ||||
|     } | ||||
| 
 | ||||
|     private fun playStream(ep: Episode) { | ||||
|         Log.d(javaClass.name, "Starting Player with  mediaId: ${media.id}") | ||||
|         (activity as MainActivity).startPlayer(media.id, ep.id) | ||||
|     /** | ||||
|      * A simple pager adapter | ||||
|      */ | ||||
|     private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { | ||||
|         override fun getItemCount(): Int = fragments.size | ||||
| 
 | ||||
|         override fun createFragment(position: Int): Fragment = fragments[position] | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,61 @@ | ||||
| package org.mosad.teapod.ui.activity.main.fragments | ||||
|  | ||||
| import android.os.Bundle | ||||
| import android.util.Log | ||||
| 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.ui.activity.main.MainActivity | ||||
| import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel | ||||
| import org.mosad.teapod.databinding.FragmentMediaEpisodesBinding | ||||
| import org.mosad.teapod.util.Episode | ||||
| import org.mosad.teapod.util.adapter.EpisodeItemAdapter | ||||
|  | ||||
| class MediaFragmentEpisodes : 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 | ||||
|  | ||||
|         // set onItemClick only in adapter is initialized | ||||
|         if (this::adapterRecEpisodes.isInitialized) { | ||||
|             adapterRecEpisodes.onImageClick = { _, position -> | ||||
|                 playEpisode(model.media.episodes[position]) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun onResume() { | ||||
|         super.onResume() | ||||
|  | ||||
|         // if adapterRecEpisodes is initialized, update the watched state for the episodes | ||||
|         if (this::adapterRecEpisodes.isInitialized) { | ||||
|             model.media.episodes.forEachIndexed { index, episode -> | ||||
|                 adapterRecEpisodes.updateWatchedState(episode.watched, index) | ||||
|             } | ||||
|             adapterRecEpisodes.notifyDataSetChanged() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun playEpisode(ep: Episode) { | ||||
|         (activity as MainActivity).startPlayer(model.media.id, ep.id) | ||||
|         Log.d(javaClass.name, "Started Player with  episodeId: ${ep.id}") | ||||
|  | ||||
|         model.updateNextEpisode(ep) // set the correct next episode | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -0,0 +1,41 @@ | ||||
| package org.mosad.teapod.ui.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.FragmentMediaSimilarBinding | ||||
| import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel | ||||
| import org.mosad.teapod.util.adapter.MediaItemAdapter | ||||
| import org.mosad.teapod.util.decoration.MediaItemDecoration | ||||
| import org.mosad.teapod.util.showFragment | ||||
|  | ||||
| class MediaFragmentSimilar : Fragment()  { | ||||
|  | ||||
|     private lateinit var binding: FragmentMediaSimilarBinding | ||||
|     private val model: MediaFragmentViewModel by activityViewModels() | ||||
|  | ||||
|     private lateinit var adapterSimilar: MediaItemAdapter | ||||
|  | ||||
|     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { | ||||
|         binding = FragmentMediaSimilarBinding.inflate(inflater, container, false) | ||||
|         return binding.root | ||||
|     } | ||||
|  | ||||
|     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||
|         super.onViewCreated(view, savedInstanceState) | ||||
|  | ||||
|         adapterSimilar = MediaItemAdapter(model.media.info.similar) | ||||
|         binding.recyclerMediaSimilar.adapter = adapterSimilar | ||||
|         binding.recyclerMediaSimilar.addItemDecoration(MediaItemDecoration(9)) | ||||
|  | ||||
|         // set onItemClick only in adapter is initialized | ||||
|         if (this::adapterSimilar.isInitialized) { | ||||
|             adapterSimilar.onItemClick = { mediaId, _ -> | ||||
|                 activity?.showFragment(MediaFragment(mediaId)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,4 +1,4 @@ | ||||
| package org.mosad.teapod.activity.main.fragments | ||||
| package org.mosad.teapod.ui.activity.main.fragments | ||||
| 
 | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| @ -7,7 +7,6 @@ import android.view.ViewGroup | ||||
| import android.widget.SearchView | ||||
| import androidx.fragment.app.Fragment | ||||
| import kotlinx.coroutines.* | ||||
| import org.mosad.teapod.activity.main.MainActivity | ||||
| import org.mosad.teapod.databinding.FragmentSearchBinding | ||||
| import org.mosad.teapod.parser.AoDParser | ||||
| import org.mosad.teapod.util.decoration.MediaItemDecoration | ||||
| @ -0,0 +1,48 @@ | ||||
| package org.mosad.teapod.ui.activity.main.viewmodel | ||||
|  | ||||
| import android.app.Application | ||||
| import androidx.lifecycle.AndroidViewModel | ||||
| import org.mosad.teapod.parser.AoDParser | ||||
| import org.mosad.teapod.util.* | ||||
| import org.mosad.teapod.util.DataTypes.MediaType | ||||
|  | ||||
| /** | ||||
|  * handle media, next ep and tmdb | ||||
|  */ | ||||
| class MediaFragmentViewModel(application: Application) : AndroidViewModel(application) { | ||||
|  | ||||
|     var media = Media(-1, "", MediaType.OTHER) | ||||
|         internal set | ||||
|     var nextEpisode = Episode() | ||||
|         internal set | ||||
|     var tmdb = TMDBResponse() | ||||
|         internal set | ||||
|  | ||||
|     /** | ||||
|      * set media, tmdb and nextEpisode | ||||
|      */ | ||||
|     suspend fun load(mediaId: Int) { | ||||
|         media = AoDParser.getMediaById(mediaId) | ||||
|         tmdb = TMDBApiController().search(media.info.title, media.type) | ||||
|  | ||||
|         if (media.type == MediaType.TVSHOW) { | ||||
|             nextEpisode = if (media.episodes.firstOrNull{ !it.watched } != null) { | ||||
|                 media.episodes.first{ !it.watched } | ||||
|             } else { | ||||
|                 media.episodes.first() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * get the next episode based on episode number (the true next episode) | ||||
|      * if no matching is found, use first episode | ||||
|      */ | ||||
|     fun updateNextEpisode(currentEp: Episode) { | ||||
|         if (media.type == MediaType.MOVIE) return // return if movie | ||||
|  | ||||
|         nextEpisode = media.episodes.firstOrNull{ it.number > currentEp.number } | ||||
|             ?: media.episodes.first() | ||||
|     } | ||||
|  | ||||
| } | ||||
| @ -1,4 +1,4 @@ | ||||
| package org.mosad.teapod.activity.onboarding | ||||
| package org.mosad.teapod.ui.activity.onboarding | ||||
| 
 | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| @ -1,4 +1,4 @@ | ||||
| package org.mosad.teapod.activity.onboarding | ||||
| package org.mosad.teapod.ui.activity.onboarding | ||||
| 
 | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| @ -1,4 +1,4 @@ | ||||
| package org.mosad.teapod.activity.onboarding | ||||
| package org.mosad.teapod.ui.activity.onboarding | ||||
| 
 | ||||
| import android.content.Intent | ||||
| import android.os.Bundle | ||||
| @ -8,7 +8,7 @@ import androidx.fragment.app.Fragment | ||||
| import androidx.fragment.app.FragmentActivity | ||||
| import androidx.viewpager2.adapter.FragmentStateAdapter | ||||
| import com.google.android.material.tabs.TabLayoutMediator | ||||
| import org.mosad.teapod.activity.main.MainActivity | ||||
| import org.mosad.teapod.ui.activity.main.MainActivity | ||||
| import org.mosad.teapod.databinding.ActivityOnboardingBinding | ||||
| 
 | ||||
| class OnboardingActivity : AppCompatActivity() { | ||||
| @ -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 | ||||
| @ -1,4 +1,4 @@ | ||||
| package org.mosad.teapod.activity.player | ||||
| package org.mosad.teapod.ui.activity.player | ||||
| 
 | ||||
| import android.animation.Animator | ||||
| import android.animation.AnimatorListenerAdapter | ||||
| @ -1,4 +1,4 @@ | ||||
| package org.mosad.teapod.activity.player | ||||
| package org.mosad.teapod.ui.activity.player | ||||
| 
 | ||||
| import android.app.Application | ||||
| import android.net.Uri | ||||
| @ -6,7 +6,7 @@ import android.view.LayoutInflater | ||||
| import android.view.ViewGroup | ||||
| import android.widget.LinearLayout | ||||
| import org.mosad.teapod.databinding.PlayerEpisodesListBinding | ||||
| import org.mosad.teapod.activity.player.PlayerViewModel | ||||
| import org.mosad.teapod.ui.activity.player.PlayerViewModel | ||||
| import org.mosad.teapod.util.adapter.PlayerEpisodeItemAdapter | ||||
|  | ||||
| class EpisodesListPlayer @JvmOverloads constructor( | ||||
|  | ||||
| @ -13,7 +13,7 @@ import android.widget.TextView | ||||
| import androidx.core.view.children | ||||
| import org.mosad.teapod.R | ||||
| import org.mosad.teapod.databinding.PlayerLanguageSettingsBinding | ||||
| import org.mosad.teapod.activity.player.PlayerViewModel | ||||
| import org.mosad.teapod.ui.activity.player.PlayerViewModel | ||||
| import java.util.* | ||||
|  | ||||
| class LanguageSettingsPlayer @JvmOverloads constructor( | ||||
|  | ||||
| @ -55,6 +55,7 @@ data class Media( | ||||
|     fun getEpisodeById(id: Int) = episodes.first { it.id == id } | ||||
| } | ||||
|  | ||||
| // TODO all val? | ||||
| data class Info( | ||||
|     var title: String = "", | ||||
|     var posterUrl: String = "", | ||||
| @ -62,7 +63,8 @@ data class Info( | ||||
|     var description: String = "", | ||||
|     var year: Int = 0, | ||||
|     var age: Int = 0, | ||||
|     var episodesCount: Int = 0 | ||||
|     var episodesCount: Int = 0, | ||||
|     var similar: List<ItemMedia> = listOf() | ||||
| ) | ||||
|  | ||||
| /** | ||||
| @ -96,6 +98,7 @@ data class Stream( | ||||
|  | ||||
| /** | ||||
|  * this class is used for tmdb responses | ||||
|  * TODO why is runtime var? | ||||
|  */ | ||||
| data class TMDBResponse( | ||||
|     val id: Int = 0, | ||||
|  | ||||
| @ -7,7 +7,7 @@ | ||||
|     android:layout_height="match_parent" | ||||
|     android:background="#000000" | ||||
|     android:keepScreenOn="true" | ||||
|     tools:context=".activity.player.PlayerActivity"> | ||||
|     tools:context=".ui.activity.player.PlayerActivity"> | ||||
|  | ||||
|     <com.google.android.exoplayer2.ui.StyledPlayerView | ||||
|         android:id="@+id/video_view" | ||||
|  | ||||
| @ -6,7 +6,7 @@ | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:background="?themePrimary" | ||||
|     tools:context=".activity.main.fragments.AboutFragment"> | ||||
|     tools:context=".ui.activity.main.fragments.AboutFragment"> | ||||
|  | ||||
|     <LinearLayout | ||||
|         android:layout_width="match_parent" | ||||
|  | ||||
| @ -5,7 +5,7 @@ | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:background="?themePrimary" | ||||
|     tools:context=".activity.main.fragments.AccountFragment"> | ||||
|     tools:context=".ui.activity.main.fragments.AccountFragment"> | ||||
|  | ||||
|     <ScrollView | ||||
|         android:layout_width="match_parent" | ||||
|  | ||||
| @ -6,7 +6,7 @@ | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:background="?themePrimary" | ||||
|     tools:context=".activity.main.fragments.HomeFragment"> | ||||
|     tools:context=".ui.activity.main.fragments.HomeFragment"> | ||||
|  | ||||
|     <ScrollView | ||||
|         android:layout_width="match_parent" | ||||
|  | ||||
| @ -5,7 +5,7 @@ | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:background="?themePrimary" | ||||
|     tools:context=".activity.main.fragments.LibraryFragment"> | ||||
|     tools:context=".ui.activity.main.fragments.LibraryFragment"> | ||||
|  | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|         android:id="@+id/recycler_media_library" | ||||
|  | ||||
| @ -5,7 +5,7 @@ | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:background="?themePrimary" | ||||
|     tools:context=".activity.main.fragments.MediaFragment"> | ||||
|     tools:context=".ui.activity.main.fragments.MediaFragment"> | ||||
|  | ||||
|     <androidx.core.widget.NestedScrollView | ||||
|         android:layout_width="match_parent" | ||||
| @ -148,17 +148,24 @@ | ||||
|                 </LinearLayout> | ||||
|             </LinearLayout> | ||||
|  | ||||
|             <androidx.recyclerview.widget.RecyclerView | ||||
|                 android:id="@+id/recycler_episodes" | ||||
|             <com.google.android.material.tabs.TabLayout | ||||
|                 android:id="@+id/tab_episodes_similar" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:background="@android:color/transparent" | ||||
|                 android:layout_marginStart="7dp" | ||||
|                 android:layout_marginTop="17dp" | ||||
|                 android:layout_marginTop="12dp" | ||||
|                 android:layout_marginEnd="7dp" | ||||
|                 android:nestedScrollingEnabled="false" | ||||
|                 app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" | ||||
|                 tools:layout_editor_absoluteY="298dp" | ||||
|                 tools:listitem="@layout/item_episode" /> | ||||
|                 app:tabMode="scrollable" | ||||
|                 app:tabGravity="start" | ||||
|                 app:tabSelectedTextColor="?textPrimary" | ||||
|                 app:tabTextColor="?textSecondary" /> | ||||
|  | ||||
|             <androidx.viewpager2.widget.ViewPager2 | ||||
|                 android:id="@+id/pager_episodes_similar" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" /> | ||||
|  | ||||
|         </LinearLayout> | ||||
|     </androidx.core.widget.NestedScrollView> | ||||
|  | ||||
|  | ||||
							
								
								
									
										20
									
								
								app/src/main/res/layout/fragment_media_episodes.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								app/src/main/res/layout/fragment_media_episodes.xml
									
									
									
									
									
										Normal 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="wrap_content"> | ||||
|  | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|         android:id="@+id/recycler_episodes" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:nestedScrollingEnabled="false" | ||||
|         android:paddingStart="7dp" | ||||
|         android:paddingEnd="7dp" | ||||
|         app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" | ||||
|         tools:layout_editor_absoluteY="298dp" | ||||
|         tools:listitem="@layout/item_episode" /> | ||||
|  | ||||
| </FrameLayout> | ||||
							
								
								
									
										22
									
								
								app/src/main/res/layout/fragment_media_similar.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/src/main/res/layout/fragment_media_similar.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| <?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="wrap_content"> | ||||
|  | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|         android:id="@+id/recycler_media_similar" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:orientation="vertical" | ||||
|         android:paddingStart="3dp" | ||||
|         android:paddingTop="6dp" | ||||
|         android:paddingEnd="3dp" | ||||
|         android:paddingBottom="3dp" | ||||
|         app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" | ||||
|         app:spanCount="2" | ||||
|         tools:listitem="@layout/item_media" /> | ||||
|  | ||||
| </FrameLayout> | ||||
| @ -5,7 +5,7 @@ | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:background="?themePrimary" | ||||
|     tools:context=".activity.main.fragments.SearchFragment"> | ||||
|     tools:context=".ui.activity.main.fragments.SearchFragment"> | ||||
|  | ||||
|     <SearchView | ||||
|         android:id="@+id/search_text" | ||||
|  | ||||
| @ -7,25 +7,25 @@ | ||||
|  | ||||
|     <fragment | ||||
|         android:id="@+id/navigation_home" | ||||
|         android:name="org.mosad.teapod.activity.main.fragments.HomeFragment" | ||||
|         android:name="org.mosad.teapod.ui.activity.main.fragments.HomeFragment" | ||||
|         android:label="@string/title_home" | ||||
|         tools:layout="@layout/fragment_home" /> | ||||
|  | ||||
|     <fragment | ||||
|         android:id="@+id/navigation_library" | ||||
|         android:name="org.mosad.teapod.activity.main.fragments.LibraryFragment" | ||||
|         android:name="org.mosad.teapod.ui.activity.main.fragments.LibraryFragment" | ||||
|         android:label="@string/title_library" | ||||
|         tools:layout="@layout/fragment_library" /> | ||||
|  | ||||
|     <fragment | ||||
|         android:id="@+id/navigation_search" | ||||
|         android:name="org.mosad.teapod.activity.main.fragments.SearchFragment" | ||||
|         android:name="org.mosad.teapod.ui.activity.main.fragments.SearchFragment" | ||||
|         android:label="@string/title_search" | ||||
|         tools:layout="@layout/fragment_search" /> | ||||
|  | ||||
|     <fragment | ||||
|         android:id="@+id/navigation_account" | ||||
|         android:name="org.mosad.teapod.activity.main.fragments.AccountFragment" | ||||
|         android:name="org.mosad.teapod.ui.activity.main.fragments.AccountFragment" | ||||
|         android:label="@string/title_account" | ||||
|         tools:layout="@layout/fragment_account" /> | ||||
|  | ||||
|  | ||||
| @ -27,6 +27,7 @@ | ||||
|         <item quantity="one">%d Minute</item> | ||||
|         <item quantity="other">%d Minuten</item> | ||||
|     </plurals> | ||||
|     <string name="similar_titles">Ähnliche Titel</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> | ||||
|  | ||||
|  | ||||
| @ -34,6 +34,7 @@ | ||||
|         <item quantity="one">%d Minute</item> | ||||
|         <item quantity="other">%d Minutes</item> | ||||
|     </plurals> | ||||
|     <string name="similar_titles">Similar titles</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_poster_desc" translatable="false">episode poster</string> | ||||
|  | ||||
		Reference in New Issue
	
	Block a user