summaryrefslogtreecommitdiffstats
path: root/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
diff options
context:
space:
mode:
Diffstat (limited to 'src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt236
1 files changed, 236 insertions, 0 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
new file mode 100644
index 000000000..ebc0f164a
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
@@ -0,0 +1,236 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.inputmethod.InputMethodManager
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.core.widget.doOnTextChanged
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.preference.PreferenceManager
+import info.debatty.java.stringsimilarity.Jaccard
+import info.debatty.java.stringsimilarity.JaroWinkler
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.adapters.GameAdapter
+import org.yuzu.yuzu_emu.databinding.FragmentSearchBinding
+import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager
+import org.yuzu.yuzu_emu.model.Game
+import org.yuzu.yuzu_emu.model.GamesViewModel
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.utils.FileUtil
+import org.yuzu.yuzu_emu.utils.Log
+import java.util.Locale
+
+class SearchFragment : Fragment() {
+ private var _binding: FragmentSearchBinding? = null
+ private val binding get() = _binding!!
+
+ private val gamesViewModel: GamesViewModel by activityViewModels()
+ private val homeViewModel: HomeViewModel by activityViewModels()
+
+ private lateinit var preferences: SharedPreferences
+
+ companion object {
+ private const val SEARCH_TEXT = "SearchText"
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentSearchBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ homeViewModel.setNavigationVisibility(visible = true, animated = false)
+ preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+
+ if (savedInstanceState != null) {
+ binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
+ }
+
+ binding.gridGamesSearch.apply {
+ layoutManager = AutofitGridLayoutManager(
+ requireContext(),
+ requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
+ )
+ adapter = GameAdapter(requireActivity() as AppCompatActivity)
+ }
+
+ binding.chipGroup.setOnCheckedStateChangeListener { _, _ -> filterAndSearch() }
+
+ binding.searchText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int ->
+ if (text.toString().isNotEmpty()) {
+ binding.clearButton.visibility = View.VISIBLE
+ } else {
+ binding.clearButton.visibility = View.INVISIBLE
+ }
+ filterAndSearch()
+ }
+
+ gamesViewModel.apply {
+ searchFocused.observe(viewLifecycleOwner) { searchFocused ->
+ if (searchFocused) {
+ focusSearch()
+ gamesViewModel.setSearchFocused(false)
+ }
+ }
+
+ games.observe(viewLifecycleOwner) { filterAndSearch() }
+ searchedGames.observe(viewLifecycleOwner) {
+ (binding.gridGamesSearch.adapter as GameAdapter).submitList(it)
+ if (it.isEmpty()) {
+ binding.noResultsView.visibility = View.VISIBLE
+ } else {
+ binding.noResultsView.visibility = View.GONE
+ }
+ }
+ }
+
+ binding.clearButton.setOnClickListener { binding.searchText.setText("") }
+
+ binding.searchBackground.setOnClickListener { focusSearch() }
+
+ setInsets()
+ filterAndSearch()
+ }
+
+ private inner class ScoredGame(val score: Double, val item: Game)
+
+ private fun filterAndSearch() {
+ val baseList = gamesViewModel.games.value!!
+ val filteredList: List<Game> = when (binding.chipGroup.checkedChipId) {
+ R.id.chip_recently_played -> {
+ baseList.filter {
+ val lastPlayedTime = preferences.getLong(it.keyLastPlayedTime, 0L)
+ lastPlayedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
+ }
+ }
+
+ R.id.chip_recently_added -> {
+ baseList.filter {
+ val addedTime = preferences.getLong(it.keyAddedToLibraryTime, 0L)
+ addedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
+ }
+ }
+
+ R.id.chip_homebrew -> {
+ baseList.filter {
+ Log.error("Guh - ${it.path}")
+ FileUtil.hasExtension(it.path, "nro")
+ || FileUtil.hasExtension(it.path, "nso")
+ }
+ }
+
+ R.id.chip_retail -> baseList.filter {
+ FileUtil.hasExtension(it.path, "xci")
+ || FileUtil.hasExtension(it.path, "nsp")
+ }
+
+ else -> baseList
+ }
+
+ if (binding.searchText.text.toString().isEmpty()
+ && binding.chipGroup.checkedChipId != View.NO_ID
+ ) {
+ gamesViewModel.setSearchedGames(filteredList)
+ return
+ }
+
+ val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault())
+ val searchAlgorithm = if (searchTerm.length > 1) Jaccard(2) else JaroWinkler()
+ val sortedList: List<Game> = filteredList.mapNotNull { game ->
+ val title = game.title.lowercase(Locale.getDefault())
+ val score = searchAlgorithm.similarity(searchTerm, title)
+ if (score > 0.03) {
+ ScoredGame(score, game)
+ } else {
+ null
+ }
+ }.sortedByDescending { it.score }.map { it.item }
+ gamesViewModel.setSearchedGames(sortedList)
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ if (_binding != null) {
+ outState.putString(SEARCH_TEXT, binding.searchText.text.toString())
+ }
+ }
+
+ private fun focusSearch() {
+ if (_binding != null) {
+ binding.searchText.requestFocus()
+ val imm =
+ requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
+ imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT)
+ }
+ }
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat ->
+ val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+ val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
+ val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
+ val spacingNavigationRail =
+ resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
+ val chipSpacing = resources.getDimensionPixelSize(R.dimen.spacing_chip)
+
+ binding.constraintSearch.updatePadding(
+ left = barInsets.left + cutoutInsets.left,
+ top = barInsets.top,
+ right = barInsets.right + cutoutInsets.right
+ )
+
+ binding.gridGamesSearch.updatePadding(
+ top = extraListSpacing,
+ bottom = barInsets.bottom + spacingNavigation + extraListSpacing
+ )
+ binding.noResultsView.updatePadding(bottom = spacingNavigation + barInsets.bottom)
+
+ val mlpDivider = binding.divider.layoutParams as ViewGroup.MarginLayoutParams
+ if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
+ binding.frameSearch.updatePadding(left = spacingNavigationRail)
+ binding.gridGamesSearch.updatePadding(left = spacingNavigationRail)
+ binding.noResultsView.updatePadding(left = spacingNavigationRail)
+ binding.chipGroup.updatePadding(
+ left = chipSpacing + spacingNavigationRail,
+ right = chipSpacing
+ )
+ mlpDivider.leftMargin = chipSpacing + spacingNavigationRail
+ mlpDivider.rightMargin = chipSpacing
+ } else {
+ binding.frameSearch.updatePadding(right = spacingNavigationRail)
+ binding.gridGamesSearch.updatePadding(right = spacingNavigationRail)
+ binding.noResultsView.updatePadding(right = spacingNavigationRail)
+ binding.chipGroup.updatePadding(
+ left = chipSpacing,
+ right = chipSpacing + spacingNavigationRail
+ )
+ mlpDivider.leftMargin = chipSpacing
+ mlpDivider.rightMargin = chipSpacing + spacingNavigationRail
+ }
+ binding.divider.layoutParams = mlpDivider
+
+ windowInsets
+ }
+}