new search behavior

This commit is contained in:
Bnyro 2022-07-31 22:17:36 +02:00
parent f33b9b7afb
commit 022281e107
9 changed files with 224 additions and 324 deletions

View File

@ -17,9 +17,11 @@ import android.view.WindowInsetsController
import android.view.WindowManager
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.os.bundleOf
import androidx.fragment.app.replace
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.ui.setupWithNavController
@ -28,6 +30,7 @@ import com.github.libretube.Globals
import com.github.libretube.R
import com.github.libretube.databinding.ActivityMainBinding
import com.github.libretube.fragments.PlayerFragment
import com.github.libretube.fragments.SearchFragment
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.services.ClosingService
@ -46,6 +49,7 @@ class MainActivity : AppCompatActivity() {
lateinit var navController: NavController
private var startFragmentId = R.id.homeFragment
var autoRotationEnabled = false
private var searchFragment: SearchFragment? = null
override fun onCreate(savedInstanceState: Bundle?) {
// set the app theme (e.g. Material You)
@ -154,7 +158,26 @@ class MainActivity : AppCompatActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.action_bar, menu)
return true
val searchItem = menu.findItem(R.id.action_search)
// stuff for the search in the topBar
val searchView = searchItem.actionView as SearchView
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
val bundle = Bundle()
bundle.putString("query", query)
navController.navigate(R.id.searchResultFragment, bundle)
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
val bundle = Bundle()
bundle.putString("query", newText)
navController.navigate(R.id.searchFragment, bundle)
return true
}
})
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -162,10 +185,6 @@ class MainActivity : AppCompatActivity() {
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_search -> {
navController.navigate(R.id.searchFragment)
true
}
R.id.action_settings -> {
val settingsIntent = Intent(this, SettingsActivity::class.java)
startActivity(settingsIntent)
@ -185,6 +204,14 @@ class MainActivity : AppCompatActivity() {
}
}
private fun addToHistory(query: String) {
val searchHistoryEnabled =
PreferenceHelper.getBoolean(PreferenceKeys.SEARCH_HISTORY_TOGGLE, true)
if (searchHistoryEnabled && query != "") {
PreferenceHelper.saveToSearchHistory(query)
}
}
override fun onStart() {
super.onStart()
val intentData: Uri? = intent?.data

View File

@ -2,16 +2,12 @@ package com.github.libretube.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.EditText
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.SearchhistoryRowBinding
import com.github.libretube.fragments.SearchFragment
import com.github.libretube.preferences.PreferenceHelper
class SearchHistoryAdapter(
private var historyList: List<String>,
private val editText: EditText,
private val searchFragment: SearchFragment
private var historyList: List<String>
) :
RecyclerView.Adapter<SearchHistoryViewHolder>() {
@ -37,8 +33,6 @@ class SearchHistoryAdapter(
}
root.setOnClickListener {
editText.setText(historyQuery)
searchFragment.fetchSearch(historyQuery)
}
}
}

View File

@ -2,15 +2,11 @@ package com.github.libretube.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.EditText
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.databinding.SearchsuggestionRowBinding
import com.github.libretube.fragments.SearchFragment
class SearchSuggestionsAdapter(
private var suggestionsList: List<String>,
private var editText: EditText,
private val searchFragment: SearchFragment
private var suggestionsList: List<String>
) :
RecyclerView.Adapter<SearchSuggestionsViewHolder>() {
@ -31,8 +27,6 @@ class SearchSuggestionsAdapter(
holder.binding.apply {
suggestionText.text = suggestion
root.setOnClickListener {
editText.setText(suggestion)
searchFragment.fetchSearch(editText.text.toString())
}
}
}

View File

@ -1,51 +1,30 @@
package com.github.libretube.fragments
import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.TextView.GONE
import android.widget.TextView.OnEditorActionListener
import android.widget.TextView.VISIBLE
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.adapters.SearchAdapter
import com.github.libretube.adapters.SearchHistoryAdapter
import com.github.libretube.adapters.SearchSuggestionsAdapter
import com.github.libretube.databinding.FragmentSearchBinding
import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.preferences.PreferenceKeys
import com.github.libretube.util.RetrofitInstance
import com.github.libretube.util.hideKeyboard
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import retrofit2.HttpException
import java.io.IOException
class SearchFragment : Fragment() {
class SearchFragment() : Fragment() {
private val TAG = "SearchFragment"
private lateinit var binding: FragmentSearchBinding
private var apiSearchFilter = "all"
private var nextPage: String? = null
private var searchAdapter: SearchAdapter? = null
private var isLoading: Boolean = true
private var isFetchingSearch: Boolean = false
private var query: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
}
query = arguments?.getString("query")
}
override fun onCreateView(
@ -60,103 +39,13 @@ class SearchFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.clearSearchImageView.setOnClickListener {
binding.autoCompleteTextView.text.clear()
binding.historyRecycler.adapter = null
showHistory()
}
binding.filterMenuImageView.setOnClickListener {
val filterOptions = arrayOf(
getString(R.string.all),
getString(R.string.videos),
getString(R.string.channels),
getString(R.string.playlists),
getString(R.string.music_songs),
getString(R.string.music_videos),
getString(R.string.music_albums),
getString(R.string.music_playlists)
)
MaterialAlertDialogBuilder(view.context)
.setTitle(getString(R.string.choose_filter))
.setItems(filterOptions) { _, id ->
apiSearchFilter = when (id) {
0 -> "all"
1 -> "videos"
2 -> "channels"
3 -> "playlists"
4 -> "music_songs"
5 -> "music_videos"
6 -> "music_albums"
7 -> "music_playlists"
else -> "all"
}
fetchSearch(binding.autoCompleteTextView.text.toString())
}
.setNegativeButton(getString(R.string.cancel), null)
.show()
}
// show search history
binding.historyRecycler.layoutManager = LinearLayoutManager(view.context)
showHistory()
binding.searchRecycler.layoutManager = GridLayoutManager(view.context, 1)
binding.autoCompleteTextView.requestFocus()
val imm =
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(binding.autoCompleteTextView, InputMethodManager.SHOW_IMPLICIT)
binding.autoCompleteTextView.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
if (s.toString() != "") {
binding.searchRecycler.adapter = null
binding.searchRecycler.viewTreeObserver
.addOnScrollChangedListener {
if (!binding.searchRecycler.canScrollVertically(1)) {
fetchNextSearchItems(binding.autoCompleteTextView.text.toString())
}
}
fetchSuggestions(s.toString())
}
}
override fun afterTextChanged(s: Editable?) {
if (s!!.isEmpty()) {
binding.historyRecycler.adapter = null
showHistory()
}
}
})
binding.autoCompleteTextView.setOnEditorActionListener(
OnEditorActionListener { textView, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH && textView.text.toString() != "") {
view.let { context?.hideKeyboard(it) }
binding.searchRecycler.visibility = VISIBLE
binding.historyRecycler.visibility = GONE
fetchSearch(binding.autoCompleteTextView.text.toString())
return@OnEditorActionListener true
}
false
}
)
// fetch the search
fetchSuggestions(query!!)
}
private fun fetchSuggestions(query: String) {
fun run() {
lifecycleScope.launchWhenCreated {
binding.searchRecycler.visibility = GONE
binding.historyRecycler.visibility = VISIBLE
val response = try {
RetrofitInstance.api.getSuggestions(query)
} catch (e: IOException) {
@ -168,74 +57,17 @@ class SearchFragment : Fragment() {
return@launchWhenCreated
}
// only load the suggestions if the input field didn't get cleared yet
if (binding.autoCompleteTextView.text.toString() != "") {
val suggestionsAdapter =
SearchSuggestionsAdapter(
response,
binding.autoCompleteTextView,
this@SearchFragment
)
binding.historyRecycler.adapter = suggestionsAdapter
}
}
}
if (!isFetchingSearch) run()
}
fun fetchSearch(query: String) {
runOnUiThread {
binding.historyRecycler.visibility = GONE
}
lifecycleScope.launchWhenCreated {
isFetchingSearch = true
view?.let { context?.hideKeyboard(it) }
val response = try {
RetrofitInstance.api.getSearchResults(query, apiSearchFilter)
} catch (e: IOException) {
println(e)
Log.e(TAG, "IOException, you might not have internet connection $e")
return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG, "HttpException, unexpected response")
return@launchWhenCreated
}
nextPage = response.nextpage
if (response.items!!.isNotEmpty()) {
runOnUiThread {
binding.searchRecycler.visibility = VISIBLE
searchAdapter = SearchAdapter(response.items, childFragmentManager)
binding.searchRecycler.adapter = searchAdapter
}
}
addToHistory(query)
isLoading = false
isFetchingSearch = false
}
}
private fun fetchNextSearchItems(query: String) {
lifecycleScope.launchWhenCreated {
if (!isLoading) {
isLoading = true
val response = try {
RetrofitInstance.api.getSearchResultsNextPage(
query,
apiSearchFilter,
nextPage!!
val suggestionsAdapter =
SearchSuggestionsAdapter(
response
)
} catch (e: IOException) {
println(e)
Log.e(TAG, "IOException, you might not have internet connection")
return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG, "HttpException, unexpected response," + e.response())
return@launchWhenCreated
runOnUiThread {
binding.suggestionsRecycler.layoutManager = LinearLayoutManager(requireContext())
binding.suggestionsRecycler.adapter = suggestionsAdapter
}
nextPage = response.nextpage
searchAdapter?.updateItems(response.items!!)
isLoading = false
}
}
run()
}
private fun Fragment?.runOnUiThread(action: () -> Unit) {
@ -244,35 +76,9 @@ class SearchFragment : Fragment() {
activity?.runOnUiThread(action)
}
override fun onResume() {
super.onResume()
requireActivity().window.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN)
}
override fun onStop() {
super.onStop()
view?.let { context?.hideKeyboard(it) }
}
private fun showHistory() {
binding.searchRecycler.visibility = GONE
val historyList = PreferenceHelper.getSearchHistory()
if (historyList.isNotEmpty()) {
binding.historyRecycler.adapter =
SearchHistoryAdapter(
historyList,
binding.autoCompleteTextView,
this
)
binding.historyRecycler.visibility = VISIBLE
}
}
private fun addToHistory(query: String) {
val searchHistoryEnabled =
PreferenceHelper.getBoolean(PreferenceKeys.SEARCH_HISTORY_TOGGLE, true)
if (searchHistoryEnabled && query != "") {
PreferenceHelper.saveToSearchHistory(query)
}
override fun onDestroy() {
// remove the backstack entries
findNavController().popBackStack(R.id.searchFragment, true)
super.onDestroy()
}
}

View File

@ -0,0 +1,137 @@
package com.github.libretube.fragments
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.adapters.SearchAdapter
import com.github.libretube.databinding.FragmentSearchResultBinding
import com.github.libretube.util.RetrofitInstance
import com.github.libretube.util.hideKeyboard
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import retrofit2.HttpException
import java.io.IOException
class SearchResultFragment : Fragment() {
private val TAG = "SearchResultFragment"
private lateinit var binding: FragmentSearchResultBinding
private lateinit var nextPage: String
private var query: String = ""
private var apiSearchFilter: String = "all"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
query = arguments?.getString("query").toString()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentSearchResultBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.filterMenuImageView.setOnClickListener {
val filterOptions = arrayOf(
getString(R.string.all),
getString(R.string.videos),
getString(R.string.channels),
getString(R.string.playlists),
getString(R.string.music_songs),
getString(R.string.music_videos),
getString(R.string.music_albums),
getString(R.string.music_playlists)
)
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.choose_filter))
.setItems(filterOptions) { _, id ->
apiSearchFilter = when (id) {
0 -> "all"
1 -> "videos"
2 -> "channels"
3 -> "playlists"
4 -> "music_songs"
5 -> "music_videos"
6 -> "music_albums"
7 -> "music_playlists"
else -> "all"
}
}
.setNegativeButton(getString(R.string.cancel), null)
.show()
}
fetchSearch()
binding.searchRecycler.viewTreeObserver
.addOnScrollChangedListener {
if (!binding.searchRecycler.canScrollVertically(1)) {
fetchNextSearchItems()
}
}
}
private fun fetchSearch() {
lifecycleScope.launchWhenCreated {
view?.let { context?.hideKeyboard(it) }
val response = try {
RetrofitInstance.api.getSearchResults(query, apiSearchFilter)
} catch (e: IOException) {
println(e)
Log.e(TAG, "IOException, you might not have internet connection $e")
return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG, "HttpException, unexpected response")
return@launchWhenCreated
}
runOnUiThread {
if (response.items?.isNotEmpty() == true) {
binding.searchRecycler.layoutManager = LinearLayoutManager(requireContext())
binding.searchRecycler.adapter = SearchAdapter(response.items, childFragmentManager)
}
}
nextPage = response.nextpage!!
}
}
private fun fetchNextSearchItems() {
lifecycleScope.launchWhenCreated {
val response = try {
RetrofitInstance.api.getSearchResultsNextPage(
query,
apiSearchFilter,
nextPage
)
} catch (e: IOException) {
println(e)
Log.e(TAG, "IOException, you might not have internet connection")
return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG, "HttpException, unexpected response," + e.response())
return@launchWhenCreated
}
nextPage = response.nextpage!!
with(binding.searchRecycler.adapter as SearchAdapter) {
if (response.items?.isNotEmpty() == true) this.updateItems(response.items)
}
}
}
private fun Fragment?.runOnUiThread(action: () -> Unit) {
this ?: return
if (!isAdded) return // Fragment not attached to an Activity
activity?.runOnUiThread(action)
}
}

View File

@ -1,101 +1,14 @@
<?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"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.SearchFragment">
<LinearLayout
android:id="@+id/searchbar_holder"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/suggestions_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
android:layout_marginVertical="10dp" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/outlinedTextField"
style="@style/Widget.Material3.CardView.Filled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:layout_weight="1"
app:cardCornerRadius="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="30dp"
android:background="@android:color/transparent"
app:hintEnabled="false">
<EditText
android:id="@+id/autoCompleteTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:hint="@string/search_hint"
android:imeOptions="actionSearch"
android:inputType="textFilter|textNoSuggestions"
android:maxLines="1"
android:padding="12dp" />
</com.google.android.material.textfield.TextInputLayout>
<ImageView
android:id="@+id/clearSearch_imageView"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_close" />
</RelativeLayout>
</com.google.android.material.card.MaterialCardView>
<ImageView
android:id="@+id/filterMenu_imageView"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginTop="25dp"
android:layout_marginEnd="20dp"
android:layout_weight="0"
android:src="@drawable/ic_filter" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/history_recycler"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginVertical="10dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/searchbar_holder" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/search_recycler"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/searchbar_holder" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".fragments.SearchFragment">
<ImageView
android:id="@+id/filterMenu_imageView"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginTop="25dp"
android:layout_marginEnd="20dp"
android:src="@drawable/ic_filter" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/search_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp" />
</LinearLayout>

View File

@ -6,7 +6,8 @@
android:id="@+id/action_search"
android:icon="@drawable/ic_search"
android:title="@string/search_hint"
app:showAsAction="ifRoom" />
app:showAsAction="ifRoom"
app:actionViewClass="androidx.appcompat.widget.SearchView" />
<item
android:id="@+id/action_settings"

View File

@ -19,12 +19,17 @@
android:id="@+id/libraryFragment"
android:name="com.github.libretube.fragments.LibraryFragment"
android:label="fragment_library"
tools:layout="@layout/fragment_library"></fragment>
tools:layout="@layout/fragment_library" />
<fragment
android:id="@+id/searchFragment"
android:name="com.github.libretube.fragments.SearchFragment"
android:label="fragment_search"
tools:layout="@layout/fragment_search" />
<fragment
android:id="@+id/searchResultFragment"
android:name="com.github.libretube.fragments.SearchResultFragment"
android:label="fragment_search"
tools:layout="@layout/fragment_search_result" />
<fragment
android:id="@+id/channelFragment"
android:name="com.github.libretube.fragments.ChannelFragment"