Merge pull request #452 from Bnyro/master

search suggestions rewrite
This commit is contained in:
Bnyro 2022-06-10 14:26:16 +02:00 committed by GitHub
commit c13054bfb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 166 additions and 105 deletions

View File

@ -4,17 +4,19 @@ import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AutoCompleteTextView import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.fragments.SearchFragment
import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.imageview.ShapeableImageView
class SearchHistoryAdapter( class SearchHistoryAdapter(
private val context: Context, private val context: Context,
private var historyList: List<String>, private var historyList: List<String>,
private val editText: AutoCompleteTextView private val editText: EditText,
private val searchFragment: SearchFragment
) : ) :
RecyclerView.Adapter<SearchHistoryViewHolder>() { RecyclerView.Adapter<SearchHistoryViewHolder>() {
@ -34,17 +36,14 @@ class SearchHistoryAdapter(
holder.v.findViewById<ShapeableImageView>(R.id.delete_history).setOnClickListener { holder.v.findViewById<ShapeableImageView>(R.id.delete_history).setOnClickListener {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
historyList = historyList - history historyList = historyList - history
sharedPreferences.edit().putStringSet("search_history", HashSet(historyList)).apply()
sharedPreferences.edit().putStringSet("search_history", HashSet(historyList))
.apply()
notifyDataSetChanged() notifyDataSetChanged()
} }
holder.v.setOnClickListener { holder.v.setOnClickListener {
editText.setText(history) editText.setText(history)
searchFragment.fetchSearch(history)
} }
} }
} }

View File

@ -0,0 +1,43 @@
package com.github.libretube.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.fragments.SearchFragment
class SearchSuggestionsAdapter(
private var suggestionsList: List<String>,
private var editText: EditText,
private val searchFragment: SearchFragment
) :
RecyclerView.Adapter<SearchSuggestionsViewHolder>() {
override fun getItemCount(): Int {
return suggestionsList.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchSuggestionsViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val cell = layoutInflater.inflate(R.layout.searchsuggestion_row, parent, false)
return SearchSuggestionsViewHolder(cell)
}
override fun onBindViewHolder(holder: SearchSuggestionsViewHolder, position: Int) {
val suggestion = suggestionsList[position]
val suggestionTextView = holder.v.findViewById<TextView>(R.id.suggestion_text)
suggestionTextView.text = suggestion
holder.v.setOnClickListener {
editText.setText(suggestion)
searchFragment.fetchSearch(editText.text.toString())
}
}
}
class SearchSuggestionsViewHolder(val v: View) : RecyclerView.ViewHolder(v) {
init {
}
}

View File

@ -1,7 +1,6 @@
package com.github.libretube.fragments package com.github.libretube.fragments
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
@ -12,8 +11,7 @@ import android.view.ViewGroup
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.ArrayAdapter import android.widget.EditText
import android.widget.AutoCompleteTextView
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView.GONE import android.widget.TextView.GONE
import android.widget.TextView.OnEditorActionListener import android.widget.TextView.OnEditorActionListener
@ -27,12 +25,12 @@ import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.adapters.SearchAdapter import com.github.libretube.adapters.SearchAdapter
import com.github.libretube.adapters.SearchHistoryAdapter import com.github.libretube.adapters.SearchHistoryAdapter
import com.github.libretube.adapters.SearchSuggestionsAdapter
import com.github.libretube.hideKeyboard import com.github.libretube.hideKeyboard
import com.github.libretube.util.RetrofitInstance import com.github.libretube.util.RetrofitInstance
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.IOException import java.io.IOException
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.HttpException import retrofit2.HttpException
@ -42,8 +40,11 @@ class SearchFragment : Fragment() {
private var apiSearchFilter = "all" private var apiSearchFilter = "all"
private var nextPage: String? = null private var nextPage: String? = null
private lateinit var searchRecView: RecyclerView private lateinit var searchRecView: RecyclerView
private lateinit var historyRecView: RecyclerView
private lateinit var autoTextView: EditText
private var searchAdapter: SearchAdapter? = null private var searchAdapter: SearchAdapter? = null
private var isLoading: Boolean = true private var isLoading: Boolean = true
private var isFetchingSearch: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -62,18 +63,15 @@ class SearchFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
searchRecView = view.findViewById<RecyclerView>(R.id.search_recycler) searchRecView = view.findViewById(R.id.search_recycler)
historyRecView = view.findViewById(R.id.history_recycler)
autoTextView = view.findViewById(R.id.autoCompleteTextView)
val autoTextView = view.findViewById<AutoCompleteTextView>(R.id.autoCompleteTextView)
val clearSearchButton = view.findViewById<ImageView>(R.id.clearSearch_imageView) val clearSearchButton = view.findViewById<ImageView>(R.id.clearSearch_imageView)
val historyRecycler = view.findViewById<RecyclerView>(R.id.history_recycler)
val filterImageView = view.findViewById<ImageView>(R.id.filterMenu_imageView) val filterImageView = view.findViewById<ImageView>(R.id.filterMenu_imageView)
var tempSelectedItem = 0 var tempSelectedItem = 0
val sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(requireContext())
clearSearchButton.setOnClickListener { clearSearchButton.setOnClickListener {
autoTextView.text.clear() autoTextView.text.clear()
} }
@ -92,53 +90,41 @@ class SearchFragment : Fragment() {
MaterialAlertDialogBuilder(view.context) MaterialAlertDialogBuilder(view.context)
.setTitle(getString(R.string.choose_filter)) .setTitle(getString(R.string.choose_filter))
.setSingleChoiceItems( .setSingleChoiceItems(filterOptions, selectedFilter) { _, id ->
filterOptions, selectedFilter, tempSelectedItem = id
DialogInterface.OnClickListener { _, id -> }
tempSelectedItem = id
}
)
.setPositiveButton( .setPositiveButton(
getString(R.string.okay), getString(R.string.okay),
DialogInterface.OnClickListener { _, _ -> ) { _, _ ->
selectedFilter = tempSelectedItem selectedFilter = tempSelectedItem
apiSearchFilter = when (selectedFilter) { apiSearchFilter = when (selectedFilter) {
0 -> "all" 0 -> "all"
1 -> "videos" 1 -> "videos"
2 -> "channels" 2 -> "channels"
3 -> "playlists" 3 -> "playlists"
4 -> "music_songs" 4 -> "music_songs"
5 -> "music_videos" 5 -> "music_videos"
6 -> "music_albums" 6 -> "music_albums"
7 -> "music_playlists" 7 -> "music_playlists"
else -> "all" else -> "all"
}
fetchSearch(autoTextView.text.toString())
} }
) fetchSearch(autoTextView.text.toString())
}
.setNegativeButton(getString(R.string.cancel), null) .setNegativeButton(getString(R.string.cancel), null)
.create() .create()
.show() .show()
} }
// show search history // show search history
historyRecView.layoutManager = LinearLayoutManager(view.context)
searchRecView.visibility = GONE showHistory()
historyRecycler.visibility = VISIBLE
historyRecycler.layoutManager = LinearLayoutManager(view.context)
val historyList = getHistory()
if (historyList.isNotEmpty()) {
historyRecycler.adapter =
SearchHistoryAdapter(requireContext(), historyList, autoTextView)
}
searchRecView.layoutManager = GridLayoutManager(view.context, 1) searchRecView.layoutManager = GridLayoutManager(view.context, 1)
autoTextView.requestFocus() autoTextView.requestFocus()
val imm = val imm =
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(autoTextView, InputMethodManager.SHOW_IMPLICIT) imm.showSoftInput(autoTextView, InputMethodManager.SHOW_IMPLICIT)
autoTextView.addTextChangedListener(object : TextWatcher { autoTextView.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged( override fun beforeTextChanged(
s: CharSequence?, s: CharSequence?,
@ -150,8 +136,6 @@ class SearchFragment : Fragment() {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
if (s!! != "") { if (s!! != "") {
searchRecView.visibility = VISIBLE
historyRecycler.visibility = GONE
searchRecView.adapter = null searchRecView.adapter = null
searchRecView.viewTreeObserver searchRecView.viewTreeObserver
@ -163,21 +147,13 @@ class SearchFragment : Fragment() {
GlobalScope.launch { GlobalScope.launch {
fetchSuggestions(s.toString(), autoTextView) fetchSuggestions(s.toString(), autoTextView)
delay(1000)
fetchSearch(s.toString())
} }
} }
} }
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
if (s!!.isEmpty()) { if (s!!.isEmpty()) {
searchRecView.visibility = GONE showHistory()
historyRecycler.visibility = VISIBLE
val historyList = getHistory()
if (historyList.isNotEmpty()) {
historyRecycler.adapter =
SearchHistoryAdapter(requireContext(), historyList, autoTextView)
}
} }
} }
}) })
@ -185,45 +161,44 @@ class SearchFragment : Fragment() {
OnEditorActionListener { _, actionId, _ -> OnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) { if (actionId == EditorInfo.IME_ACTION_SEARCH) {
hideKeyboard() hideKeyboard()
autoTextView.dismissDropDown() searchRecView.visibility = VISIBLE
if (sharedPreferences.getBoolean( historyRecView.visibility = GONE
"search_history_toggle", fetchSearch(autoTextView.text.toString())
true
)
) {
val newString = autoTextView.text.toString()
addToHistory(newString)
}
return@OnEditorActionListener true return@OnEditorActionListener true
} }
false false
} }
) )
autoTextView.setOnItemClickListener { _, _, _, _ ->
hideKeyboard()
}
} }
private fun fetchSuggestions(query: String, autoTextView: AutoCompleteTextView) { private fun fetchSuggestions(query: String, autoTextView: EditText) {
lifecycleScope.launchWhenCreated { fun run() {
val response = try { lifecycleScope.launchWhenCreated {
RetrofitInstance.api.getSuggestions(query) searchRecView.visibility = GONE
} catch (e: IOException) { historyRecView.visibility = VISIBLE
println(e) val response = try {
Log.e(TAG, "IOException, you might not have internet connection") RetrofitInstance.api.getSuggestions(query)
return@launchWhenCreated } catch (e: IOException) {
} catch (e: HttpException) { println(e)
Log.e(TAG, "HttpException, unexpected response") Log.e(TAG, "IOException, you might not have internet connection")
return@launchWhenCreated return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG, "HttpException, unexpected response")
return@launchWhenCreated
}
val suggestionsAdapter =
SearchSuggestionsAdapter(response, autoTextView, this@SearchFragment)
historyRecView.adapter = suggestionsAdapter
} }
val adapter =
ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, response)
autoTextView.setAdapter(adapter)
} }
if (!isFetchingSearch) run()
} }
private fun fetchSearch(query: String) { fun fetchSearch(query: String) {
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
isFetchingSearch = true
hideKeyboard()
Log.e("here", "here")
val response = try { val response = try {
RetrofitInstance.api.getSearchResults(query, apiSearchFilter) RetrofitInstance.api.getSearchResults(query, apiSearchFilter)
} catch (e: IOException) { } catch (e: IOException) {
@ -237,11 +212,15 @@ class SearchFragment : Fragment() {
nextPage = response.nextpage nextPage = response.nextpage
if (response.items!!.isNotEmpty()) { if (response.items!!.isNotEmpty()) {
runOnUiThread { runOnUiThread {
historyRecView.visibility = GONE
searchRecView.visibility = VISIBLE
searchAdapter = SearchAdapter(response.items, childFragmentManager) searchAdapter = SearchAdapter(response.items, childFragmentManager)
searchRecView.adapter = searchAdapter searchRecView.adapter = searchAdapter
} }
} }
addToHistory(query)
isLoading = false isLoading = false
isFetchingSearch = false
} }
} }
@ -286,27 +265,37 @@ class SearchFragment : Fragment() {
hideKeyboard() hideKeyboard()
} }
private fun showHistory() {
searchRecView.visibility = GONE
val historyList = getHistory()
if (historyList.isNotEmpty()) {
historyRecView.adapter =
SearchHistoryAdapter(requireContext(), historyList, autoTextView, this)
historyRecView.visibility = VISIBLE
}
}
private fun addToHistory(query: String) { private fun addToHistory(query: String) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val searchHistoryEnabled = sharedPreferences.getBoolean("search_history_toggle", true)
if (searchHistoryEnabled) {
var historyList = getHistory()
var historyList = getHistory() if ((historyList.isNotEmpty() && historyList.contains(query)) || query == "") {
return
} else {
historyList = historyList + query
}
if (historyList.isNotEmpty() && query == historyList[historyList.size - 1]) { if (historyList.size > 10) {
return historyList = historyList.takeLast(10)
} else if (query == "") { }
return
} else { val set: Set<String> = HashSet(historyList)
historyList = historyList + query
sharedPreferences.edit().putStringSet("search_history", set)
.apply()
} }
if (historyList.size > 10) {
historyList = historyList.takeLast(10)
}
val set: Set<String> = HashSet(historyList)
sharedPreferences.edit().putStringSet("search_history", set)
.apply()
} }
private fun getHistory(): List<String> { private fun getHistory(): List<String> {

View File

@ -42,12 +42,11 @@
android:background="@android:color/transparent" android:background="@android:color/transparent"
app:hintEnabled="false"> app:hintEnabled="false">
<AutoCompleteTextView <EditText
android:id="@+id/autoCompleteTextView" android:id="@+id/autoCompleteTextView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:dropDownWidth="match_parent"
android:hint="@string/search_hint" android:hint="@string/search_hint"
android:imeOptions="actionSearch" android:imeOptions="actionSearch"
android:inputType="text" android:inputType="text"

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_marginBottom="16dp"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:id="@+id/search_icon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:src="@drawable/ic_search"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_margin="5dp" />
<TextView
android:id="@+id/suggestion_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/search_icon"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>