improved MediaFragment UI
* fix searchview not losing focus when media is selected
This commit is contained in:
		| @ -9,6 +9,8 @@ import androidx.appcompat.app.AppCompatActivity | ||||
| import androidx.fragment.app.Fragment | ||||
| import androidx.fragment.app.commit | ||||
| 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.preferences.EncryptedPreferences | ||||
| 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 | ||||
|  | ||||
|         val tmdb = TMDBApiController().search(media.title, media.type) | ||||
|  | ||||
| @ -36,10 +36,10 @@ class AoDParser { | ||||
|                 .execute() | ||||
|  | ||||
|             val authenticityToken = resAuth.parse().select("meta[name=csrf-token]").attr("content") | ||||
|             println("Authenticity token is: $authenticityToken") | ||||
|             val authCookies = resAuth.cookies() | ||||
|  | ||||
|             val cookies = resAuth.cookies() | ||||
|             println("cookies: $cookies") | ||||
|             Log.i(javaClass.name, "Received authenticity token: $authenticityToken") | ||||
|             Log.i(javaClass.name, "Received authenticity cookies: $authCookies") | ||||
|  | ||||
|             val data = mapOf( | ||||
|                 Pair("user[login]", EncryptedPreferences.login), | ||||
| @ -53,7 +53,7 @@ class AoDParser { | ||||
|                 .method(Connection.Method.POST) | ||||
|                 .data(data) | ||||
|                 .postDataCharset("UTF-8") | ||||
|                 .cookies(cookies) | ||||
|                 .cookies(authCookies) | ||||
|                 .execute() | ||||
|  | ||||
|             //println(resLogin.body()) | ||||
| @ -122,6 +122,15 @@ class AoDParser { | ||||
|  | ||||
|             //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 csrfToken = res.select("meta[name=csrf-token]").attr("content") | ||||
|  | ||||
| @ -176,10 +185,16 @@ class AoDParser { | ||||
|                             .first().asJsonObject | ||||
|                             .get("file").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( | ||||
|                             episodeTitle, | ||||
|                             episodeStream | ||||
|                             episodeStream, | ||||
|                             episodePoster, | ||||
|                             episodeDescription, | ||||
|                             episodeNumber | ||||
|                         ) | ||||
|                     } | ||||
|  | ||||
|  | ||||
| @ -55,20 +55,26 @@ class MediaFragment(private val media: Media, private val tmdb: TMDBResponse) : | ||||
|             .into(image_poster) | ||||
|  | ||||
|         text_title.text = media.title | ||||
|         // TODO add  year, fsk | ||||
|         text_overview.text = if (tmdb.overview.isNotEmpty()) tmdb.overview else media.shortDesc | ||||
|         text_year.text = media.info.year.toString() | ||||
|         text_age.text = media.info.age.toString() | ||||
|         text_overview.text = media.shortDesc //if (tmdb.overview.isNotEmpty()) tmdb.overview else media.shortDesc | ||||
|  | ||||
|         // specific gui | ||||
|         if (media.type == MediaType.TVSHOW) { | ||||
|             val episodeTitles = media.episodes.map { it.title } | ||||
|  | ||||
|             adapterRecEpisodes = EpisodesAdapter(episodeTitles) | ||||
|             adapterRecEpisodes = EpisodesAdapter(media.episodes, requireContext()) | ||||
|             viewManager = LinearLayoutManager(context) | ||||
|             recycler_episodes.layoutManager = viewManager | ||||
|             recycler_episodes.adapter = adapterRecEpisodes | ||||
|  | ||||
|             text_episodes_or_runtime.text = getString(R.string.text_episodes_count, media.info.episodesCount) | ||||
|         } else if (media.type == MediaType.MOVIE) { | ||||
|             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 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -16,7 +16,6 @@ import org.mosad.teapod.util.Media | ||||
|  | ||||
| class SearchFragment : Fragment() { | ||||
|  | ||||
|     private val instance = this | ||||
|     private lateinit var adapter : CustomAdapter | ||||
|  | ||||
|     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { | ||||
| @ -46,6 +45,8 @@ class SearchFragment : Fragment() { | ||||
|     private fun initActions() { | ||||
|         search_text.setOnQueryTextListener(object : SearchView.OnQueryTextListener { | ||||
|             override fun onQueryTextSubmit(query: String?): Boolean { | ||||
|                 adapter.filter.filter(query) | ||||
|                 adapter.notifyDataSetChanged() | ||||
|                 return false | ||||
|             } | ||||
|  | ||||
| @ -57,12 +58,14 @@ class SearchFragment : Fragment() { | ||||
|         }) | ||||
|  | ||||
|         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 | ||||
|             mainActivity.showDetailFragment(media) | ||||
|                 (activity as MainActivity).showDetailFragment(media).join() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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 { | ||||
|         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) | ||||
|  | ||||
| @ -1,13 +1,15 @@ | ||||
| package org.mosad.teapod.util | ||||
|  | ||||
| import android.content.Context | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import com.bumptech.glide.Glide | ||||
| import kotlinx.android.synthetic.main.component_episode.view.* | ||||
| 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 | ||||
|  | ||||
| @ -18,7 +20,11 @@ class EpisodesAdapter(private val data: List<String>) : RecyclerView.Adapter<Epi | ||||
|     } | ||||
|  | ||||
|     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 { | ||||
| @ -28,7 +34,7 @@ class EpisodesAdapter(private val data: List<String>) : RecyclerView.Adapter<Epi | ||||
|     inner class MyViewHolder(val view: View) : RecyclerView.ViewHolder(view) { | ||||
|         init { | ||||
|             view.setOnClickListener { | ||||
|                 onItemClick?.invoke(data[adapterPosition], adapterPosition) | ||||
|                 onItemClick?.invoke(data[adapterPosition].title, adapterPosition) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -15,6 +15,7 @@ class TMDBApiController { | ||||
|     private val apiUrl = "https://api.themoviedb.org/3" | ||||
|     private val searchMovieUrl = "$apiUrl/search/movie" | ||||
|     private val searchTVUrl = "$apiUrl/search/tv" | ||||
|     private val getMovieUrl = "$apiUrl/movie" | ||||
|     private val apiKey = "de959cf9c07a08b5ca7cb51cda9a40c2" | ||||
|     private val language = "de" | ||||
|     private val preparedParameters = "?api_key=$apiKey&language=$language" | ||||
| @ -44,11 +45,12 @@ class TMDBApiController { | ||||
|  | ||||
|             return@async if (response.get("total_results").asInt > 0) { | ||||
|                 response.get("results").asJsonArray.first().asJsonObject.let { | ||||
|                     val id = getStringNotNull(it,"id").toInt() | ||||
|                     val overview = getStringNotNull(it,"overview") | ||||
|                     val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) | ||||
|                     val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) | ||||
|  | ||||
|                     TMDBResponse("", overview, posterPath, backdropPath) | ||||
|                     TMDBResponse(id, "", overview, posterPath, backdropPath) | ||||
|                 } | ||||
|             } else { | ||||
|                 TMDBResponse() | ||||
| @ -66,11 +68,13 @@ class TMDBApiController { | ||||
|  | ||||
|             return@async if (response.get("total_results").asInt > 0) { | ||||
|                 response.get("results").asJsonArray.first().asJsonObject.let { | ||||
|                     val id = getStringNotNull(it,"id").toInt() | ||||
|                     val overview = getStringNotNull(it,"overview") | ||||
|                     val posterPath = getStringNotNullPrefix(it, "poster_path", imageUrl) | ||||
|                     val backdropPath = getStringNotNullPrefix(it, "backdrop_path", imageUrl) | ||||
|                     val runtime = getMovieRuntime(id) | ||||
|  | ||||
|                     TMDBResponse("", overview, posterPath, backdropPath) | ||||
|                     TMDBResponse(id, "", overview, posterPath, backdropPath, runtime) | ||||
|                 } | ||||
|             } else { | ||||
|                 TMDBResponse() | ||||
| @ -80,6 +84,24 @@ class TMDBApiController { | ||||
|         }.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, | ||||
|      * else return an empty string | ||||
|  | ||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/shape_rounden_corner.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/shape_rounden_corner.xml
									
									
									
									
									
										Normal 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> | ||||
| @ -16,10 +16,8 @@ | ||||
|  | ||||
|         <ImageView | ||||
|             android:id="@+id/image_episode" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_weight="1" | ||||
|             android:minWidth="48dp" | ||||
|             android:layout_width="128dp" | ||||
|             android:layout_height="72dp" | ||||
|             app:srcCompat="@drawable/ic_baseline_account_box_24" /> | ||||
|  | ||||
|         <TextView | ||||
|  | ||||
| @ -15,7 +15,7 @@ | ||||
|         <LinearLayout | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:orientation="vertical" > | ||||
|             android:orientation="vertical"> | ||||
|  | ||||
|             <FrameLayout | ||||
|                 android:layout_width="match_parent" | ||||
| @ -40,16 +40,52 @@ | ||||
|  | ||||
|             </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 | ||||
|                 android:id="@+id/button_play" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_marginStart="7dp" | ||||
|                 android:layout_marginTop="24dp" | ||||
|                 android:layout_marginTop="6dp" | ||||
|                 android:layout_marginEnd="7dp" | ||||
|                 android:gravity="center" | ||||
|                 android:textAllCaps="false" | ||||
|                 android:text="@string/button_play" | ||||
|                 android:textAllCaps="false" | ||||
|                 android:textColor="@android:color/primary_text_dark" | ||||
|                 android:textSize="16sp" | ||||
|                 app:backgroundTint="#4A4141" | ||||
| @ -59,7 +95,7 @@ | ||||
|             <TextView | ||||
|                 android:id="@+id/text_title" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="19dp" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_gravity="center" | ||||
|                 android:layout_marginStart="7dp" | ||||
|                 android:layout_marginTop="12dp" | ||||
| @ -72,9 +108,9 @@ | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:layout_gravity="center" | ||||
|                 android:layout_marginStart="7dp" | ||||
|                 android:layout_marginTop="10dp" | ||||
|                 android:layout_marginEnd="7dp" | ||||
|                 android:layout_marginStart="12dp" | ||||
|                 android:layout_marginTop="7dp" | ||||
|                 android:layout_marginEnd="12dp" | ||||
|                 android:text="@string/text_overview_ex" /> | ||||
|  | ||||
|             <androidx.recyclerview.widget.RecyclerView | ||||
|  | ||||
| @ -10,6 +10,8 @@ | ||||
|  | ||||
|     <!-- media fragment --> | ||||
|     <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 --> | ||||
|     <string name="account">Account</string> | ||||
|  | ||||
| @ -12,6 +12,10 @@ | ||||
|     <string name="button_play">Play</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_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 --> | ||||
|     <string name="account">Account</string> | ||||
|  | ||||
		Reference in New Issue
	
	Block a user