diff --git a/app/src/main/java/com/github/libretube/activities/MainActivity.kt b/app/src/main/java/com/github/libretube/activities/MainActivity.kt index 41131a4ef..b396050fb 100644 --- a/app/src/main/java/com/github/libretube/activities/MainActivity.kt +++ b/app/src/main/java/com/github/libretube/activities/MainActivity.kt @@ -17,6 +17,7 @@ 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 @@ -46,6 +47,7 @@ class MainActivity : AppCompatActivity() { lateinit var navController: NavController private var startFragmentId = R.id.homeFragment var autoRotationEnabled = false + lateinit var searchView: SearchView override fun onCreate(savedInstanceState: Bundle?) { // set the app theme (e.g. Material You) @@ -133,6 +135,7 @@ class MainActivity : AppCompatActivity() { // clear backstack if it's the start fragment if (startFragmentId == it.itemId) navController.backQueue.clear() // set menu item on click listeners + removeSearchFocus() when (it.itemId) { R.id.homeFragment -> { navController.navigate(R.id.homeFragment) @@ -151,10 +154,37 @@ class MainActivity : AppCompatActivity() { } } + private fun removeSearchFocus() { + searchView.setQuery("", false) + searchView.clearFocus() + searchView.onActionViewCollapsed() + } + 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 + + // stuff for the search in the topBar + val searchItem = menu.findItem(R.id.action_search) + searchView = searchItem.actionView as SearchView + searchView.setMaxWidth(Integer.MAX_VALUE) + + 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 { @@ -311,6 +341,9 @@ class MainActivity : AppCompatActivity() { } override fun onBackPressed() { + // remove focus from search + removeSearchFocus() + if (binding.mainMotionLayout.progress == 0F) { try { minimizePlayer() diff --git a/app/src/main/java/com/github/libretube/adapters/SearchHistoryAdapter.kt b/app/src/main/java/com/github/libretube/adapters/SearchHistoryAdapter.kt index 1f123083c..bc3ac4384 100644 --- a/app/src/main/java/com/github/libretube/adapters/SearchHistoryAdapter.kt +++ b/app/src/main/java/com/github/libretube/adapters/SearchHistoryAdapter.kt @@ -2,16 +2,14 @@ package com.github.libretube.adapters import android.view.LayoutInflater import android.view.ViewGroup -import android.widget.EditText +import androidx.appcompat.widget.SearchView 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, - private val editText: EditText, - private val searchFragment: SearchFragment + private val searchView: SearchView ) : RecyclerView.Adapter() { @@ -37,8 +35,7 @@ class SearchHistoryAdapter( } root.setOnClickListener { - editText.setText(historyQuery) - searchFragment.fetchSearch(historyQuery) + searchView.setQuery(historyQuery, true) } } } diff --git a/app/src/main/java/com/github/libretube/adapters/SearchSuggestionsAdapter.kt b/app/src/main/java/com/github/libretube/adapters/SearchSuggestionsAdapter.kt index fa751abb1..8ab6014c2 100644 --- a/app/src/main/java/com/github/libretube/adapters/SearchSuggestionsAdapter.kt +++ b/app/src/main/java/com/github/libretube/adapters/SearchSuggestionsAdapter.kt @@ -2,15 +2,13 @@ package com.github.libretube.adapters import android.view.LayoutInflater import android.view.ViewGroup -import android.widget.EditText +import androidx.appcompat.widget.SearchView import androidx.recyclerview.widget.RecyclerView import com.github.libretube.databinding.SearchsuggestionRowBinding -import com.github.libretube.fragments.SearchFragment class SearchSuggestionsAdapter( private var suggestionsList: List, - private var editText: EditText, - private val searchFragment: SearchFragment + private val searchView: SearchView ) : RecyclerView.Adapter() { @@ -31,8 +29,7 @@ class SearchSuggestionsAdapter( holder.binding.apply { suggestionText.text = suggestion root.setOnClickListener { - editText.setText(suggestion) - searchFragment.fetchSearch(editText.text.toString()) + searchView.setQuery(suggestion, true) } } } diff --git a/app/src/main/java/com/github/libretube/fragments/SearchFragment.kt b/app/src/main/java/com/github/libretube/fragments/SearchFragment.kt index 968f8ebf3..47c2cc3fe 100644 --- a/app/src/main/java/com/github/libretube/fragments/SearchFragment.kt +++ b/app/src/main/java/com/github/libretube/fragments/SearchFragment.kt @@ -1,51 +1,34 @@ 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.activities.MainActivity 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 +43,26 @@ 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() + // add the query to the history + if (query != null) addToHistory(query!!) + + binding.suggestionsRecycler.layoutManager = LinearLayoutManager(requireContext()) + // fetch the search or history + if (query == null || query == "") showHistory() + else fetchSuggestions(query!!) + } + + private fun addToHistory(query: String) { + val searchHistoryEnabled = + PreferenceHelper.getBoolean(PreferenceKeys.SEARCH_HISTORY_TOGGLE, true) + if (searchHistoryEnabled && query != "") { + PreferenceHelper.saveToSearchHistory(query) } - - 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 - } - ) } 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 +74,28 @@ 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, + (activity as MainActivity).searchView ) - } 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.adapter = suggestionsAdapter } - nextPage = response.nextpage - searchAdapter?.updateItems(response.items!!) - isLoading = false } } + run() + } + + private fun showHistory() { + val historyList = PreferenceHelper.getSearchHistory() + if (historyList.isNotEmpty()) { + binding.suggestionsRecycler.adapter = + SearchHistoryAdapter( + historyList, + (activity as MainActivity).searchView + ) + } } private fun Fragment?.runOnUiThread(action: () -> Unit) { @@ -244,35 +104,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() } } diff --git a/app/src/main/java/com/github/libretube/fragments/SearchResultFragment.kt b/app/src/main/java/com/github/libretube/fragments/SearchResultFragment.kt new file mode 100644 index 000000000..6c4fa41b3 --- /dev/null +++ b/app/src/main/java/com/github/libretube/fragments/SearchResultFragment.kt @@ -0,0 +1,128 @@ +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 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 lateinit var searchAdapter: SearchAdapter + 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) + + // filter options + binding.filterChipGroup.setOnCheckedStateChangeListener { _, _ -> + apiSearchFilter = when ( + binding.filterChipGroup.checkedChipId + ) { + R.id.chip_all -> "all" + R.id.chip_videos -> "videos" + R.id.chip_channels -> "channels" + R.id.chip_playlists -> "playlists" + R.id.chip_music_songs -> "music_songs" + R.id.chip_music_videos -> "music_videos" + R.id.chip_music_albums -> "music_albums" + R.id.chip_music_playlists -> "music_playlists" + else -> throw IllegalArgumentException("Filter out of range") + } + fetchSearch() + } + + 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()) + searchAdapter = SearchAdapter(response.items, childFragmentManager) + binding.searchRecycler.adapter = searchAdapter + } + } + 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!! + kotlin.runCatching { + if (response.items?.isNotEmpty() == true) { + searchAdapter.updateItems(response.items.toMutableList()) + } + } + } + } + + private fun Fragment?.runOnUiThread(action: () -> Unit) { + this ?: return + if (!isAdded) return // Fragment not attached to an Activity + activity?.runOnUiThread(action) + } +} diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index ddc946bc5..efdad326a 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -1,101 +1,14 @@ - - + android:layout_marginVertical="10dp" /> - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search_result.xml b/app/src/main/res/layout/fragment_search_result.xml new file mode 100644 index 000000000..30245a28a --- /dev/null +++ b/app/src/main/res/layout/fragment_search_result.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/action_bar.xml b/app/src/main/res/menu/action_bar.xml index 14d5d8619..9039aeaa4 100644 --- a/app/src/main/res/menu/action_bar.xml +++ b/app/src/main/res/menu/action_bar.xml @@ -6,7 +6,9 @@ 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" + android:focusableInTouchMode="true" /> + tools:layout="@layout/fragment_library" /> + @android:color/white + + \ No newline at end of file