package relax.offline.music.fragment import android.annotation.SuppressLint import android.content.Context import android.os.Handler import android.os.Looper import android.text.Editable import android.text.TextWatcher import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.TextView import android.widget.TextView.OnEditorActionListener import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.flexbox.AlignItems import com.google.android.flexbox.FlexWrap import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.JustifyContent import com.gyf.immersionbar.ktx.immersionBar import relax.offline.music.adapter.SearchHistoryAdapter import relax.offline.music.adapter.SearchSuggestionsAdapter import relax.offline.music.databinding.FragmentSearchBinding import relax.offline.music.innertube.Innertube import relax.offline.music.innertube.models.bodies.SearchBody import relax.offline.music.innertube.models.bodies.SearchSuggestionsBody import relax.offline.music.innertube.requests.moSearchPage import relax.offline.music.innertube.requests.searchSuggestions import relax.offline.music.util.LogTag import relax.offline.music.view.SearchResultOptimalView import relax.offline.music.view.SearchResultOtherView import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.isActive import kotlinx.coroutines.selects.select class SearchFragment : MoBaseFragment(), TextWatcher, View.OnFocusChangeListener, SearchSuggestionsAdapter.OnItemClickListener, OnEditorActionListener, SearchHistoryAdapter.HistoryOnItemClickListener { interface SearchFragmentCancelClickListener { fun onFragmentClick() } fun setButtonClickListener(listener: SearchFragmentCancelClickListener) { this.buttonClickListener = listener } private var buttonClickListener: SearchFragmentCancelClickListener? = null private val requests: Channel = Channel(Channel.UNLIMITED) sealed class Request { data object SearchSuggestions : Request() data class SearchData(val input: String) : Request() } private var searchSuggestionsAdapterAdapter: SearchSuggestionsAdapter? = null private var searchSuggestionsList: MutableList = mutableListOf() private var searchHistorySet: MutableSet = mutableSetOf() private var searchHistory: MutableList = mutableListOf() private var searchHistoryAdapter: SearchHistoryAdapter? = null override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentSearchBinding get() = FragmentSearchBinding::inflate override suspend fun onViewCreated() { initView() initSearchSuggestionsAdapter() initSearchHistoryAdapter() initImmersionBar() onReceive() } private fun initView() { binding.cancelBtn.setOnClickListener { buttonClickListener?.onFragmentClick() } binding.searchEdit.let { it.setOnEditorActionListener(this) it.addTextChangedListener(this) it.onFocusChangeListener = this it.requestFocus() } binding.deleteInputBtn.setOnClickListener { binding.searchEdit.text.clear() } binding.deleteHistoryBtn.setOnClickListener { appStore.searchHistoryStore = emptySet() updateHistoryUi() } } private fun initSearchSuggestionsAdapter() { searchSuggestionsAdapterAdapter = SearchSuggestionsAdapter(requireActivity(), searchSuggestionsList) searchSuggestionsAdapterAdapter?.setOnItemClickListener(this) binding.searchSuggestionsRv.layoutManager = LinearLayoutManager( requireActivity(), LinearLayoutManager.VERTICAL, false ) binding.searchSuggestionsRv.adapter = searchSuggestionsAdapterAdapter } private fun initSearchHistoryAdapter() { searchHistoryAdapter = SearchHistoryAdapter(requireActivity(), searchHistory) searchHistoryAdapter?.setOnItemClickListener(this) val layoutManager = FlexboxLayoutManager(requireActivity()) layoutManager.flexWrap = FlexWrap.WRAP // 设置换行方式为自动换行 layoutManager.alignItems = AlignItems.STRETCH // 设置项目在副轴上的对齐方式为拉伸 layoutManager.justifyContent = JustifyContent.FLEX_START // 设置主轴上的对齐方式为起始端对齐 binding.historyRv.layoutManager = layoutManager binding.historyRv.adapter = searchHistoryAdapter updateHistoryUi() } @SuppressLint("NotifyDataSetChanged") private fun updateHistoryUi() { searchHistory.clear() searchHistory.addAll(appStore.searchHistoryStore) searchHistoryAdapter?.notifyDataSetChanged() if (appStore.searchHistoryStore.isNotEmpty()) { binding.historyLayout.visibility = View.VISIBLE } else { binding.historyLayout.visibility = View.GONE } } private fun initImmersionBar() { immersionBar { statusBarDarkFont(false) statusBarView(binding.view) } } @SuppressLint("NotifyDataSetChanged", "SetTextI18n") private suspend fun onReceive() { while (isActive) { select { requests.onReceive { when (it) { is Request.SearchSuggestions -> { val input = binding.searchEdit.text.toString().trim() Innertube.searchSuggestions(SearchSuggestionsBody(input = input)) ?.onSuccess { suggestionsList -> LogTag.LogD(TAG, "suggestionsList->${suggestionsList?.size}") if (suggestionsList != null) { showSearchSuggestions() searchSuggestionsList.clear() searchSuggestionsList.addAll(suggestionsList) searchSuggestionsAdapterAdapter?.notifyDataSetChanged() } else { binding.searchSuggestionsLayout.visibility = View.GONE } }?.onFailure { error -> LogTag.LogD(TAG, "searchSuggestions onFailure->${error}") binding.searchSuggestionsLayout.visibility = View.GONE } } is Request.SearchData -> { binding.contentLayout.removeAllViews() binding.searchEdit.clearFocus() showLoadingLayout() val input = it.input if (input.isNotEmpty()) { searchHistorySet.clear() searchHistorySet.addAll(appStore.searchHistoryStore) if (!appStore.searchHistoryStore.contains(input)) {//把不存在的记录保存到集合中 searchHistorySet.add(input) } appStore.searchHistoryStore = searchHistorySet updateHistoryUi() Innertube.moSearchPage(SearchBody(query = input))?.onSuccess { result -> showResultContent() for (dataPage: Innertube.SearchDataPage in result) { LogTag.LogD(TAG,"moSearchPage dataPage->$dataPage") if (dataPage.type == 1) {//type为1的是最佳结果。 binding.contentLayout.addView( SearchResultOptimalView(requireActivity(), dataPage) ) } else if (dataPage.type == 2) {//type为2的是其他搜索结果。 if (dataPage.searchResultList.isNotEmpty()) {//如何数据集合为空就不添加view binding.contentLayout.addView( SearchResultOtherView( requireActivity(), dataPage ) ) } } } }?.onFailure { showNoContentLayout() } } } } } } } } override fun onHiddenChanged(hidden: Boolean) { super.onHiddenChanged(hidden) if (!hidden) { binding.searchEdit.requestFocus() } else { binding.searchEdit.clearFocus() binding.searchEdit.text.clear() binding.contentLayout.removeAllViews() } } override fun onHistoryItemClick(position: Int) { binding.searchEdit.setText(searchHistory[position]) requests.trySend(Request.SearchData(searchHistory[position])) } override fun onItemClick(position: Int) { binding.searchEdit.setText(searchSuggestionsList[position]) requests.trySend(Request.SearchData(searchSuggestionsList[position])) } override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { if (actionId == EditorInfo.IME_ACTION_SEARCH) { requests.trySend(Request.SearchData(binding.searchEdit.text.toString().trim())) return true } return false } override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} @SuppressLint("NotifyDataSetChanged") override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { if (s.isNullOrEmpty()) {//没有输入内容,隐藏删除按钮,展示历史记录,重新获取焦点弹出键盘。 binding.deleteInputBtn.visibility = View.GONE showSearchHistory() updateHistoryUi() binding.searchEdit.requestFocus() } else { binding.deleteInputBtn.visibility = View.VISIBLE } } override fun afterTextChanged(s: Editable?) { requests.trySend(Request.SearchSuggestions) } override fun onFocusChange(v: View?, hasFocus: Boolean) { if (hasFocus) { Handler(Looper.getMainLooper()).postDelayed({ showImm() }, 200) } else { Handler(Looper.getMainLooper()).postDelayed({ hideImm() }, 200) } } private fun showImm() { val imm = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.showSoftInput(binding.searchEdit, InputMethodManager.SHOW_IMPLICIT) } private fun hideImm() { val imm = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.hideSoftInputFromWindow(binding.searchEdit.windowToken, 0) } private fun showResultContent() { binding.contentScrollView.visibility = View.VISIBLE binding.searchSuggestionsLayout.visibility = View.GONE binding.historyLayout.visibility = View.GONE binding.loadingLayout.visibility = View.GONE binding.noContentLayout.visibility = View.GONE } private fun showSearchSuggestions() { binding.contentScrollView.visibility = View.GONE binding.searchSuggestionsLayout.visibility = View.VISIBLE binding.historyLayout.visibility = View.GONE binding.loadingLayout.visibility = View.GONE binding.noContentLayout.visibility = View.GONE } private fun showSearchHistory() { binding.contentScrollView.visibility = View.GONE binding.searchSuggestionsLayout.visibility = View.GONE binding.historyLayout.visibility = View.VISIBLE binding.loadingLayout.visibility = View.GONE binding.noContentLayout.visibility = View.GONE } private fun showLoadingLayout() { binding.loadingLayout.visibility = View.VISIBLE binding.noContentLayout.visibility = View.GONE binding.contentScrollView.visibility = View.GONE binding.searchSuggestionsLayout.visibility = View.GONE binding.historyLayout.visibility = View.GONE } private fun showNoContentLayout() { binding.loadingLayout.visibility = View.GONE binding.noContentLayout.visibility = View.VISIBLE binding.contentScrollView.visibility = View.GONE binding.searchSuggestionsLayout.visibility = View.GONE binding.historyLayout.visibility = View.GONE } }