improved MediaFragment UI

* fix searchview not losing focus when media is selected
This commit is contained in:
Jannik 2020-10-13 15:56:07 +02:00
parent 597271d4de
commit dcaf64acde
Signed by: Seil0
GPG Key ID: E8459F3723C52C24
12 changed files with 139 additions and 35 deletions

View File

@ -9,6 +9,8 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.commit import androidx.fragment.app.commit
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.mosad.teapod.parser.AoDParser import org.mosad.teapod.parser.AoDParser
import org.mosad.teapod.preferences.EncryptedPreferences import org.mosad.teapod.preferences.EncryptedPreferences
import org.mosad.teapod.ui.MediaFragment import org.mosad.teapod.ui.MediaFragment
@ -84,7 +86,10 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
} }
} }
fun showDetailFragment(media: Media) { /**
* TODO show loading fragment
*/
fun showDetailFragment(media: Media) = GlobalScope.launch {
media.episodes = AoDParser().loadStreams(media) // load the streams for the selected media media.episodes = AoDParser().loadStreams(media) // load the streams for the selected media
val tmdb = TMDBApiController().search(media.title, media.type) val tmdb = TMDBApiController().search(media.title, media.type)

View File

@ -36,10 +36,10 @@ class AoDParser {
.execute() .execute()
val authenticityToken = resAuth.parse().select("meta[name=csrf-token]").attr("content") val authenticityToken = resAuth.parse().select("meta[name=csrf-token]").attr("content")
println("Authenticity token is: $authenticityToken") val authCookies = resAuth.cookies()
val cookies = resAuth.cookies() Log.i(javaClass.name, "Received authenticity token: $authenticityToken")
println("cookies: $cookies") Log.i(javaClass.name, "Received authenticity cookies: $authCookies")
val data = mapOf( val data = mapOf(
Pair("user[login]", EncryptedPreferences.login), Pair("user[login]", EncryptedPreferences.login),
@ -53,7 +53,7 @@ class AoDParser {
.method(Connection.Method.POST) .method(Connection.Method.POST)
.data(data) .data(data)
.postDataCharset("UTF-8") .postDataCharset("UTF-8")
.cookies(cookies) .cookies(authCookies)
.execute() .execute()
//println(resLogin.body()) //println(resLogin.body())
@ -122,6 +122,15 @@ class AoDParser {
//println(res) //println(res)
// parse additional info from the media page
res.select("table.vertical-table").select("tr").forEach {
when (it.select("th").text().toLowerCase(Locale.ROOT)) {
"produktionsjahr" -> media.info.year = it.select("td").text().toInt()
"fsk" -> media.info.age = it.select("td").text().toInt()
"episodenanzahl" -> media.info.episodesCount = it.select("td").text().toInt()
}
}
val playlists = res.select("input.streamstarter_html5").eachAttr("data-playlist") val playlists = res.select("input.streamstarter_html5").eachAttr("data-playlist")
val csrfToken = res.select("meta[name=csrf-token]").attr("content") val csrfToken = res.select("meta[name=csrf-token]").attr("content")
@ -176,10 +185,16 @@ class AoDParser {
.first().asJsonObject .first().asJsonObject
.get("file").asString .get("file").asString
val episodeTitle = it.asJsonObject.get("title").asString val episodeTitle = it.asJsonObject.get("title").asString
val episodePoster = it.asJsonObject.get("image").asString
val episodeDescription = it.asJsonObject.get("description").asString
val episodeNumber = episodeTitle.substringAfter(", Ep. ").toInt()
Episode( Episode(
episodeTitle, episodeTitle,
episodeStream episodeStream,
episodePoster,
episodeDescription,
episodeNumber
) )
} }

View File

@ -55,20 +55,26 @@ class MediaFragment(private val media: Media, private val tmdb: TMDBResponse) :
.into(image_poster) .into(image_poster)
text_title.text = media.title text_title.text = media.title
// TODO add year, fsk text_year.text = media.info.year.toString()
text_overview.text = if (tmdb.overview.isNotEmpty()) tmdb.overview else media.shortDesc text_age.text = media.info.age.toString()
text_overview.text = media.shortDesc //if (tmdb.overview.isNotEmpty()) tmdb.overview else media.shortDesc
// specific gui // specific gui
if (media.type == MediaType.TVSHOW) { if (media.type == MediaType.TVSHOW) {
val episodeTitles = media.episodes.map { it.title } adapterRecEpisodes = EpisodesAdapter(media.episodes, requireContext())
adapterRecEpisodes = EpisodesAdapter(episodeTitles)
viewManager = LinearLayoutManager(context) viewManager = LinearLayoutManager(context)
recycler_episodes.layoutManager = viewManager recycler_episodes.layoutManager = viewManager
recycler_episodes.adapter = adapterRecEpisodes recycler_episodes.adapter = adapterRecEpisodes
text_episodes_or_runtime.text = getString(R.string.text_episodes_count, media.info.episodesCount)
} else if (media.type == MediaType.MOVIE) { } else if (media.type == MediaType.MOVIE) {
recycler_episodes.visibility = View.GONE recycler_episodes.visibility = View.GONE
if (tmdb.runtime > 0) {
text_episodes_or_runtime.text = getString(R.string.text_runtime, tmdb.runtime)
} else {
text_episodes_or_runtime.visibility = View.GONE
}
} }
} }

View File

@ -16,7 +16,6 @@ import org.mosad.teapod.util.Media
class SearchFragment : Fragment() { class SearchFragment : Fragment() {
private val instance = this
private lateinit var adapter : CustomAdapter private lateinit var adapter : CustomAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -46,6 +45,8 @@ class SearchFragment : Fragment() {
private fun initActions() { private fun initActions() {
search_text.setOnQueryTextListener(object : SearchView.OnQueryTextListener { search_text.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean { override fun onQueryTextSubmit(query: String?): Boolean {
adapter.filter.filter(query)
adapter.notifyDataSetChanged()
return false return false
} }
@ -57,12 +58,14 @@ class SearchFragment : Fragment() {
}) })
list_search.setOnItemClickListener { _, _, position, _ -> list_search.setOnItemClickListener { _, _, position, _ ->
val media = adapter.getItem(position) as Media search_text.clearFocus() // remove focus from the SearchView
println("selected item is: ${media.title}") runBlocking {
val media = adapter.getItem(position) as Media
println("selected item is: ${media.title}")
val mainActivity = activity as MainActivity (activity as MainActivity).showDetailFragment(media).join()
mainActivity.showDetailFragment(media) }
} }
} }
} }

View File

@ -8,12 +8,14 @@ class DataTypes {
} }
} }
data class Media(val title: String, val link: String, val type: DataTypes.MediaType, val posterLink: String, val shortDesc : String, var episodes: List<Episode> = listOf()) { data class Media(val title: String, val link: String, val type: DataTypes.MediaType, val posterLink: String, val shortDesc : String, var episodes: List<Episode> = listOf(), val info : Info = Info()) {
override fun toString(): String { override fun toString(): String {
return title return title
} }
} }
data class Episode(val title: String = "", val streamUrl: String = "", val posterLink: String = "", var watched: Boolean = false) data class Info(var description: String = "", var year: Int = 0, var age: Int = 0, var episodesCount: Int = 0)
data class TMDBResponse(val title: String = "", val overview: String = "", val posterUrl: String = "", val backdropUrl: String = "") data class Episode(val title: String = "", val streamUrl: String = "", val posterLink: String = "", var description: String = "", var number: Int = 0, var watched: Boolean = false)
data class TMDBResponse(val id: Int = 0, val title: String = "", val overview: String = "", val posterUrl: String = "", val backdropUrl: String = "", var runtime: Int = 0)

View File

@ -1,13 +1,15 @@
package org.mosad.teapod.util package org.mosad.teapod.util
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import kotlinx.android.synthetic.main.component_episode.view.* import kotlinx.android.synthetic.main.component_episode.view.*
import org.mosad.teapod.R import org.mosad.teapod.R
class EpisodesAdapter(private val data: List<String>) : RecyclerView.Adapter<EpisodesAdapter.MyViewHolder>() { class EpisodesAdapter(private val data: List<Episode>, private val context: Context) : RecyclerView.Adapter<EpisodesAdapter.MyViewHolder>() {
var onItemClick: ((String, Int) -> Unit)? = null var onItemClick: ((String, Int) -> Unit)? = null
@ -18,7 +20,11 @@ class EpisodesAdapter(private val data: List<String>) : RecyclerView.Adapter<Epi
} }
override fun onBindViewHolder(holder: MyViewHolder, position: Int) { override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.view .text_episode_title.text = data[position] holder.view.text_episode_title.text = "Episode ${data[position].number} ${data[position].description}"
if (data[position].posterLink.isNotEmpty()) {
Glide.with(context).load(data[position].posterLink).into(holder.view.image_episode)
}
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
@ -28,7 +34,7 @@ class EpisodesAdapter(private val data: List<String>) : RecyclerView.Adapter<Epi
inner class MyViewHolder(val view: View) : RecyclerView.ViewHolder(view) { inner class MyViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
init { init {
view.setOnClickListener { view.setOnClickListener {
onItemClick?.invoke(data[adapterPosition], adapterPosition) onItemClick?.invoke(data[adapterPosition].title, adapterPosition)
} }
} }
} }

View File

@ -15,6 +15,7 @@ class TMDBApiController {
private val apiUrl = "https://api.themoviedb.org/3" private val apiUrl = "https://api.themoviedb.org/3"
private val searchMovieUrl = "$apiUrl/search/movie" private val searchMovieUrl = "$apiUrl/search/movie"
private val searchTVUrl = "$apiUrl/search/tv" private val searchTVUrl = "$apiUrl/search/tv"
private val getMovieUrl = "$apiUrl/movie"
private val apiKey = "de959cf9c07a08b5ca7cb51cda9a40c2" private val apiKey = "de959cf9c07a08b5ca7cb51cda9a40c2"
private val language = "de" private val language = "de"
private val preparedParameters = "?api_key=$apiKey&language=$language" private val preparedParameters = "?api_key=$apiKey&language=$language"
@ -44,11 +45,12 @@ class TMDBApiController {
return@async if (response.get("total_results").asInt > 0) { return@async if (response.get("total_results").asInt > 0) {
response.get("results").asJsonArray.first().asJsonObject.let { response.get("results").asJsonArray.first().asJsonObject.let {
val id = getStringNotNull(it,"id").toInt()
val overview = getStringNotNull(it,"overview") val overview = getStringNotNull(it,"overview")
val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
TMDBResponse("", overview, posterPath, backdropPath) TMDBResponse(id, "", overview, posterPath, backdropPath)
} }
} else { } else {
TMDBResponse() TMDBResponse()
@ -66,11 +68,13 @@ class TMDBApiController {
return@async if (response.get("total_results").asInt > 0) { return@async if (response.get("total_results").asInt > 0) {
response.get("results").asJsonArray.first().asJsonObject.let { response.get("results").asJsonArray.first().asJsonObject.let {
val id = getStringNotNull(it,"id").toInt()
val overview = getStringNotNull(it,"overview") val overview = getStringNotNull(it,"overview")
val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl)
val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl)
val runtime = getMovieRuntime(id)
TMDBResponse("", overview, posterPath, backdropPath) TMDBResponse(id, "", overview, posterPath, backdropPath, runtime)
} }
} else { } else {
TMDBResponse() TMDBResponse()
@ -80,6 +84,24 @@ class TMDBApiController {
}.await() }.await()
} }
/**
* currently only used for runtime, need a rework
*/
fun getMovieRuntime(id: Int): Int = runBlocking {
val url = URL("$getMovieUrl/$id?api_key=$apiKey&language=$language")
GlobalScope.async {
val response = JsonParser.parseString(url.readText()).asJsonObject
//println(response)
val runtime = getStringNotNull(response,"runtime").toInt()
println(runtime)
return@async runtime
}.await()
}
/** /**
* return memberName as string if it's not JsonNull, * return memberName as string if it's not JsonNull,
* else return an empty string * else return an empty string

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#B0B0B0"/>
<corners android:radius="3dp"/>
</shape>

View File

@ -16,10 +16,8 @@
<ImageView <ImageView
android:id="@+id/image_episode" android:id="@+id/image_episode"
android:layout_width="wrap_content" android:layout_width="128dp"
android:layout_height="wrap_content" android:layout_height="72dp"
android:layout_weight="1"
android:minWidth="48dp"
app:srcCompat="@drawable/ic_baseline_account_box_24" /> app:srcCompat="@drawable/ic_baseline_account_box_24" />
<TextView <TextView

View File

@ -15,7 +15,7 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" > android:orientation="vertical">
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -40,16 +40,52 @@
</FrameLayout> </FrameLayout>
<LinearLayout
android:id="@+id/linear_media_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/text_year"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="2dp"
android:text="@string/text_year_ex" />
<TextView
android:id="@+id/text_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:background="@drawable/shape_rounden_corner"
android:paddingStart="3dp"
android:paddingTop="2dp"
android:paddingEnd="3dp"
android:paddingBottom="2dp"
android:text="@string/text_age_ex" />
<TextView
android:id="@+id/text_episodes_or_runtime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:padding="2dp"
android:text="@string/text_episodes_count" />
</LinearLayout>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/button_play" android:id="@+id/button_play"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="7dp" android:layout_marginStart="7dp"
android:layout_marginTop="24dp" android:layout_marginTop="6dp"
android:layout_marginEnd="7dp" android:layout_marginEnd="7dp"
android:gravity="center" android:gravity="center"
android:textAllCaps="false"
android:text="@string/button_play" android:text="@string/button_play"
android:textAllCaps="false"
android:textColor="@android:color/primary_text_dark" android:textColor="@android:color/primary_text_dark"
android:textSize="16sp" android:textSize="16sp"
app:backgroundTint="#4A4141" app:backgroundTint="#4A4141"
@ -59,7 +95,7 @@
<TextView <TextView
android:id="@+id/text_title" android:id="@+id/text_title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="19dp" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginStart="7dp" android:layout_marginStart="7dp"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
@ -72,9 +108,9 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginStart="7dp" android:layout_marginStart="12dp"
android:layout_marginTop="10dp" android:layout_marginTop="7dp"
android:layout_marginEnd="7dp" android:layout_marginEnd="12dp"
android:text="@string/text_overview_ex" /> android:text="@string/text_overview_ex" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView

View File

@ -10,6 +10,8 @@
<!-- media fragment --> <!-- media fragment -->
<string name="button_play">Abspielen</string> <string name="button_play">Abspielen</string>
<string name="text_episodes_count">%1$d Episoden</string>
<string name="text_runtime">%1$d Minuten</string>
<!-- settings fragment --> <!-- settings fragment -->
<string name="account">Account</string> <string name="account">Account</string>

View File

@ -12,6 +12,10 @@
<string name="button_play">Play</string> <string name="button_play">Play</string>
<string name="text_title_ex" translatable="false">A Silent Voice</string> <string name="text_title_ex" translatable="false">A Silent Voice</string>
<string name="text_overview_ex" translatable="false">Shouya Ishida starts bullying the new girl in class …</string> <string name="text_overview_ex" translatable="false">Shouya Ishida starts bullying the new girl in class …</string>
<string name="text_year_ex" translatable="false">2016</string>
<string name="text_age_ex" translatable="false">6</string>
<string name="text_episodes_count">%1$d episodes</string>
<string name="text_runtime">%1$d Minutes</string>
<!-- settings fragment --> <!-- settings fragment -->
<string name="account">Account</string> <string name="account">Account</string>