Musicoo/app/src/main/java/relax/offline/music/fragment/SearchFragment.kt
2024-05-24 20:36:01 +08:00

321 lines
13 KiB
Kotlin

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<FragmentSearchBinding>(), 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<Request> = 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<String> = mutableListOf()
private var searchHistorySet: MutableSet<String> = mutableSetOf()
private var searchHistory: MutableList<String> = 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<Unit> {
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
}
}