add search for tv shows
media items are currently not selectable, the app will crashpull/49/head
parent
63ce910ec5
commit
4fd6f9ca7e
|
@ -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<BrowseResult>(result.component1()?.obj()?.toString()!!)
|
||||
println(test.items.size)
|
||||
|
||||
// TODO return
|
||||
return result.component1()?.obj()?.let {
|
||||
json.decodeFromString(it.toString())
|
||||
} ?: NoneSearchResult
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<SearchCollection>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SearchCollection(
|
||||
@SerialName("type") val type: String,
|
||||
@SerialName("items") val items: List<Item>
|
||||
)
|
||||
|
||||
val NoneSearchResult = SearchResult(0, emptyList())
|
||||
|
||||
|
||||
|
||||
@Serializable
|
||||
data class BrowseResult(val total: Int, val items: List<Item>)
|
||||
|
||||
// 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<List<Poster>>, val poster_wide: List<Lis
|
|||
data class Poster(val height: Int, val width: Int, val source: String, val type: String)
|
||||
|
||||
/**
|
||||
* Series return type
|
||||
* Series data type
|
||||
*/
|
||||
@Serializable
|
||||
data class Series(
|
||||
|
|
|
@ -135,10 +135,10 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
|||
private fun load() {
|
||||
val time = measureTimeMillis {
|
||||
// start the initial loading
|
||||
val loadingJob = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope"))
|
||||
.async {
|
||||
launch { MetaDBController.list() }
|
||||
}
|
||||
// val loadingJob = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope"))
|
||||
// .async {
|
||||
// launch { MetaDBController.list() }
|
||||
// }
|
||||
|
||||
// load all saved stuff here
|
||||
Preferences.load(this)
|
||||
|
@ -153,8 +153,6 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
|||
runBlocking { Crunchyroll.index() }
|
||||
}
|
||||
|
||||
|
||||
|
||||
// if (EncryptedPreferences.password.isEmpty()) {
|
||||
// showOnboarding()
|
||||
// } else {
|
||||
|
@ -174,7 +172,7 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
|||
// }
|
||||
// }
|
||||
|
||||
runBlocking { loadingJob.await() } // wait for initial loading to finish
|
||||
// runBlocking { loadingJob.await() } // wait for initial loading to finish
|
||||
}
|
||||
Log.i(javaClass.name, "loading and login in $time ms")
|
||||
|
||||
|
|
|
@ -147,8 +147,8 @@ class HomeFragment : Fragment() {
|
|||
* * only update actual change and not all data (performance)
|
||||
*/
|
||||
fun updateMyListMedia() {
|
||||
adapterMyList.updateMediaList(mapMyListToItemMedia())
|
||||
adapterMyList.notifyDataSetChanged()
|
||||
//adapterMyList.updateMediaList(mapMyListToItemMedia())
|
||||
//adapterMyList.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun mapMyListToItemMedia(): List<ItemMedia> {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ItemMedia>()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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<ItemMedia>) : RecyclerView.Adapter<MediaItemAdapter.MediaViewHolder>(), Filterable {
|
||||
class MediaItemAdapter(private val items: List<ItemMedia>) : RecyclerView.Adapter<MediaItemAdapter.MediaViewHolder>() {
|
||||
|
||||
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<ItemMedia>) : 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<ItemMedia>) {
|
||||
filteredMedia = mediaList
|
||||
return items.size
|
||||
}
|
||||
|
||||
inner class MediaViewHolder(val binding: ItemMediaBinding) :
|
||||
|
@ -44,40 +31,11 @@ class MediaItemAdapter(private val initMedia: List<ItemMedia>) : 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<ItemMedia>
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue