add search for tv shows
media items are currently not selectable, the app will crash
This commit is contained in:
parent
63ce910ec5
commit
4fd6f9ca7e
|
@ -134,18 +134,20 @@ object Crunchyroll {
|
||||||
return browseResult
|
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 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)
|
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()!!)
|
return result.component1()?.obj()?.let {
|
||||||
println(test.items.size)
|
json.decodeFromString(it.toString())
|
||||||
|
} ?: NoneSearchResult
|
||||||
// TODO return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -13,9 +13,29 @@ enum class SortBy(val str: String) {
|
||||||
POPULARITY("popularity")
|
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
|
@Serializable
|
||||||
data class BrowseResult(val total: Int, val items: List<Item>)
|
data class BrowseResult(val total: Int, val items: List<Item>)
|
||||||
|
|
||||||
|
// the data class Item is used in browse and search
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Item(
|
data class Item(
|
||||||
val id: String,
|
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)
|
data class Poster(val height: Int, val width: Int, val source: String, val type: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Series return type
|
* Series data type
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Series(
|
data class Series(
|
||||||
|
|
|
@ -135,10 +135,10 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
||||||
private fun load() {
|
private fun load() {
|
||||||
val time = measureTimeMillis {
|
val time = measureTimeMillis {
|
||||||
// start the initial loading
|
// start the initial loading
|
||||||
val loadingJob = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope"))
|
// val loadingJob = CoroutineScope(Dispatchers.IO + CoroutineName("InitialLoadingScope"))
|
||||||
.async {
|
// .async {
|
||||||
launch { MetaDBController.list() }
|
// launch { MetaDBController.list() }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// load all saved stuff here
|
// load all saved stuff here
|
||||||
Preferences.load(this)
|
Preferences.load(this)
|
||||||
|
@ -153,8 +153,6 @@ class MainActivity : AppCompatActivity(), NavigationBarView.OnItemSelectedListen
|
||||||
runBlocking { Crunchyroll.index() }
|
runBlocking { Crunchyroll.index() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// if (EncryptedPreferences.password.isEmpty()) {
|
// if (EncryptedPreferences.password.isEmpty()) {
|
||||||
// showOnboarding()
|
// showOnboarding()
|
||||||
// } else {
|
// } 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")
|
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)
|
* * only update actual change and not all data (performance)
|
||||||
*/
|
*/
|
||||||
fun updateMyListMedia() {
|
fun updateMyListMedia() {
|
||||||
adapterMyList.updateMediaList(mapMyListToItemMedia())
|
//adapterMyList.updateMediaList(mapMyListToItemMedia())
|
||||||
adapterMyList.notifyDataSetChanged()
|
//adapterMyList.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapMyListToItemMedia(): List<ItemMedia> {
|
private fun mapMyListToItemMedia(): List<ItemMedia> {
|
||||||
|
|
|
@ -45,7 +45,7 @@ class LibraryFragment : Fragment() {
|
||||||
|
|
||||||
adapter = MediaItemAdapter(itemList)
|
adapter = MediaItemAdapter(itemList)
|
||||||
adapter.onItemClick = { mediaIdStr, _ ->
|
adapter.onItemClick = { mediaIdStr, _ ->
|
||||||
activity?.showFragment(MediaFragment(mediaIdStr = mediaIdStr))
|
activity?.showFragment(MediaFragment(mediaIdStr))
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.recyclerMediaLibrary.adapter = adapter
|
binding.recyclerMediaLibrary.adapter = adapter
|
||||||
|
@ -78,9 +78,7 @@ class LibraryFragment : Fragment() {
|
||||||
})
|
})
|
||||||
nextItemIndex += pageSize
|
nextItemIndex += pageSize
|
||||||
|
|
||||||
adapter.updateMediaList(itemList)
|
|
||||||
adapter.notifyItemRangeInserted(firstNewItemIndex, pageSize)
|
adapter.notifyItemRangeInserted(firstNewItemIndex, pageSize)
|
||||||
|
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,16 +7,24 @@ import android.view.ViewGroup
|
||||||
import android.widget.SearchView
|
import android.widget.SearchView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.mosad.teapod.databinding.FragmentSearchBinding
|
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.adapter.MediaItemAdapter
|
||||||
|
import org.mosad.teapod.util.decoration.MediaItemDecoration
|
||||||
import org.mosad.teapod.util.showFragment
|
import org.mosad.teapod.util.showFragment
|
||||||
|
|
||||||
class SearchFragment : Fragment() {
|
class SearchFragment : Fragment() {
|
||||||
|
|
||||||
private lateinit var binding: FragmentSearchBinding
|
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 {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
binding = FragmentSearchBinding.inflate(inflater, container, false)
|
binding = FragmentSearchBinding.inflate(inflater, container, false)
|
||||||
|
@ -29,10 +37,10 @@ class SearchFragment : Fragment() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
// create and set the adapter, needs context
|
// create and set the adapter, needs context
|
||||||
context?.let {
|
context?.let {
|
||||||
adapter = MediaItemAdapter(emptyList()) // TODO
|
adapter = MediaItemAdapter(itemList)
|
||||||
adapter!!.onItemClick = { mediaId, _ ->
|
adapter.onItemClick = { mediaIdStr, _ ->
|
||||||
binding.searchText.clearFocus()
|
binding.searchText.clearFocus()
|
||||||
activity?.showFragment(MediaFragment("")) //(mediaId))
|
activity?.showFragment(MediaFragment(mediaIdStr))
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.recyclerMediaSearch.adapter = adapter
|
binding.recyclerMediaSearch.adapter = adapter
|
||||||
|
@ -46,16 +54,65 @@ class SearchFragment : Fragment() {
|
||||||
private fun initActions() {
|
private fun initActions() {
|
||||||
binding.searchText.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
binding.searchText.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
adapter?.filter?.filter(query)
|
query?.let { search(it) }
|
||||||
adapter?.notifyDataSetChanged()
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
adapter?.filter?.filter(newText)
|
newText?.let { search(it) }
|
||||||
adapter?.notifyDataSetChanged()
|
|
||||||
return false
|
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")
|
println("loading crunchyroll media $crunchyId")
|
||||||
|
|
||||||
// TODO info also in browse result item
|
// TODO info also in browse result item
|
||||||
|
// TODO doesn't support search
|
||||||
mediaCrunchy = Crunchyroll.browsingCache.find { it ->
|
mediaCrunchy = Crunchyroll.browsingCache.find { it ->
|
||||||
it.id == crunchyId
|
it.id == crunchyId
|
||||||
} ?: NoneItem
|
} ?: 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
|
// 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
|
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) {
|
tmdbResult = when (MediaType.TVSHOW) {
|
||||||
MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId)
|
MediaType.MOVIE -> tmdbApiController.getMovieDetails(tmdbId)
|
||||||
|
|
|
@ -2,19 +2,14 @@ package org.mosad.teapod.util.adapter
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Filter
|
|
||||||
import android.widget.Filterable
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import org.mosad.teapod.databinding.ItemMediaBinding
|
import org.mosad.teapod.databinding.ItemMediaBinding
|
||||||
import org.mosad.teapod.util.ItemMedia
|
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
|
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 {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaItemAdapter.MediaViewHolder {
|
||||||
return MediaViewHolder(ItemMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
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) {
|
override fun onBindViewHolder(holder: MediaItemAdapter.MediaViewHolder, position: Int) {
|
||||||
holder.binding.root.apply {
|
holder.binding.root.apply {
|
||||||
holder.binding.textTitle.text = filteredMedia[position].title
|
holder.binding.textTitle.text = items[position].title
|
||||||
Glide.with(context).load(filteredMedia[position].posterUrl).into(holder.binding.imagePoster)
|
Glide.with(context).load(items[position].posterUrl).into(holder.binding.imagePoster)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
return filteredMedia.size
|
return items.size
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilter(): Filter {
|
|
||||||
return filter
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateMediaList(mediaList: List<ItemMedia>) {
|
|
||||||
filteredMedia = mediaList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class MediaViewHolder(val binding: ItemMediaBinding) :
|
inner class MediaViewHolder(val binding: ItemMediaBinding) :
|
||||||
|
@ -44,40 +31,11 @@ class MediaItemAdapter(private val initMedia: List<ItemMedia>) : RecyclerView.Ad
|
||||||
init {
|
init {
|
||||||
binding.root.setOnClickListener {
|
binding.root.setOnClickListener {
|
||||||
onItemClick?.invoke(
|
onItemClick?.invoke(
|
||||||
filteredMedia[bindingAdapterPosition].id,
|
items[bindingAdapterPosition].id,
|
||||||
bindingAdapterPosition
|
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