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:
Jannik 2021-02-06 19:02:12 +01:00
parent 1e9e02c879
commit 5bb51c9054
34 changed files with 357 additions and 121 deletions

View File

@ -46,8 +46,8 @@ dependencies {
implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.2' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.3'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.2' implementation 'androidx.navigation:navigation-ui-ktx:2.3.3'
implementation 'androidx.security:security-crypto:1.1.0-alpha03' implementation 'androidx.security:security-crypto:1.1.0-alpha03'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'

View File

@ -13,7 +13,7 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme.Dark"> android:theme="@style/AppTheme.Dark">
<activity <activity
android:name=".activity.SplashActivity" android:name="org.mosad.teapod.ui.activity.SplashActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/SplashTheme" android:theme="@style/SplashTheme"
android:screenOrientation="portrait"> android:screenOrientation="portrait">
@ -23,24 +23,24 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".activity.onboarding.OnboardingActivity" android:name="org.mosad.teapod.ui.activity.onboarding.OnboardingActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:launchMode="singleTop" android:launchMode="singleTop"
android:windowSoftInputMode="adjustPan"> android:windowSoftInputMode="adjustPan">
</activity> </activity>
<activity <activity
android:name=".activity.main.MainActivity" android:name="org.mosad.teapod.ui.activity.main.MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:screenOrientation="portrait"> android:screenOrientation="portrait">
</activity> </activity>
<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:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|layoutDirection"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTask" android:launchMode="singleTask"
android:parentActivityName=".activity.main.MainActivity" android:parentActivityName="org.mosad.teapod.ui.activity.main.MainActivity"
android:supportsPictureInPicture="true" android:supportsPictureInPicture="true"
android:taskAffinity=".player.PlayerActivity" android:taskAffinity=".player.PlayerActivity"
android:theme="@style/PlayerTheme" android:theme="@style/PlayerTheme"

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 = ""
@ -323,7 +323,7 @@ object AoDParser {
} }
Log.i(javaClass.name, "Loaded playlists successfully") 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 -> res.select("table.vertical-table").select("tr").forEach { row ->
when (row.select("th").text().toLowerCase(Locale.ROOT)) { when (row.select("th").text().toLowerCase(Locale.ROOT)) {
"produktionsjahr" -> media.info.year = row.select("td").text().toInt() "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) { if (media.type == MediaType.TVSHOW) {
res.select("div.three-box-container > div.episodebox").forEach { episodebox -> res.select("div.three-box-container > div.episodebox").forEach { episodebox ->
// make sure the episode has a streaming link // make sure the episode has a streaming link

View File

@ -1,9 +1,9 @@
package org.mosad.teapod.activity package org.mosad.teapod.ui.activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import org.mosad.teapod.activity.main.MainActivity import org.mosad.teapod.ui.activity.main.MainActivity
class SplashActivity : AppCompatActivity() { class SplashActivity : AppCompatActivity() {

View File

@ -20,7 +20,7 @@
* *
*/ */
package org.mosad.teapod.activity.main package org.mosad.teapod.ui.activity.main
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
@ -37,20 +37,19 @@ import kotlinx.coroutines.runBlocking
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.ActivityMainBinding import org.mosad.teapod.databinding.ActivityMainBinding
import org.mosad.teapod.parser.AoDParser 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.EncryptedPreferences
import org.mosad.teapod.preferences.Preferences import org.mosad.teapod.preferences.Preferences
import org.mosad.teapod.ui.components.LoginDialog import org.mosad.teapod.ui.components.LoginDialog
import org.mosad.teapod.activity.main.fragments.AccountFragment import org.mosad.teapod.ui.activity.main.fragments.AccountFragment
import org.mosad.teapod.activity.main.fragments.HomeFragment import org.mosad.teapod.ui.activity.main.fragments.HomeFragment
import org.mosad.teapod.activity.main.fragments.LibraryFragment import org.mosad.teapod.ui.activity.main.fragments.LibraryFragment
import org.mosad.teapod.activity.main.fragments.SearchFragment import org.mosad.teapod.ui.activity.main.fragments.SearchFragment
import org.mosad.teapod.activity.onboarding.OnboardingActivity import org.mosad.teapod.ui.activity.onboarding.OnboardingActivity
import org.mosad.teapod.util.DataTypes import org.mosad.teapod.util.DataTypes
import org.mosad.teapod.util.StorageController import org.mosad.teapod.util.StorageController
import org.mosad.teapod.util.exitAndRemoveTask import org.mosad.teapod.util.exitAndRemoveTask
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import kotlin.system.exitProcess
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener { class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener {
@ -60,6 +59,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

@ -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.content.Intent
import android.net.Uri import android.net.Uri

View File

@ -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.os.Bundle
import android.util.Log import android.util.Log
@ -9,7 +9,7 @@ import androidx.fragment.app.Fragment
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItemsSingleChoice import com.afollestad.materialdialogs.list.listItemsSingleChoice
import org.mosad.teapod.BuildConfig 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.R
import org.mosad.teapod.databinding.FragmentAccountBinding import org.mosad.teapod.databinding.FragmentAccountBinding
import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.AoDParser

View File

@ -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.os.Bundle
import android.util.Log import android.util.Log
@ -11,7 +11,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.mosad.teapod.R 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.databinding.FragmentHomeBinding
import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.util.ItemMedia import org.mosad.teapod.util.ItemMedia

View File

@ -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.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -9,7 +9,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.mosad.teapod.activity.main.MainActivity
import org.mosad.teapod.databinding.FragmentLibraryBinding import org.mosad.teapod.databinding.FragmentLibraryBinding
import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.util.adapter.MediaItemAdapter import org.mosad.teapod.util.adapter.MediaItemAdapter

View File

@ -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.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
@ -8,26 +8,34 @@ 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.ui.activity.main.MainActivity
import org.mosad.teapod.ui.activity.main.viewmodel.MediaFragmentViewModel
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
/**
* 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() { 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 +46,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 +70,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 +85,27 @@ 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)
} }
// clear fragments, since it lives in onCreate scope (don't do this in onPause/onStop -> FragmentManager transaction)
fragments.clear()
pagerAdapter.notifyDataSetChanged()
// 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,28 +114,44 @@ 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(MediaFragmentEpisodes())
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
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 { binding.buttonPlay.setOnClickListener {
when (media.type) { when (media.type) {
MediaType.MOVIE -> playStream(media.episodes.first()) MediaType.MOVIE -> playEpisode(media.episodes.first())
MediaType.TVSHOW -> playEpisode(nextEpisode) MediaType.TVSHOW -> playEpisode(nextEpisode)
else -> Log.e(javaClass.name, "Wrong Type: $media.type") else -> Log.e(javaClass.name, "Wrong Type: $media.type")
} }
@ -152,30 +173,26 @@ 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])
}
}
} }
/**
* play the current episode
* TODO this is also used in MediaFragmentEpisode, we should only have on implementation
*/
private fun playEpisode(ep: Episode) { 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 model.updateNextEpisode(ep) // set the correct next episode
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}") * A simple pager adapter
(activity as MainActivity).startPlayer(media.id, ep.id) */
private inner class ScreenSlidePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = fragments.size
override fun createFragment(position: Int): Fragment = fragments[position]
} }
} }

View File

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

View File

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

View File

@ -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.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -7,7 +7,6 @@ import android.view.ViewGroup
import android.widget.SearchView import android.widget.SearchView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.mosad.teapod.activity.main.MainActivity
import org.mosad.teapod.databinding.FragmentSearchBinding import org.mosad.teapod.databinding.FragmentSearchBinding
import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.util.decoration.MediaItemDecoration import org.mosad.teapod.util.decoration.MediaItemDecoration

View File

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

View File

@ -1,4 +1,4 @@
package org.mosad.teapod.activity.onboarding package org.mosad.teapod.ui.activity.onboarding
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater

View File

@ -1,4 +1,4 @@
package org.mosad.teapod.activity.onboarding package org.mosad.teapod.ui.activity.onboarding
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater

View File

@ -1,4 +1,4 @@
package org.mosad.teapod.activity.onboarding package org.mosad.teapod.ui.activity.onboarding
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
@ -8,7 +8,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayoutMediator 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 import org.mosad.teapod.databinding.ActivityOnboardingBinding
class OnboardingActivity : AppCompatActivity() { class OnboardingActivity : AppCompatActivity() {
@ -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

@ -1,4 +1,4 @@
package org.mosad.teapod.activity.player package org.mosad.teapod.ui.activity.player
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter

View File

@ -1,4 +1,4 @@
package org.mosad.teapod.activity.player package org.mosad.teapod.ui.activity.player
import android.app.Application import android.app.Application
import android.net.Uri import android.net.Uri

View File

@ -6,7 +6,7 @@ import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import org.mosad.teapod.databinding.PlayerEpisodesListBinding 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 import org.mosad.teapod.util.adapter.PlayerEpisodeItemAdapter
class EpisodesListPlayer @JvmOverloads constructor( class EpisodesListPlayer @JvmOverloads constructor(

View File

@ -13,7 +13,7 @@ import android.widget.TextView
import androidx.core.view.children import androidx.core.view.children
import org.mosad.teapod.R import org.mosad.teapod.R
import org.mosad.teapod.databinding.PlayerLanguageSettingsBinding import org.mosad.teapod.databinding.PlayerLanguageSettingsBinding
import org.mosad.teapod.activity.player.PlayerViewModel import org.mosad.teapod.ui.activity.player.PlayerViewModel
import java.util.* import java.util.*
class LanguageSettingsPlayer @JvmOverloads constructor( class LanguageSettingsPlayer @JvmOverloads constructor(

View File

@ -55,6 +55,7 @@ data class Media(
fun getEpisodeById(id: Int) = episodes.first { it.id == id } fun getEpisodeById(id: Int) = episodes.first { it.id == id }
} }
// TODO all val?
data class Info( data class Info(
var title: String = "", var title: String = "",
var posterUrl: String = "", var posterUrl: String = "",
@ -62,7 +63,8 @@ data class Info(
var description: String = "", var description: String = "",
var year: Int = 0, var year: Int = 0,
var age: 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 * this class is used for tmdb responses
* TODO why is runtime var?
*/ */
data class TMDBResponse( data class TMDBResponse(
val id: Int = 0, val id: Int = 0,

View File

@ -7,7 +7,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#000000" android:background="#000000"
android:keepScreenOn="true" android:keepScreenOn="true"
tools:context=".activity.player.PlayerActivity"> tools:context=".ui.activity.player.PlayerActivity">
<com.google.android.exoplayer2.ui.StyledPlayerView <com.google.android.exoplayer2.ui.StyledPlayerView
android:id="@+id/video_view" android:id="@+id/video_view"

View File

@ -6,7 +6,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?themePrimary" android:background="?themePrimary"
tools:context=".activity.main.fragments.AboutFragment"> tools:context=".ui.activity.main.fragments.AboutFragment">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -5,7 +5,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?themePrimary" android:background="?themePrimary"
tools:context=".activity.main.fragments.AccountFragment"> tools:context=".ui.activity.main.fragments.AccountFragment">
<ScrollView <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -6,7 +6,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?themePrimary" android:background="?themePrimary"
tools:context=".activity.main.fragments.HomeFragment"> tools:context=".ui.activity.main.fragments.HomeFragment">
<ScrollView <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -5,7 +5,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?themePrimary" android:background="?themePrimary"
tools:context=".activity.main.fragments.LibraryFragment"> tools:context=".ui.activity.main.fragments.LibraryFragment">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_media_library" android:id="@+id/recycler_media_library"

View File

@ -5,7 +5,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?themePrimary" android:background="?themePrimary"
tools:context=".activity.main.fragments.MediaFragment"> tools:context=".ui.activity.main.fragments.MediaFragment">
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
@ -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" />
</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="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>

View 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>

View File

@ -5,7 +5,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?themePrimary" android:background="?themePrimary"
tools:context=".activity.main.fragments.SearchFragment"> tools:context=".ui.activity.main.fragments.SearchFragment">
<SearchView <SearchView
android:id="@+id/search_text" android:id="@+id/search_text"

View File

@ -7,25 +7,25 @@
<fragment <fragment
android:id="@+id/navigation_home" 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" android:label="@string/title_home"
tools:layout="@layout/fragment_home" /> tools:layout="@layout/fragment_home" />
<fragment <fragment
android:id="@+id/navigation_library" 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" android:label="@string/title_library"
tools:layout="@layout/fragment_library" /> tools:layout="@layout/fragment_library" />
<fragment <fragment
android:id="@+id/navigation_search" 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" android:label="@string/title_search"
tools:layout="@layout/fragment_search" /> tools:layout="@layout/fragment_search" />
<fragment <fragment
android:id="@+id/navigation_account" 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" android:label="@string/title_account"
tools:layout="@layout/fragment_account" /> tools:layout="@layout/fragment_account" />

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>