From 4fd6f9ca7e9f43bafb86e44dce3371b7d4f748ac Mon Sep 17 00:00:00 2001 From: Jannik Date: Mon, 27 Dec 2021 22:50:29 +0100 Subject: [PATCH] add search for tv shows media items are currently not selectable, the app will crash --- .../teapod/parser/crunchyroll/Crunchyroll.kt | 18 +++-- .../teapod/parser/crunchyroll/DataTypes.kt | 22 +++++- .../teapod/ui/activity/main/MainActivity.kt | 12 ++- .../activity/main/fragments/HomeFragment.kt | 4 +- .../main/fragments/LibraryFragment.kt | 4 +- .../activity/main/fragments/SearchFragment.kt | 75 ++++++++++++++++--- .../main/viewmodel/MediaFragmentViewModel.kt | 3 +- .../teapod/util/adapter/MediaItemAdapter.kt | 52 ++----------- 8 files changed, 112 insertions(+), 78 deletions(-) diff --git a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt index f1d734a..21f9e64 100644 --- a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt +++ b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/Crunchyroll.kt @@ -134,18 +134,20 @@ object Crunchyroll { return browseResult } - // // TODO locale de-DE, type - suspend fun search(query: String, n: Int = 10) { + /** + * TODO + */ + suspend fun search(query: String, n: Int = 10): SearchResult { val searchEndpoint = "/content/v1/search" - val parameters = listOf("q" to query, "n" to n) + val parameters = listOf("q" to query, "n" to n, "locale" to locale, "type" to "series") val result = request(searchEndpoint, parameters) - println("${result.component1()?.obj()?.get("total")}") + // TODO episodes have thumbnails as image, and not poster_tall/poster_tall, + // to work around this, for now only tv shows are supported - val test = json.decodeFromString(result.component1()?.obj()?.toString()!!) - println(test.items.size) - - // TODO return + return result.component1()?.obj()?.let { + json.decodeFromString(it.toString()) + } ?: NoneSearchResult } /** diff --git a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt index 6086b10..1f9edba 100644 --- a/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt +++ b/app/src/main/java/org/mosad/teapod/parser/crunchyroll/DataTypes.kt @@ -13,9 +13,29 @@ enum class SortBy(val str: String) { POPULARITY("popularity") } +/** + * Search data type + */ +@Serializable +data class SearchResult( + @SerialName("total") val total: Int, + @SerialName("items") val items: List +) + +@Serializable +data class SearchCollection( + @SerialName("type") val type: String, + @SerialName("items") val items: List +) + +val NoneSearchResult = SearchResult(0, emptyList()) + + + @Serializable data class BrowseResult(val total: Int, val items: List) +// the data class Item is used in browse and search @Serializable data class Item( val id: String, @@ -38,7 +58,7 @@ data class Images(val poster_tall: List>, val poster_wide: List { diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt index 00f992c..58f5ec0 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/LibraryFragment.kt @@ -45,7 +45,7 @@ class LibraryFragment : Fragment() { adapter = MediaItemAdapter(itemList) adapter.onItemClick = { mediaIdStr, _ -> - activity?.showFragment(MediaFragment(mediaIdStr = mediaIdStr)) + activity?.showFragment(MediaFragment(mediaIdStr)) } binding.recyclerMediaLibrary.adapter = adapter @@ -78,9 +78,7 @@ class LibraryFragment : Fragment() { }) nextItemIndex += pageSize - adapter.updateMediaList(itemList) adapter.notifyItemRangeInserted(firstNewItemIndex, pageSize) - isLoading = false } } diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt index 29d9bcd..ca924d4 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/fragments/SearchFragment.kt @@ -7,16 +7,24 @@ import android.view.ViewGroup import android.widget.SearchView import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.async import kotlinx.coroutines.launch import org.mosad.teapod.databinding.FragmentSearchBinding -import org.mosad.teapod.util.decoration.MediaItemDecoration +import org.mosad.teapod.parser.crunchyroll.Crunchyroll +import org.mosad.teapod.util.ItemMedia import org.mosad.teapod.util.adapter.MediaItemAdapter +import org.mosad.teapod.util.decoration.MediaItemDecoration import org.mosad.teapod.util.showFragment class SearchFragment : Fragment() { private lateinit var binding: FragmentSearchBinding - private var adapter : MediaItemAdapter? = null + private lateinit var adapter: MediaItemAdapter + + private val itemList = arrayListOf() + private var searchJob: Job? = null + private var oldSearchQuery = "" override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = FragmentSearchBinding.inflate(inflater, container, false) @@ -29,10 +37,10 @@ class SearchFragment : Fragment() { lifecycleScope.launch { // create and set the adapter, needs context context?.let { - adapter = MediaItemAdapter(emptyList()) // TODO - adapter!!.onItemClick = { mediaId, _ -> + adapter = MediaItemAdapter(itemList) + adapter.onItemClick = { mediaIdStr, _ -> binding.searchText.clearFocus() - activity?.showFragment(MediaFragment("")) //(mediaId)) + activity?.showFragment(MediaFragment(mediaIdStr)) } binding.recyclerMediaSearch.adapter = adapter @@ -46,16 +54,65 @@ class SearchFragment : Fragment() { private fun initActions() { binding.searchText.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { - adapter?.filter?.filter(query) - adapter?.notifyDataSetChanged() + query?.let { search(it) } return false } override fun onQueryTextChange(newText: String?): Boolean { - adapter?.filter?.filter(newText) - adapter?.notifyDataSetChanged() + newText?.let { search(it) } return false } }) } + + private fun search(query: String) { + // if the query hasn't changed since the last successful search, return + if (query == oldSearchQuery) return + + // cancel search job if one is already running + if (searchJob?.isActive == true) searchJob?.cancel() + + searchJob = lifecycleScope.async { + // TODO maybe wait a few ms (500ms?) before searching, if the user inputs any other chars + val results = Crunchyroll.search(query, 50) + + itemList.clear() // TODO needs clean up + + // TODO add top results first heading + itemList.addAll(results.items[0].items.map { item -> + ItemMedia(item.id, item.title, item.images.poster_wide[0][0].source) + }) + + // TODO currently only tv shows are supported, hence only the first items array + // should be always present + +// // TODO add tv shows heading +// if (results.items.size >= 2) { +// itemList.addAll(results.items[1].items.map { item -> +// ItemMedia(item.id, item.title, item.images.poster_wide[0][0].source) +// }) +// } +// +// // TODO add movies heading +// if (results.items.size >= 3) { +// itemList.addAll(results.items[2].items.map { item -> +// ItemMedia(item.id, item.title, item.images.poster_wide[0][0].source) +// }) +// } +// +// // TODO add episodes heading +// if (results.items.size >= 4) { +// itemList.addAll(results.items[3].items.map { item -> +// ItemMedia(item.id, item.title, item.images.poster_wide[0][0].source) +// }) +// } + + adapter.notifyDataSetChanged() + //adapter.notifyItemRangeInserted(0, itemList.size) + + // after successfully searching the query term, add it as old query, to make sure we + // don't search again if the query hasn't changed + oldSearchQuery = query + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt b/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt index d422ca1..ac73a6e 100644 --- a/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt +++ b/app/src/main/java/org/mosad/teapod/ui/activity/main/viewmodel/MediaFragmentViewModel.kt @@ -41,6 +41,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic println("loading crunchyroll media $crunchyId") // TODO info also in browse result item + // TODO doesn't support search mediaCrunchy = Crunchyroll.browsingCache.find { it -> it.id == crunchyId } ?: NoneItem @@ -61,7 +62,7 @@ class MediaFragmentViewModel(application: Application) : AndroidViewModel(applic // use tmdb search to get media info TODO media type is hardcoded, use type info from browse result once implemented mediaMeta = null // set mediaMeta to null, if metaDB doesn't know the media - val tmdbId = tmdbApiController.search(stripTitleInfo(mediaCrunchy.title), MediaType.TVSHOW) + val tmdbId = tmdbApiController.search(mediaCrunchy.title, MediaType.TVSHOW) tmdbResult = when (MediaType.TVSHOW) { MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId) diff --git a/app/src/main/java/org/mosad/teapod/util/adapter/MediaItemAdapter.kt b/app/src/main/java/org/mosad/teapod/util/adapter/MediaItemAdapter.kt index 63747d4..1097426 100644 --- a/app/src/main/java/org/mosad/teapod/util/adapter/MediaItemAdapter.kt +++ b/app/src/main/java/org/mosad/teapod/util/adapter/MediaItemAdapter.kt @@ -2,19 +2,14 @@ package org.mosad.teapod.util.adapter import android.view.LayoutInflater import android.view.ViewGroup -import android.widget.Filter -import android.widget.Filterable import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import org.mosad.teapod.databinding.ItemMediaBinding import org.mosad.teapod.util.ItemMedia -import java.util.* -class MediaItemAdapter(private val initMedia: List) : RecyclerView.Adapter(), Filterable { +class MediaItemAdapter(private val items: List) : RecyclerView.Adapter() { var onItemClick: ((id: String, position: Int) -> Unit)? = null - private val filter = MediaFilter() - private var filteredMedia = initMedia.map { it.copy() } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaItemAdapter.MediaViewHolder { return MediaViewHolder(ItemMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false)) @@ -22,21 +17,13 @@ class MediaItemAdapter(private val initMedia: List) : RecyclerView.Ad override fun onBindViewHolder(holder: MediaItemAdapter.MediaViewHolder, position: Int) { holder.binding.root.apply { - holder.binding.textTitle.text = filteredMedia[position].title - Glide.with(context).load(filteredMedia[position].posterUrl).into(holder.binding.imagePoster) + holder.binding.textTitle.text = items[position].title + Glide.with(context).load(items[position].posterUrl).into(holder.binding.imagePoster) } } override fun getItemCount(): Int { - return filteredMedia.size - } - - override fun getFilter(): Filter { - return filter - } - - fun updateMediaList(mediaList: List) { - filteredMedia = mediaList + return items.size } inner class MediaViewHolder(val binding: ItemMediaBinding) : @@ -44,40 +31,11 @@ class MediaItemAdapter(private val initMedia: List) : RecyclerView.Ad init { binding.root.setOnClickListener { onItemClick?.invoke( - filteredMedia[bindingAdapterPosition].id, + items[bindingAdapterPosition].id, bindingAdapterPosition ) } } } - inner class MediaFilter : Filter() { - override fun performFiltering(constraint: CharSequence?): FilterResults { - val filterTerm = constraint.toString().lowercase(Locale.ROOT) - val results = FilterResults() - - val filteredList = if (filterTerm.isEmpty()) { - initMedia - } else { - initMedia.filter { - it.title.lowercase(Locale.ROOT).contains(filterTerm) - } - } - - results.values = filteredList - results.count = filteredList.size - - return results - } - - @Suppress("unchecked_cast") - /** - * suppressing unchecked cast is safe, since we only use Media - */ - override fun publishResults(constraint: CharSequence?, results: FilterResults?) { - filteredMedia = results?.values as List - notifyDataSetChanged() - } - } - } \ No newline at end of file