446 lines
18 KiB
Kotlin
446 lines
18 KiB
Kotlin
package com.lagradost.cloudstream3.ui.search
|
|
|
|
import android.content.res.Configuration
|
|
import android.os.Bundle
|
|
import android.view.LayoutInflater
|
|
import android.view.View
|
|
import android.view.ViewGroup
|
|
import android.view.WindowManager
|
|
import android.widget.AbsListView
|
|
import android.widget.ArrayAdapter
|
|
import android.widget.ImageView
|
|
import android.widget.ListView
|
|
import androidx.appcompat.widget.SearchView
|
|
import androidx.core.view.isVisible
|
|
import androidx.fragment.app.Fragment
|
|
import androidx.fragment.app.activityViewModels
|
|
import androidx.preference.PreferenceManager
|
|
import androidx.recyclerview.widget.GridLayoutManager
|
|
import androidx.recyclerview.widget.RecyclerView
|
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
import com.google.android.material.button.MaterialButton
|
|
import com.lagradost.cloudstream3.*
|
|
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
|
|
import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality
|
|
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
|
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
|
import com.lagradost.cloudstream3.APIHolder.getApiSettings
|
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
|
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
|
|
import com.lagradost.cloudstream3.mvvm.Resource
|
|
import com.lagradost.cloudstream3.mvvm.logError
|
|
import com.lagradost.cloudstream3.mvvm.observe
|
|
import com.lagradost.cloudstream3.ui.APIRepository
|
|
import com.lagradost.cloudstream3.ui.home.HomeFragment
|
|
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips
|
|
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.currentSpan
|
|
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList
|
|
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.updateChips
|
|
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
|
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
|
import com.lagradost.cloudstream3.utils.Coroutines.main
|
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
|
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
|
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
|
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
|
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
|
|
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
|
import kotlinx.android.synthetic.main.fragment_search.*
|
|
import kotlinx.android.synthetic.main.tvtypes_chips.*
|
|
import java.util.concurrent.locks.ReentrantLock
|
|
|
|
const val SEARCH_PREF_TAGS = "search_pref_tags"
|
|
const val SEARCH_PREF_PROVIDERS = "search_pref_providers"
|
|
|
|
class SearchFragment : Fragment() {
|
|
companion object {
|
|
fun List<SearchResponse>.filterSearchResponse(): List<SearchResponse> {
|
|
return this.filter { response ->
|
|
if (response is AnimeSearchResponse) {
|
|
val status = response.dubStatus
|
|
(status.isNullOrEmpty()) || (status.any {
|
|
APIRepository.dubStatusActive.contains(it)
|
|
})
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private val searchViewModel: SearchViewModel by activityViewModels()
|
|
|
|
override fun onCreateView(
|
|
inflater: LayoutInflater,
|
|
container: ViewGroup?,
|
|
savedInstanceState: Bundle?,
|
|
): View? {
|
|
activity?.window?.setSoftInputMode(
|
|
WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
|
|
)
|
|
return inflater.inflate(R.layout.fragment_search, container, false)
|
|
}
|
|
|
|
private fun fixGrid() {
|
|
activity?.getSpanCount()?.let {
|
|
currentSpan = it
|
|
}
|
|
search_autofit_results.spanCount = currentSpan
|
|
currentSpan = currentSpan
|
|
HomeFragment.configEvent.invoke(currentSpan)
|
|
}
|
|
|
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
super.onConfigurationChanged(newConfig)
|
|
fixGrid()
|
|
}
|
|
|
|
override fun onDestroyView() {
|
|
hideKeyboard()
|
|
super.onDestroyView()
|
|
}
|
|
|
|
override fun onResume() {
|
|
super.onResume()
|
|
afterPluginsLoadedEvent += ::reloadRepos
|
|
}
|
|
|
|
override fun onStop() {
|
|
super.onStop()
|
|
afterPluginsLoadedEvent -= ::reloadRepos
|
|
}
|
|
|
|
var selectedSearchTypes = mutableListOf<TvType>()
|
|
var selectedApis = mutableSetOf<String>()
|
|
|
|
fun search(query: String?) {
|
|
if (query == null) return
|
|
context?.getApiSettings()?.let { settings ->
|
|
searchViewModel.searchAndCancel(
|
|
query = query,
|
|
providersActive = selectedApis.filter { name ->
|
|
settings.contains(name) && getApiFromNameNull(name)?.supportedTypes?.any {
|
|
selectedSearchTypes.contains(
|
|
it
|
|
)
|
|
} == true
|
|
}.toSet()
|
|
)
|
|
}
|
|
}
|
|
|
|
// Null if defined as a variable
|
|
// This needs to be run after view created
|
|
|
|
private fun reloadRepos(success: Boolean = false) = main {
|
|
searchViewModel.reloadRepos()
|
|
context?.filterProviderByPreferredMedia()?.let { validAPIs ->
|
|
bindChips(
|
|
home_select_group,
|
|
selectedSearchTypes,
|
|
validAPIs.flatMap { api -> api.supportedTypes }.distinct()
|
|
) { list ->
|
|
if (selectedSearchTypes.toSet() != list.toSet()) {
|
|
setKey(SEARCH_PREF_TAGS, selectedSearchTypes)
|
|
selectedSearchTypes.clear()
|
|
selectedSearchTypes.addAll(list)
|
|
search(main_search?.query?.toString())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
super.onViewCreated(view, savedInstanceState)
|
|
|
|
context?.fixPaddingStatusbar(searchRoot)
|
|
fixGrid()
|
|
reloadRepos()
|
|
|
|
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>? = activity?.let {
|
|
SearchAdapter(
|
|
ArrayList(),
|
|
search_autofit_results,
|
|
) { callback ->
|
|
SearchHelper.handleSearchClickCallback(activity, callback)
|
|
}
|
|
}
|
|
|
|
search_autofit_results.adapter = adapter
|
|
search_loading_bar.alpha = 0f
|
|
|
|
val searchExitIcon =
|
|
main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
|
|
// val searchMagIcon =
|
|
// main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
|
|
//searchMagIcon.scaleX = 0.65f
|
|
//searchMagIcon.scaleY = 0.65f
|
|
|
|
context?.let { ctx ->
|
|
val validAPIs = ctx.filterProviderByPreferredMedia()
|
|
selectedApis = ctx.getKey(
|
|
SEARCH_PREF_PROVIDERS,
|
|
defVal = validAPIs.map { it.name }
|
|
)!!.toMutableSet()
|
|
}
|
|
|
|
search_filter.setOnClickListener { searchView ->
|
|
searchView?.context?.let { ctx ->
|
|
val validAPIs = ctx.filterProviderByPreferredMedia(hasHomePageIsRequired = false)
|
|
var currentValidApis = listOf<MainAPI>()
|
|
val currentSelectedApis = if (selectedApis.isEmpty()) validAPIs.map { it.name }
|
|
.toMutableSet() else selectedApis
|
|
|
|
val builder =
|
|
BottomSheetDialog(ctx)
|
|
|
|
builder.behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
|
builder.setContentView(R.layout.home_select_mainpage)
|
|
builder.show()
|
|
builder.let { dialog ->
|
|
val isMultiLang = ctx.getApiProviderLangSettings().size > 1
|
|
|
|
val cancelBtt = dialog.findViewById<MaterialButton>(R.id.cancel_btt)
|
|
val applyBtt = dialog.findViewById<MaterialButton>(R.id.apply_btt)
|
|
|
|
val listView = dialog.findViewById<ListView>(R.id.listview1)
|
|
val arrayAdapter = ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
|
|
listView?.adapter = arrayAdapter
|
|
listView?.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
|
|
|
|
listView?.setOnItemClickListener { _, _, i, _ ->
|
|
if (currentValidApis.isNotEmpty()) {
|
|
val api = currentValidApis[i].name
|
|
if (currentSelectedApis.contains(api)) {
|
|
listView.setItemChecked(i, false)
|
|
currentSelectedApis -= api
|
|
} else {
|
|
listView.setItemChecked(i, true)
|
|
currentSelectedApis += api
|
|
}
|
|
}
|
|
}
|
|
|
|
fun updateList(types: List<TvType>) {
|
|
setKey(SEARCH_PREF_TAGS, types.map {it.name})
|
|
|
|
arrayAdapter.clear()
|
|
currentValidApis = validAPIs.filter { api ->
|
|
api.supportedTypes.any {
|
|
types.contains(it)
|
|
}
|
|
}.sortedBy { it.name.lowercase() }
|
|
|
|
val names = currentValidApis.map {
|
|
if (isMultiLang) "${
|
|
SubtitleHelper.getFlagFromIso(
|
|
it.lang
|
|
)?.plus(" ") ?: ""
|
|
}${it.name}" else it.name
|
|
}
|
|
for ((index, api) in currentValidApis.map { it.name }.withIndex()) {
|
|
listView?.setItemChecked(index, currentSelectedApis.contains(api))
|
|
}
|
|
|
|
//arrayAdapter.notifyDataSetChanged()
|
|
arrayAdapter.addAll(names)
|
|
arrayAdapter.notifyDataSetChanged()
|
|
}
|
|
|
|
val selectedSearchTypes = getKey<List<String>>(SEARCH_PREF_TAGS)
|
|
?.mapNotNull { listName ->
|
|
TvType.values().firstOrNull { it.name == listName }
|
|
}
|
|
?.toMutableList()
|
|
?: mutableListOf(TvType.Movie, TvType.TvSeries)
|
|
|
|
bindChips(
|
|
dialog.home_select_group,
|
|
selectedSearchTypes,
|
|
TvType.values().toList()
|
|
) { list ->
|
|
updateList(list)
|
|
}
|
|
|
|
cancelBtt?.setOnClickListener {
|
|
dialog.dismissSafe()
|
|
}
|
|
|
|
cancelBtt?.setOnClickListener {
|
|
dialog.dismissSafe()
|
|
}
|
|
|
|
applyBtt?.setOnClickListener {
|
|
//if (currentApiName != selectedApiName) {
|
|
// currentApiName?.let(callback)
|
|
//}
|
|
dialog.dismissSafe()
|
|
}
|
|
|
|
dialog.setOnDismissListener {
|
|
context?.setKey(SEARCH_PREF_PROVIDERS, currentSelectedApis.toList())
|
|
selectedApis = currentSelectedApis
|
|
}
|
|
updateList(selectedSearchTypes.toList())
|
|
}
|
|
}
|
|
}
|
|
|
|
val settingsManager = context?.let { PreferenceManager.getDefaultSharedPreferences(it) }
|
|
val isAdvancedSearch = settingsManager?.getBoolean("advanced_search", true) ?: true
|
|
|
|
selectedSearchTypes = context?.getKey<List<String>>(SEARCH_PREF_TAGS)
|
|
?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } }
|
|
?.toMutableList()
|
|
?: mutableListOf(TvType.Movie, TvType.TvSeries)
|
|
|
|
if (isTrueTvSettings()) {
|
|
search_filter.isFocusable = true
|
|
search_filter.isFocusableInTouchMode = true
|
|
}
|
|
|
|
main_search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
|
override fun onQueryTextSubmit(query: String): Boolean {
|
|
search(query)
|
|
|
|
main_search?.let {
|
|
hideKeyboard(it)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
override fun onQueryTextChange(newText: String): Boolean {
|
|
//searchViewModel.quickSearch(newText)
|
|
val showHistory = newText.isBlank()
|
|
if (showHistory) {
|
|
searchViewModel.clearSearch()
|
|
searchViewModel.updateHistory()
|
|
}
|
|
|
|
search_history_recycler?.isVisible = showHistory
|
|
|
|
search_master_recycler?.isVisible = !showHistory && isAdvancedSearch
|
|
search_autofit_results?.isVisible = !showHistory && !isAdvancedSearch
|
|
|
|
return true
|
|
}
|
|
})
|
|
|
|
observe(searchViewModel.currentHistory) { list ->
|
|
(search_history_recycler.adapter as? SearchHistoryAdaptor?)?.updateList(list)
|
|
}
|
|
|
|
searchViewModel.updateHistory()
|
|
|
|
observe(searchViewModel.searchResponse) {
|
|
when (it) {
|
|
is Resource.Success -> {
|
|
it.value.let { data ->
|
|
if (data.isNotEmpty()) {
|
|
(search_autofit_results?.adapter as SearchAdapter?)?.updateList(data)
|
|
}
|
|
}
|
|
searchExitIcon.alpha = 1f
|
|
search_loading_bar.alpha = 0f
|
|
}
|
|
is Resource.Failure -> {
|
|
// Toast.makeText(activity, "Server error", Toast.LENGTH_LONG).show()
|
|
searchExitIcon.alpha = 1f
|
|
search_loading_bar.alpha = 0f
|
|
}
|
|
is Resource.Loading -> {
|
|
searchExitIcon.alpha = 0f
|
|
search_loading_bar.alpha = 1f
|
|
}
|
|
}
|
|
}
|
|
|
|
val listLock = ReentrantLock()
|
|
observe(searchViewModel.currentSearch) { list ->
|
|
try {
|
|
// https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist
|
|
listLock.lock()
|
|
(search_master_recycler?.adapter as ParentItemAdapter?)?.apply {
|
|
val newItems = list.map { ongoing ->
|
|
val dataList =
|
|
if (ongoing.data is Resource.Success) ongoing.data.value else ArrayList()
|
|
val dataListFiltered =
|
|
context?.filterSearchResultByFilmQuality(dataList) ?: dataList
|
|
val ongoingList = HomePageList(
|
|
ongoing.apiName,
|
|
dataListFiltered
|
|
)
|
|
ongoingList
|
|
}
|
|
updateList(newItems)
|
|
|
|
//notifyDataSetChanged()
|
|
}
|
|
} catch (e: Exception) {
|
|
logError(e)
|
|
} finally {
|
|
listLock.unlock()
|
|
}
|
|
}
|
|
|
|
|
|
/*main_search.setOnQueryTextFocusChangeListener { _, b ->
|
|
if (b) {
|
|
// https://stackoverflow.com/questions/12022715/unable-to-show-keyboard-automatically-in-the-searchview
|
|
showInputMethod(view.findFocus())
|
|
}
|
|
}*/
|
|
//main_search.onActionViewExpanded()*/
|
|
|
|
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
|
ParentItemAdapter(mutableListOf(), { callback ->
|
|
SearchHelper.handleSearchClickCallback(activity, callback)
|
|
}, { item ->
|
|
activity?.loadHomepageList(item)
|
|
})
|
|
|
|
val historyAdapter = SearchHistoryAdaptor(mutableListOf()) { click ->
|
|
val searchItem = click.item
|
|
when (click.clickAction) {
|
|
SEARCH_HISTORY_OPEN -> {
|
|
searchViewModel.clearSearch()
|
|
if (searchItem.type.isNotEmpty())
|
|
updateChips(home_select_group, searchItem.type.toMutableList())
|
|
main_search?.setQuery(searchItem.searchText, true)
|
|
}
|
|
SEARCH_HISTORY_REMOVE -> {
|
|
removeKey(SEARCH_HISTORY_KEY, searchItem.key)
|
|
searchViewModel.updateHistory()
|
|
}
|
|
else -> {
|
|
// wth are you doing???
|
|
}
|
|
}
|
|
}
|
|
|
|
search_history_recycler?.adapter = historyAdapter
|
|
search_history_recycler?.layoutManager = GridLayoutManager(context, 1)
|
|
|
|
search_master_recycler?.adapter = masterAdapter
|
|
search_master_recycler?.layoutManager = GridLayoutManager(context, 1)
|
|
|
|
// SubtitlesFragment.push(activity)
|
|
//searchViewModel.search("iron man")
|
|
//(activity as AppCompatActivity).loadResult("https://shiro.is/overlord-dubbed", "overlord-dubbed", "Shiro")
|
|
/*
|
|
(activity as AppCompatActivity?)?.supportFragmentManager.beginTransaction()
|
|
.setCustomAnimations(R.anim.enter_anim,
|
|
R.anim.exit_anim,
|
|
R.anim.pop_enter,
|
|
R.anim.pop_exit)
|
|
.add(R.id.homeRoot, PlayerFragment.newInstance(PlayerData(0, null,0)))
|
|
.commit()*/
|
|
}
|
|
|
|
} |