package com.lagradost.cloudstream3.ui.search import android.content.DialogInterface 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.app.AlertDialog 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.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent import com.lagradost.cloudstream3.databinding.FragmentSearchBinding import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding 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.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.AppUtils.ownHide import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus 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 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.filterSearchResponse(): List { return this.filter { response -> if (response is AnimeSearchResponse) { val status = response.dubStatus (status.isNullOrEmpty()) || (status.any { APIRepository.dubStatusActive.contains(it) }) } else { true } } } const val SEARCH_QUERY = "search_query" fun newInstance(query: String): Bundle { return Bundle().apply { putString(SEARCH_QUERY, query) } } } private val searchViewModel: SearchViewModel by activityViewModels() private var bottomSheetDialog: BottomSheetDialog? = null var binding: FragmentSearchBinding? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View? { activity?.window?.setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE ) bottomSheetDialog?.ownShow() val layout = if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search val root = inflater.inflate(layout, container, false) // TODO TRYCATCH binding = FragmentSearchBinding.bind(root) return root } private fun fixGrid() { activity?.getSpanCount()?.let { currentSpan = it } binding?.searchAutofitResults?.spanCount = currentSpan currentSpan = currentSpan HomeFragment.configEvent.invoke(currentSpan) } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) fixGrid() } override fun onDestroyView() { hideKeyboard() bottomSheetDialog?.ownHide() binding = null super.onDestroyView() } override fun onResume() { super.onResume() afterPluginsLoadedEvent += ::reloadRepos } override fun onStop() { super.onStop() afterPluginsLoadedEvent -= ::reloadRepos } var selectedSearchTypes = mutableListOf() var selectedApis = mutableSetOf() /** * Will filter all providers by preferred media and selectedSearchTypes. * If that results in no available providers then only filter * providers by preferred media **/ fun search(query: String?) { if (query == null) return context?.let { ctx -> val default = enumValues().sorted().filter { it != TvType.NSFW } .map { it.ordinal.toString() }.toSet() val preferredTypes = (PreferenceManager.getDefaultSharedPreferences(ctx) .getStringSet(this.getString(R.string.prefer_media_type_key), default) ?.ifEmpty { default } ?: default) .mapNotNull { it.toIntOrNull() ?: return@mapNotNull null } val settings = ctx.getApiSettings() val notFilteredBySelectedTypes = selectedApis.filter { name -> settings.contains(name) }.map { name -> name to getApiFromNameNull(name)?.supportedTypes }.filter { (_, types) -> types?.any { preferredTypes.contains(it.ordinal) } == true } searchViewModel.searchAndCancel( query = query, providersActive = notFilteredBySelectedTypes.filter { (_, types) -> types?.any { selectedSearchTypes.contains(it) } == true }.ifEmpty { notFilteredBySelectedTypes }.map { it.first }.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( binding?.tvtypesChipsScroll?.tvtypesChips, selectedSearchTypes, validAPIs.flatMap { api -> api.supportedTypes }.distinct() ) { list -> if (selectedSearchTypes.toSet() != list.toSet()) { setKey(SEARCH_PREF_TAGS, selectedSearchTypes) selectedSearchTypes.clear() selectedSearchTypes.addAll(list) search(binding?.mainSearch?.query?.toString()) } } } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) fixPaddingStatusbar(binding?.searchRoot) fixGrid() reloadRepos() binding?.apply { val adapter: RecyclerView.Adapter? = SearchAdapter( ArrayList(), searchAutofitResults, ) { callback -> SearchHelper.handleSearchClickCallback(callback) } searchAutofitResults.adapter = adapter searchLoadingBar.alpha = 0f } val searchExitIcon = binding?.mainSearch?.findViewById(androidx.appcompat.R.id.search_close_btn) // val searchMagIcon = // main_search.findViewById(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() } binding?.searchFilter?.setOnClickListener { searchView -> searchView?.context?.let { ctx -> val validAPIs = ctx.filterProviderByPreferredMedia(hasHomePageIsRequired = false) var currentValidApis = listOf() val currentSelectedApis = if (selectedApis.isEmpty()) validAPIs.map { it.name } .toMutableSet() else selectedApis val builder = BottomSheetDialog(ctx) builder.behavior.state = BottomSheetBehavior.STATE_EXPANDED val binding: HomeSelectMainpageBinding = HomeSelectMainpageBinding.inflate( builder.layoutInflater, null, false ) builder.setContentView(binding.root) builder.show() builder.let { dialog -> val isMultiLang = ctx.getApiProviderLangSettings().let { set -> set.size > 1 || set.contains(AllLanguagesName) } val cancelBtt = dialog.findViewById(R.id.cancel_btt) val applyBtt = dialog.findViewById(R.id.apply_btt) val listView = dialog.findViewById(R.id.listview1) val arrayAdapter = ArrayAdapter(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) { 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>(SEARCH_PREF_TAGS) ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } ?.toMutableList() ?: mutableListOf(TvType.Movie, TvType.TvSeries) bindChips( binding.tvtypesChipsScroll.tvtypesChips, 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>(SEARCH_PREF_TAGS) ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } ?.toMutableList() ?: mutableListOf(TvType.Movie, TvType.TvSeries) if (isTrueTvSettings()) { binding?.searchFilter?.isFocusable = true binding?.searchFilter?.isFocusableInTouchMode = true } binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { search(query) binding?.mainSearch?.let { hideKeyboard(it) } return true } override fun onQueryTextChange(newText: String): Boolean { //searchViewModel.quickSearch(newText) val showHistory = newText.isBlank() if (showHistory) { searchViewModel.clearSearch() searchViewModel.updateHistory() } binding?.apply { searchHistoryHolder.isVisible = showHistory searchMasterRecycler.isVisible = !showHistory && isAdvancedSearch searchAutofitResults.isVisible = !showHistory && !isAdvancedSearch } return true } }) binding?.searchClearCallHistory?.setOnClickListener { activity?.let { ctx -> val builder: AlertDialog.Builder = AlertDialog.Builder(ctx) val dialogClickListener = DialogInterface.OnClickListener { _, which -> when (which) { DialogInterface.BUTTON_POSITIVE -> { removeKeys(SEARCH_HISTORY_KEY) searchViewModel.updateHistory() } DialogInterface.BUTTON_NEGATIVE -> { } } } try { builder.setTitle(R.string.clear_history).setMessage( ctx.getString(R.string.delete_message).format( ctx.getString(R.string.history) ) ) .setPositiveButton(R.string.sort_clear, dialogClickListener) .setNegativeButton(R.string.cancel, dialogClickListener) .show().setDefaultFocus() } catch (e: Exception) { logError(e) // ye you somehow fucked up formatting did you? } } } observe(searchViewModel.currentHistory) { list -> binding?.searchClearCallHistory?.isVisible = list.isNotEmpty() (binding?.searchHistoryRecycler?.adapter as? SearchHistoryAdaptor?)?.updateList(list) } searchViewModel.updateHistory() observe(searchViewModel.searchResponse) { when (it) { is Resource.Success -> { it.value.let { data -> if (data.isNotEmpty()) { (binding?.searchAutofitResults?.adapter as? SearchAdapter)?.updateList(data) } } searchExitIcon?.alpha = 1f binding?.searchLoadingBar?.alpha = 0f } is Resource.Failure -> { // Toast.makeText(activity, "Server error", Toast.LENGTH_LONG).show() searchExitIcon?.alpha = 1f binding?.searchLoadingBar?.alpha = 0f } is Resource.Loading -> { searchExitIcon?.alpha = 0f binding?.searchLoadingBar?.alpha = 1f } } } val listLock = ReentrantLock() observe(searchViewModel.currentSearch) { list -> try { // https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist listLock.lock() (binding?.searchMasterRecycler?.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 = ParentItemAdapter(mutableListOf(), { callback -> SearchHelper.handleSearchClickCallback(callback) }, { item -> bottomSheetDialog = activity?.loadHomepageList(item, dismissCallback = { bottomSheetDialog = null }) }) val historyAdapter = SearchHistoryAdaptor(mutableListOf()) { click -> val searchItem = click.item when (click.clickAction) { SEARCH_HISTORY_OPEN -> { searchViewModel.clearSearch() if (searchItem.type.isNotEmpty()) updateChips(binding?.tvtypesChipsScroll?.tvtypesChips, searchItem.type.toMutableList()) binding?.mainSearch?.setQuery(searchItem.searchText, true) } SEARCH_HISTORY_REMOVE -> { removeKey(SEARCH_HISTORY_KEY, searchItem.key) searchViewModel.updateHistory() } else -> { // wth are you doing??? } } } binding?.apply { searchHistoryRecycler.adapter = historyAdapter searchHistoryRecycler.setLinearListLayout(isHorizontal = false, nextRight = FOCUS_SELF) //searchHistoryRecycler.layoutManager = GridLayoutManager(context, 1) searchMasterRecycler.adapter = masterAdapter //searchMasterRecycler.setLinearListLayout(isHorizontal = false, nextRight = FOCUS_SELF) searchMasterRecycler.layoutManager = GridLayoutManager(context, 1) // Automatically search the specified query, this allows the app search to launch from intent arguments?.getString(SEARCH_QUERY)?.let { query -> if (query.isBlank()) return@let mainSearch.setQuery(query, true) // Clear the query as to not make it request the same query every time the page is opened arguments?.putString(SEARCH_QUERY, null) } } // 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()*/ } }