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.EditText import android.widget.ImageView 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.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView 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.hideKeyboard import com.github.libretube.util.RetrofitInstance import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import retrofit2.HttpException import java.io.IOException class SearchFragment : Fragment() { private val TAG = "SearchFragment" private var selectedFilter = 0 private var apiSearchFilter = "all" private var nextPage: String? = null private lateinit var searchRecView: RecyclerView private lateinit var historyRecView: RecyclerView private var searchAdapter: SearchAdapter? = null private var isLoading: Boolean = true override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.let { } } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_search, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) searchRecView = view.findViewById(R.id.search_recycler) historyRecView = view.findViewById(R.id.history_recycler) val autoTextView = view.findViewById(R.id.autoCompleteTextView) val clearSearchButton = view.findViewById(R.id.clearSearch_imageView) val filterImageView = view.findViewById(R.id.filterMenu_imageView) var tempSelectedItem = 0 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) clearSearchButton.setOnClickListener { autoTextView.text.clear() } filterImageView.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)) .setSingleChoiceItems(filterOptions, selectedFilter) { _, id -> tempSelectedItem = id } .setPositiveButton( getString(R.string.okay), ) { _, _ -> selectedFilter = tempSelectedItem apiSearchFilter = when (selectedFilter) { 0 -> "all" 1 -> "videos" 2 -> "channels" 3 -> "playlists" 4 -> "music_songs" 5 -> "music_videos" 6 -> "music_albums" 7 -> "music_playlists" else -> "all" } fetchSearch(autoTextView.text.toString()) } .setNegativeButton(getString(R.string.cancel), null) .create() .show() } // show search history searchRecView.visibility = GONE historyRecView.visibility = VISIBLE historyRecView.layoutManager = LinearLayoutManager(view.context) val historyList = getHistory() if (historyList.isNotEmpty()) { historyRecView.adapter = SearchHistoryAdapter(requireContext(), historyList, autoTextView) } searchRecView.layoutManager = GridLayoutManager(view.context, 1) autoTextView.requestFocus() val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.showSoftInput(autoTextView, InputMethodManager.SHOW_IMPLICIT) autoTextView.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!! != "") { searchRecView.adapter = null searchRecView.viewTreeObserver .addOnScrollChangedListener { if (!searchRecView.canScrollVertically(1)) { fetchNextSearchItems(autoTextView.text.toString()) } } GlobalScope.launch { fetchSuggestions(s.toString(), autoTextView) } } } override fun afterTextChanged(s: Editable?) { if (s!!.isEmpty()) { searchRecView.visibility = GONE historyRecView.visibility = GONE val historyList = getHistory() if (historyList.isNotEmpty()) { historyRecView.adapter = SearchHistoryAdapter(requireContext(), historyList, autoTextView) } } } }) autoTextView.setOnEditorActionListener( OnEditorActionListener { _, actionId, _ -> if (actionId == EditorInfo.IME_ACTION_SEARCH) { hideKeyboard() searchRecView.visibility = VISIBLE historyRecView.visibility = GONE fetchSearch(autoTextView.text.toString()) if (sharedPreferences.getBoolean( "search_history_toggle", true ) ) { val newString = autoTextView.text.toString() addToHistory(newString) } return@OnEditorActionListener true } false } ) } private fun fetchSuggestions(query: String, autoTextView: EditText) { lifecycleScope.launchWhenCreated { val response = try { RetrofitInstance.api.getSuggestions(query) } 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") return@launchWhenCreated } historyRecView.visibility = VISIBLE val suggestionsAdapter = SearchSuggestionsAdapter(response, autoTextView) historyRecView.adapter = suggestionsAdapter } } private fun fetchSearch(query: String) { lifecycleScope.launchWhenCreated { 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 { searchAdapter = SearchAdapter(response.items, childFragmentManager) searchRecView.adapter = searchAdapter } } isLoading = false } } private fun fetchNextSearchItems(query: String) { lifecycleScope.launchWhenCreated { if (!isLoading) { isLoading = true 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 searchAdapter?.updateItems(response.items!!) isLoading = false } } } private fun Fragment?.runOnUiThread(action: () -> Unit) { this ?: return if (!isAdded) return // Fragment not attached to an Activity activity?.runOnUiThread(action) } override fun onResume() { super.onResume() requireActivity().window.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN) } override fun onStop() { super.onStop() hideKeyboard() } private fun addToHistory(query: String) { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) var historyList = getHistory() if (historyList.isNotEmpty() && query == historyList[historyList.size - 1]) { return } else if (query == "") { return } else { historyList = historyList + query } if (historyList.size > 10) { historyList = historyList.takeLast(10) } val set: Set = HashSet(historyList) sharedPreferences.edit().putStringSet("search_history", set) .apply() } private fun getHistory(): List { return try { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val set: Set = sharedPreferences.getStringSet("search_history", HashSet())!! set.toList() } catch (e: Exception) { emptyList() } } }