mirror of
				https://git.suyu.dev/suyu/suyu
				synced 2025-11-04 08:59:03 -06:00 
			
		
		
		
	android: Search Fragment
This commit is contained in:
		@@ -13,6 +13,7 @@ import android.view.ViewGroup
 | 
			
		||||
import android.widget.ImageView
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import androidx.lifecycle.lifecycleScope
 | 
			
		||||
import androidx.preference.PreferenceManager
 | 
			
		||||
import androidx.recyclerview.widget.AsyncDifferConfig
 | 
			
		||||
import androidx.recyclerview.widget.DiffUtil
 | 
			
		||||
import androidx.recyclerview.widget.ListAdapter
 | 
			
		||||
@@ -21,6 +22,7 @@ import coil.load
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import org.yuzu.yuzu_emu.NativeLibrary
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
import org.yuzu.yuzu_emu.YuzuApplication
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.CardGameBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.activities.EmulationActivity
 | 
			
		||||
import org.yuzu.yuzu_emu.model.Game
 | 
			
		||||
@@ -51,6 +53,14 @@ class GameAdapter(private val activity: AppCompatActivity) :
 | 
			
		||||
     */
 | 
			
		||||
    override fun onClick(view: View) {
 | 
			
		||||
        val holder = view.tag as GameViewHolder
 | 
			
		||||
        val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
 | 
			
		||||
        preferences.edit()
 | 
			
		||||
            .putLong(
 | 
			
		||||
                holder.game.keyLastPlayedTime,
 | 
			
		||||
                System.currentTimeMillis()
 | 
			
		||||
            )
 | 
			
		||||
            .apply()
 | 
			
		||||
 | 
			
		||||
        EmulationActivity.launch(activity, holder.game)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ import androidx.core.app.NotificationManagerCompat
 | 
			
		||||
import androidx.core.view.ViewCompat
 | 
			
		||||
import androidx.core.view.WindowInsetsCompat
 | 
			
		||||
import androidx.fragment.app.Fragment
 | 
			
		||||
import androidx.fragment.app.activityViewModels
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager
 | 
			
		||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
 | 
			
		||||
import org.yuzu.yuzu_emu.R
 | 
			
		||||
@@ -30,6 +31,7 @@ import org.yuzu.yuzu_emu.features.DocumentProvider
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
 | 
			
		||||
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
 | 
			
		||||
import org.yuzu.yuzu_emu.model.HomeSetting
 | 
			
		||||
import org.yuzu.yuzu_emu.model.HomeViewModel
 | 
			
		||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
 | 
			
		||||
 | 
			
		||||
@@ -39,6 +41,8 @@ class HomeSettingsFragment : Fragment() {
 | 
			
		||||
 | 
			
		||||
    private lateinit var mainActivity: MainActivity
 | 
			
		||||
 | 
			
		||||
    private val homeViewModel: HomeViewModel by activityViewModels()
 | 
			
		||||
 | 
			
		||||
    override fun onCreateView(
 | 
			
		||||
        inflater: LayoutInflater,
 | 
			
		||||
        container: ViewGroup?,
 | 
			
		||||
@@ -49,6 +53,7 @@ class HomeSettingsFragment : Fragment() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
 | 
			
		||||
        homeViewModel.setNavigationVisibility(visible = true, animated = false)
 | 
			
		||||
        mainActivity = requireActivity() as MainActivity
 | 
			
		||||
 | 
			
		||||
        val optionsList: List<HomeSetting> = listOf(
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,222 @@
 | 
			
		||||
// 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 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))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        gamesViewModel.searchFocused.observe(viewLifecycleOwner) { searchFocused ->
 | 
			
		||||
            if (searchFocused) {
 | 
			
		||||
                focusSearch()
 | 
			
		||||
                gamesViewModel.setSearchFocused(false)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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.games.observe(viewLifecycleOwner) { filterAndSearch() }
 | 
			
		||||
        gamesViewModel.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 = Jaccard(2)
 | 
			
		||||
        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, windowInsets: WindowInsetsCompat ->
 | 
			
		||||
            val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
 | 
			
		||||
            val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
 | 
			
		||||
            val navigationSpacing = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
 | 
			
		||||
            val chipSpacing = resources.getDimensionPixelSize(R.dimen.spacing_chip)
 | 
			
		||||
 | 
			
		||||
            binding.frameSearch.updatePadding(
 | 
			
		||||
                left = insets.left,
 | 
			
		||||
                top = insets.top,
 | 
			
		||||
                right = insets.right
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            binding.gridGamesSearch.setPadding(
 | 
			
		||||
                insets.left,
 | 
			
		||||
                extraListSpacing,
 | 
			
		||||
                insets.right,
 | 
			
		||||
                insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation) + extraListSpacing
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            binding.noResultsView.updatePadding(
 | 
			
		||||
                left = insets.left,
 | 
			
		||||
                right = insets.right,
 | 
			
		||||
                bottom = insets.bottom + navigationSpacing
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            val mlpDivider = binding.divider.layoutParams as ViewGroup.MarginLayoutParams
 | 
			
		||||
            mlpDivider.leftMargin = insets.left + chipSpacing
 | 
			
		||||
            mlpDivider.rightMargin = insets.right + chipSpacing
 | 
			
		||||
            binding.divider.layoutParams = mlpDivider
 | 
			
		||||
 | 
			
		||||
            binding.chipGroup.updatePadding(
 | 
			
		||||
                left = insets.left + chipSpacing,
 | 
			
		||||
                right = insets.right + chipSpacing
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            windowInsets
 | 
			
		||||
        }
 | 
			
		||||
}
 | 
			
		||||
@@ -71,7 +71,7 @@ class SetupFragment : Fragment() {
 | 
			
		||||
 | 
			
		||||
        mainActivity = requireActivity() as MainActivity
 | 
			
		||||
 | 
			
		||||
        homeViewModel.setNavigationVisibility(false)
 | 
			
		||||
        homeViewModel.setNavigationVisibility(visible = false, animated = false)
 | 
			
		||||
 | 
			
		||||
        requireActivity().onBackPressedDispatcher.addCallback(
 | 
			
		||||
            viewLifecycleOwner,
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,9 @@ class Game(
 | 
			
		||||
    val gameId: String,
 | 
			
		||||
    val company: String
 | 
			
		||||
) : Parcelable {
 | 
			
		||||
    val keyAddedToLibraryTime get() = "${gameId}_AddedToLibraryTime"
 | 
			
		||||
    val keyLastPlayedTime get() = "${gameId}_LastPlayed"
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        val extensions: Set<String> = HashSet(
 | 
			
		||||
            listOf(".xci", ".nsp", ".nca", ".nro")
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,9 @@ class GamesViewModel : ViewModel() {
 | 
			
		||||
    private val _shouldScrollToTop = MutableLiveData(false)
 | 
			
		||||
    val shouldScrollToTop: LiveData<Boolean> get() = _shouldScrollToTop
 | 
			
		||||
 | 
			
		||||
    private val _searchFocused = MutableLiveData(false)
 | 
			
		||||
    val searchFocused: LiveData<Boolean> get() = _searchFocused
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        reloadGames(false)
 | 
			
		||||
    }
 | 
			
		||||
@@ -45,6 +48,10 @@ class GamesViewModel : ViewModel() {
 | 
			
		||||
        _shouldScrollToTop.postValue(shouldScroll)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setSearchFocused(searchFocused: Boolean) {
 | 
			
		||||
        _searchFocused.postValue(searchFocused)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun reloadGames(directoryChanged: Boolean) {
 | 
			
		||||
        if (isReloading.value == true)
 | 
			
		||||
            return
 | 
			
		||||
 
 | 
			
		||||
@@ -5,19 +5,23 @@ import androidx.lifecycle.MutableLiveData
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
 | 
			
		||||
class HomeViewModel : ViewModel() {
 | 
			
		||||
    private val _navigationVisible = MutableLiveData(true)
 | 
			
		||||
    val navigationVisible: LiveData<Boolean> get() = _navigationVisible
 | 
			
		||||
    private val _navigationVisible = MutableLiveData<Pair<Boolean, Boolean>>()
 | 
			
		||||
    val navigationVisible: LiveData<Pair<Boolean, Boolean>> get() = _navigationVisible
 | 
			
		||||
 | 
			
		||||
    private val _statusBarShadeVisible = MutableLiveData(true)
 | 
			
		||||
    val statusBarShadeVisible: LiveData<Boolean> get() = _statusBarShadeVisible
 | 
			
		||||
 | 
			
		||||
    var navigatedToSetup = false
 | 
			
		||||
 | 
			
		||||
    fun setNavigationVisibility(visible: Boolean) {
 | 
			
		||||
        if (_navigationVisible.value == visible) {
 | 
			
		||||
    init {
 | 
			
		||||
        _navigationVisible.value = Pair(false, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
 | 
			
		||||
        if (_navigationVisible.value?.first == visible) {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        _navigationVisible.value = visible
 | 
			
		||||
        _navigationVisible.value = Pair(visible, animated)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setStatusBarShadeVisibility(visible: Boolean) {
 | 
			
		||||
 
 | 
			
		||||
@@ -52,19 +52,7 @@ class GamesFragment : Fragment() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
 | 
			
		||||
        // Use custom back navigation so the user doesn't back out of the app when trying to back
 | 
			
		||||
        // out of the search view
 | 
			
		||||
        requireActivity().onBackPressedDispatcher.addCallback(
 | 
			
		||||
            viewLifecycleOwner,
 | 
			
		||||
            object : OnBackPressedCallback(true) {
 | 
			
		||||
                override fun handleOnBackPressed() {
 | 
			
		||||
                    if (binding.searchView.currentTransitionState == TransitionState.SHOWN) {
 | 
			
		||||
                        binding.searchView.hide()
 | 
			
		||||
                    } else {
 | 
			
		||||
                        requireActivity().finish()
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        homeViewModel.setNavigationVisibility(visible = true, animated = false)
 | 
			
		||||
 | 
			
		||||
        binding.gridGames.apply {
 | 
			
		||||
            layoutManager = AutofitGridLayoutManager(
 | 
			
		||||
@@ -73,7 +61,6 @@ class GamesFragment : Fragment() {
 | 
			
		||||
            )
 | 
			
		||||
            adapter = GameAdapter(requireActivity() as AppCompatActivity)
 | 
			
		||||
        }
 | 
			
		||||
        setUpSearch()
 | 
			
		||||
 | 
			
		||||
        // Add swipe down to refresh gesture
 | 
			
		||||
        binding.swipeRefresh.setOnRefreshListener {
 | 
			
		||||
@@ -91,21 +78,16 @@ class GamesFragment : Fragment() {
 | 
			
		||||
        // Watch for when we get updates to any of our games lists
 | 
			
		||||
        gamesViewModel.isReloading.observe(viewLifecycleOwner) { isReloading ->
 | 
			
		||||
            binding.swipeRefresh.isRefreshing = isReloading
 | 
			
		||||
 | 
			
		||||
            if (!isReloading) {
 | 
			
		||||
                if (gamesViewModel.games.value!!.isEmpty()) {
 | 
			
		||||
                    binding.noticeText.visibility = View.VISIBLE
 | 
			
		||||
                } else {
 | 
			
		||||
                    binding.noticeText.visibility = View.GONE
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        gamesViewModel.games.observe(viewLifecycleOwner) {
 | 
			
		||||
            (binding.gridGames.adapter as GameAdapter).submitList(it)
 | 
			
		||||
            if (it.isEmpty()) {
 | 
			
		||||
                binding.noticeText.visibility = View.VISIBLE
 | 
			
		||||
            } else {
 | 
			
		||||
                binding.noticeText.visibility = View.GONE
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        gamesViewModel.searchedGames.observe(viewLifecycleOwner) {
 | 
			
		||||
            (binding.gridSearch.adapter as GameAdapter).submitList(it)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        gamesViewModel.shouldSwapData.observe(viewLifecycleOwner) { shouldSwapData ->
 | 
			
		||||
            if (shouldSwapData) {
 | 
			
		||||
                (binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value)
 | 
			
		||||
@@ -113,31 +95,6 @@ class GamesFragment : Fragment() {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Hide bottom navigation and FAB when using the search view
 | 
			
		||||
        binding.searchView.addTransitionListener { _: SearchView, _: TransitionState, newState: TransitionState ->
 | 
			
		||||
            when (newState) {
 | 
			
		||||
                TransitionState.SHOWING,
 | 
			
		||||
                TransitionState.SHOWN -> {
 | 
			
		||||
                    (binding.gridSearch.adapter as GameAdapter).submitList(emptyList())
 | 
			
		||||
                    searchShown()
 | 
			
		||||
                }
 | 
			
		||||
                TransitionState.HIDDEN,
 | 
			
		||||
                TransitionState.HIDING -> {
 | 
			
		||||
                    gamesViewModel.setSearchedGames(emptyList())
 | 
			
		||||
                    searchHidden()
 | 
			
		||||
                    binding.appBarSearch.setExpanded(true)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Ensure that bottom navigation or FAB don't appear upon recreation
 | 
			
		||||
        val searchState = binding.searchView.currentTransitionState
 | 
			
		||||
        if (searchState == TransitionState.SHOWN) {
 | 
			
		||||
            searchShown()
 | 
			
		||||
        } else if (searchState == TransitionState.HIDDEN) {
 | 
			
		||||
            searchHidden()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if the user reselected the games menu item and then scroll to top of the list
 | 
			
		||||
        gamesViewModel.shouldScrollToTop.observe(viewLifecycleOwner) { shouldScroll ->
 | 
			
		||||
            if (shouldScroll) {
 | 
			
		||||
@@ -162,71 +119,24 @@ class GamesFragment : Fragment() {
 | 
			
		||||
        _binding = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun searchShown() {
 | 
			
		||||
        homeViewModel.setNavigationVisibility(false)
 | 
			
		||||
        homeViewModel.setStatusBarShadeVisibility(false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun searchHidden() {
 | 
			
		||||
        homeViewModel.setNavigationVisibility(true)
 | 
			
		||||
        homeViewModel.setStatusBarShadeVisibility(true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private inner class ScoredGame(val score: Double, val item: Game)
 | 
			
		||||
 | 
			
		||||
    private fun setUpSearch() {
 | 
			
		||||
        binding.gridSearch.apply {
 | 
			
		||||
            layoutManager = AutofitGridLayoutManager(
 | 
			
		||||
                requireContext(),
 | 
			
		||||
                requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
 | 
			
		||||
            )
 | 
			
		||||
            adapter = GameAdapter(requireActivity() as AppCompatActivity)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.searchView.editText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int ->
 | 
			
		||||
            val searchTerm = text.toString().lowercase(Locale.getDefault())
 | 
			
		||||
            val searchAlgorithm = Jaccard(2)
 | 
			
		||||
            val sortedList: List<Game> = gamesViewModel.games.value!!.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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun scrollToTop() {
 | 
			
		||||
    private fun scrollToTop() {
 | 
			
		||||
        if (_binding != null) {
 | 
			
		||||
            binding.gridGames.smoothScrollToPosition(0)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setInsets() =
 | 
			
		||||
        ViewCompat.setOnApplyWindowInsetsListener(binding.gridGames) { view: View, windowInsets: WindowInsetsCompat ->
 | 
			
		||||
        ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat ->
 | 
			
		||||
            val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
 | 
			
		||||
            val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
 | 
			
		||||
            val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_large)
 | 
			
		||||
 | 
			
		||||
            view.updatePadding(
 | 
			
		||||
                top = insets.top + resources.getDimensionPixelSize(R.dimen.spacing_search),
 | 
			
		||||
            binding.gridGames.updatePadding(
 | 
			
		||||
                top = insets.top + extraListSpacing,
 | 
			
		||||
                bottom = insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation) + extraListSpacing
 | 
			
		||||
            )
 | 
			
		||||
            binding.gridSearch.updatePadding(
 | 
			
		||||
                left = insets.left,
 | 
			
		||||
                top = extraListSpacing,
 | 
			
		||||
                right = insets.right,
 | 
			
		||||
                bottom = insets.bottom + extraListSpacing
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            binding.swipeRefresh.setSlingshotDistance(
 | 
			
		||||
                resources.getDimensionPixelSize(R.dimen.spacing_refresh_slingshot)
 | 
			
		||||
            )
 | 
			
		||||
            binding.swipeRefresh.setProgressViewOffset(
 | 
			
		||||
            binding.swipeRefresh.setProgressViewEndTarget(
 | 
			
		||||
                false,
 | 
			
		||||
                insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_start),
 | 
			
		||||
                insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end)
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import android.content.Intent
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup.MarginLayoutParams
 | 
			
		||||
import android.view.WindowManager
 | 
			
		||||
import android.view.animation.PathInterpolator
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import androidx.activity.result.contract.ActivityResultContracts
 | 
			
		||||
@@ -60,6 +61,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
 | 
			
		||||
        setContentView(binding.root)
 | 
			
		||||
 | 
			
		||||
        WindowCompat.setDecorFitsSystemWindows(window, false)
 | 
			
		||||
        window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
 | 
			
		||||
 | 
			
		||||
        window.statusBarColor =
 | 
			
		||||
            ContextCompat.getColor(applicationContext, android.R.color.transparent)
 | 
			
		||||
@@ -75,26 +77,30 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
 | 
			
		||||
            supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
 | 
			
		||||
        setUpNavigation(navHostFragment.navController)
 | 
			
		||||
        (binding.navigationBar as NavigationBarView).setOnItemReselectedListener {
 | 
			
		||||
            if (it.itemId == R.id.gamesFragment) {
 | 
			
		||||
                gamesViewModel.setShouldScrollToTop(true)
 | 
			
		||||
            when (it.itemId) {
 | 
			
		||||
                R.id.gamesFragment -> gamesViewModel.setShouldScrollToTop(true)
 | 
			
		||||
                R.id.searchFragment -> gamesViewModel.setSearchFocused(true)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.statusBarShade.setBackgroundColor(
 | 
			
		||||
            MaterialColors.getColor(
 | 
			
		||||
                binding.root,
 | 
			
		||||
                R.attr.colorSurface
 | 
			
		||||
            ThemeHelper.getColorWithOpacity(
 | 
			
		||||
                MaterialColors.getColor(
 | 
			
		||||
                    binding.root,
 | 
			
		||||
                    R.attr.colorSurface
 | 
			
		||||
                ),
 | 
			
		||||
                ThemeHelper.SYSTEM_BAR_ALPHA
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        // Prevents navigation from being drawn for a short time on recreation if set to hidden
 | 
			
		||||
        if (homeViewModel.navigationVisible.value == false) {
 | 
			
		||||
        if (!homeViewModel.navigationVisible.value?.first!!) {
 | 
			
		||||
            binding.navigationBar.visibility = View.INVISIBLE
 | 
			
		||||
            binding.statusBarShade.visibility = View.INVISIBLE
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        homeViewModel.navigationVisible.observe(this) { visible ->
 | 
			
		||||
            showNavigation(visible)
 | 
			
		||||
        homeViewModel.navigationVisible.observe(this) {
 | 
			
		||||
            showNavigation(it.first, it.second)
 | 
			
		||||
        }
 | 
			
		||||
        homeViewModel.statusBarShadeVisible.observe(this) { visible ->
 | 
			
		||||
            showStatusBarShade(visible)
 | 
			
		||||
@@ -109,7 +115,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
 | 
			
		||||
    fun finishSetup(navController: NavController) {
 | 
			
		||||
        navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment)
 | 
			
		||||
        binding.navigationBar.setupWithNavController(navController)
 | 
			
		||||
        showNavigation(true)
 | 
			
		||||
        showNavigation(visible = true, animated = true)
 | 
			
		||||
 | 
			
		||||
        ThemeHelper.setNavigationBarColor(
 | 
			
		||||
            this,
 | 
			
		||||
@@ -132,7 +138,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun showNavigation(visible: Boolean) {
 | 
			
		||||
    private fun showNavigation(visible: Boolean, animated: Boolean) {
 | 
			
		||||
        if (!animated) {
 | 
			
		||||
            if (visible) {
 | 
			
		||||
                binding.navigationBar.visibility = View.VISIBLE
 | 
			
		||||
            } else {
 | 
			
		||||
                binding.navigationBar.visibility = View.INVISIBLE
 | 
			
		||||
            }
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.navigationBar.animate().apply {
 | 
			
		||||
            if (visible) {
 | 
			
		||||
                binding.navigationBar.visibility = View.VISIBLE
 | 
			
		||||
@@ -196,10 +211,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
 | 
			
		||||
        themeId = resId
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun hasExtension(path: String, extension: String): Boolean {
 | 
			
		||||
        return path.substring(path.lastIndexOf(".") + 1).contains(extension)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val getGamesDirectory =
 | 
			
		||||
        registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
 | 
			
		||||
            if (result == null)
 | 
			
		||||
@@ -232,7 +243,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
 | 
			
		||||
            if (result == null)
 | 
			
		||||
                return@registerForActivityResult
 | 
			
		||||
 | 
			
		||||
            if (!hasExtension(result.toString(), "keys")) {
 | 
			
		||||
            if (!FileUtil.hasExtension(result.toString(), "keys")) {
 | 
			
		||||
                Toast.makeText(
 | 
			
		||||
                    applicationContext,
 | 
			
		||||
                    R.string.invalid_keys_file,
 | 
			
		||||
@@ -278,7 +289,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
 | 
			
		||||
            if (result == null)
 | 
			
		||||
                return@registerForActivityResult
 | 
			
		||||
 | 
			
		||||
            if (!hasExtension(result.toString(), "bin")) {
 | 
			
		||||
            if (!FileUtil.hasExtension(result.toString(), "bin")) {
 | 
			
		||||
                Toast.makeText(
 | 
			
		||||
                    applicationContext,
 | 
			
		||||
                    R.string.invalid_keys_file,
 | 
			
		||||
 
 | 
			
		||||
@@ -292,4 +292,8 @@ object FileUtil {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun hasExtension(path: String, extension: String): Boolean {
 | 
			
		||||
        return path.substring(path.lastIndexOf(".") + 1).contains(extension)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.utils
 | 
			
		||||
 | 
			
		||||
import android.content.SharedPreferences
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import androidx.preference.PreferenceManager
 | 
			
		||||
import org.yuzu.yuzu_emu.NativeLibrary
 | 
			
		||||
@@ -14,12 +15,15 @@ import kotlin.collections.ArrayList
 | 
			
		||||
object GameHelper {
 | 
			
		||||
    const val KEY_GAME_PATH = "game_path"
 | 
			
		||||
 | 
			
		||||
    private lateinit var preferences: SharedPreferences
 | 
			
		||||
 | 
			
		||||
    fun getGames(): ArrayList<Game> {
 | 
			
		||||
        val games = ArrayList<Game>()
 | 
			
		||||
        val context = YuzuApplication.appContext
 | 
			
		||||
        val gamesDir =
 | 
			
		||||
            PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "")
 | 
			
		||||
        val gamesUri = Uri.parse(gamesDir)
 | 
			
		||||
        preferences = PreferenceManager.getDefaultSharedPreferences(context)
 | 
			
		||||
 | 
			
		||||
        // Ensure keys are loaded so that ROM metadata can be decrypted.
 | 
			
		||||
        NativeLibrary.reloadKeys()
 | 
			
		||||
@@ -60,7 +64,7 @@ object GameHelper {
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Game(
 | 
			
		||||
        val newGame = Game(
 | 
			
		||||
            name,
 | 
			
		||||
            NativeLibrary.getDescription(filePath).replace("\n", " "),
 | 
			
		||||
            NativeLibrary.getRegions(filePath),
 | 
			
		||||
@@ -68,5 +72,14 @@ object GameHelper {
 | 
			
		||||
            gameId,
 | 
			
		||||
            NativeLibrary.getCompany(filePath)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        val addedTime = preferences.getLong(newGame.keyAddedToLibraryTime, 0L)
 | 
			
		||||
        if (addedTime == 0L) {
 | 
			
		||||
            preferences.edit()
 | 
			
		||||
                .putLong(newGame.keyAddedToLibraryTime, System.currentTimeMillis())
 | 
			
		||||
                .apply()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return newGame
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_clear.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_clear.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportWidth="24"
 | 
			
		||||
    android:viewportHeight="24">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="?attr/colorControlNormal"
 | 
			
		||||
        android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_search.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_search.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportWidth="24"
 | 
			
		||||
    android:viewportHeight="24">
 | 
			
		||||
    <path
 | 
			
		||||
        android:fillColor="?attr/colorControlNormal"
 | 
			
		||||
        android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
 | 
			
		||||
</vector>
 | 
			
		||||
@@ -29,6 +29,7 @@
 | 
			
		||||
        app:layout_constraintLeft_toLeftOf="parent"
 | 
			
		||||
        app:layout_constraintRight_toRightOf="parent"
 | 
			
		||||
        app:menu="@menu/menu_navigation"
 | 
			
		||||
        app:labelVisibilityMode="selected"
 | 
			
		||||
        tools:visibility="visible" />
 | 
			
		||||
 | 
			
		||||
    <View
 | 
			
		||||
 
 | 
			
		||||
@@ -1,74 +1,34 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
 | 
			
		||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
 | 
			
		||||
    xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:app="http://schemas.android.com/apk/res-auto"
 | 
			
		||||
    xmlns:tools="http://schemas.android.com/tools"
 | 
			
		||||
    android:id="@+id/coordinator_main"
 | 
			
		||||
    android:id="@+id/swipe_refresh"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="match_parent"
 | 
			
		||||
    android:background="?attr/colorSurface">
 | 
			
		||||
    android:background="?attr/colorSurface"
 | 
			
		||||
    android:clipToPadding="false">
 | 
			
		||||
 | 
			
		||||
    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
 | 
			
		||||
        android:id="@+id/swipe_refresh"
 | 
			
		||||
    <RelativeLayout
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="match_parent"
 | 
			
		||||
        android:clipToPadding="false"
 | 
			
		||||
        app:layout_behavior="@string/searchbar_scrolling_view_behavior">
 | 
			
		||||
        android:layout_height="match_parent">
 | 
			
		||||
 | 
			
		||||
        <RelativeLayout
 | 
			
		||||
        <com.google.android.material.textview.MaterialTextView
 | 
			
		||||
            android:id="@+id/notice_text"
 | 
			
		||||
            style="@style/TextAppearance.Material3.BodyLarge"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="match_parent">
 | 
			
		||||
 | 
			
		||||
            <com.google.android.material.textview.MaterialTextView
 | 
			
		||||
                android:id="@+id/notice_text"
 | 
			
		||||
                style="@style/TextAppearance.Material3.BodyLarge"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
                android:layout_height="match_parent"
 | 
			
		||||
                android:gravity="center"
 | 
			
		||||
                android:padding="@dimen/spacing_large"
 | 
			
		||||
                android:text="@string/empty_gamelist"
 | 
			
		||||
                tools:visibility="gone" />
 | 
			
		||||
 | 
			
		||||
            <androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
                android:id="@+id/grid_games"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
                android:layout_height="match_parent"
 | 
			
		||||
                android:clipToPadding="false"
 | 
			
		||||
                tools:listitem="@layout/card_game" />
 | 
			
		||||
 | 
			
		||||
        </RelativeLayout>
 | 
			
		||||
 | 
			
		||||
    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
 | 
			
		||||
 | 
			
		||||
    <com.google.android.material.appbar.AppBarLayout
 | 
			
		||||
        android:id="@+id/app_bar_search"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:fitsSystemWindows="true"
 | 
			
		||||
        app:liftOnScrollTargetViewId="@id/grid_games">
 | 
			
		||||
 | 
			
		||||
        <com.google.android.material.search.SearchBar
 | 
			
		||||
            android:id="@+id/search_bar"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:hint="@string/home_search_games" />
 | 
			
		||||
 | 
			
		||||
    </com.google.android.material.appbar.AppBarLayout>
 | 
			
		||||
 | 
			
		||||
    <com.google.android.material.search.SearchView
 | 
			
		||||
        android:id="@+id/search_view"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="match_parent"
 | 
			
		||||
        android:hint="@string/home_search_games"
 | 
			
		||||
        app:layout_anchor="@id/search_bar">
 | 
			
		||||
            android:layout_height="match_parent"
 | 
			
		||||
            android:gravity="center"
 | 
			
		||||
            android:padding="@dimen/spacing_large"
 | 
			
		||||
            android:text="@string/empty_gamelist"
 | 
			
		||||
            tools:visibility="gone" />
 | 
			
		||||
 | 
			
		||||
        <androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
            android:id="@+id/grid_search"
 | 
			
		||||
            android:id="@+id/grid_games"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="match_parent"
 | 
			
		||||
            android:clipToPadding="false"
 | 
			
		||||
            tools:listitem="@layout/card_game" />
 | 
			
		||||
 | 
			
		||||
    </com.google.android.material.search.SearchView>
 | 
			
		||||
    </RelativeLayout>
 | 
			
		||||
 | 
			
		||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
 | 
			
		||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										180
									
								
								src/android/app/src/main/res/layout/fragment_search.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								src/android/app/src/main/res/layout/fragment_search.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,180 @@
 | 
			
		||||
<?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"
 | 
			
		||||
    xmlns:tools="http://schemas.android.com/tools"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="match_parent"
 | 
			
		||||
    android:background="?attr/colorSurface">
 | 
			
		||||
 | 
			
		||||
    <RelativeLayout
 | 
			
		||||
        android:layout_width="0dp"
 | 
			
		||||
        android:layout_height="0dp"
 | 
			
		||||
        app:layout_constraintBottom_toBottomOf="parent"
 | 
			
		||||
        app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
        app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
        app:layout_constraintTop_toBottomOf="@+id/divider">
 | 
			
		||||
 | 
			
		||||
        <LinearLayout
 | 
			
		||||
            android:id="@+id/no_results_view"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="match_parent"
 | 
			
		||||
            android:orientation="vertical"
 | 
			
		||||
            android:gravity="center">
 | 
			
		||||
 | 
			
		||||
            <ImageView
 | 
			
		||||
                android:id="@+id/icon_no_results"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
                android:layout_height="80dp"
 | 
			
		||||
                android:src="@drawable/ic_search" />
 | 
			
		||||
 | 
			
		||||
            <com.google.android.material.textview.MaterialTextView
 | 
			
		||||
                android:id="@+id/notice_text"
 | 
			
		||||
                style="@style/TextAppearance.Material3.TitleLarge"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:gravity="center"
 | 
			
		||||
                android:paddingTop="8dp"
 | 
			
		||||
                android:text="@string/search_and_filter_games"
 | 
			
		||||
                tools:visibility="visible" />
 | 
			
		||||
 | 
			
		||||
        </LinearLayout>
 | 
			
		||||
 | 
			
		||||
        <androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
            android:id="@+id/grid_games_search"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="match_parent"
 | 
			
		||||
            android:clipToPadding="false" />
 | 
			
		||||
 | 
			
		||||
    </RelativeLayout>
 | 
			
		||||
 | 
			
		||||
    <FrameLayout
 | 
			
		||||
        android:id="@+id/frame_search"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_margin="20dp"
 | 
			
		||||
        app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
        app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
        app:layout_constraintTop_toTopOf="parent">
 | 
			
		||||
 | 
			
		||||
        <com.google.android.material.card.MaterialCardView
 | 
			
		||||
            android:id="@+id/search_background"
 | 
			
		||||
            style="?attr/materialCardViewFilledStyle"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="56dp"
 | 
			
		||||
            app:cardCornerRadius="28dp">
 | 
			
		||||
 | 
			
		||||
            <LinearLayout
 | 
			
		||||
                android:id="@+id/search_container"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
                android:layout_height="match_parent"
 | 
			
		||||
                android:layout_marginStart="24dp"
 | 
			
		||||
                android:layout_marginEnd="56dp"
 | 
			
		||||
                android:orientation="horizontal">
 | 
			
		||||
 | 
			
		||||
                <ImageView
 | 
			
		||||
                    android:layout_width="28dp"
 | 
			
		||||
                    android:layout_height="28dp"
 | 
			
		||||
                    android:layout_gravity="center_vertical"
 | 
			
		||||
                    android:layout_marginEnd="24dp"
 | 
			
		||||
                    android:src="@drawable/ic_search"
 | 
			
		||||
                    app:tint="?attr/colorOnSurfaceVariant" />
 | 
			
		||||
 | 
			
		||||
                <EditText
 | 
			
		||||
                    android:id="@+id/search_text"
 | 
			
		||||
                    android:layout_width="match_parent"
 | 
			
		||||
                    android:layout_height="match_parent"
 | 
			
		||||
                    android:background="@android:color/transparent"
 | 
			
		||||
                    android:hint="@string/home_search_games"
 | 
			
		||||
                    android:inputType="text"
 | 
			
		||||
                    android:maxLines="1"
 | 
			
		||||
                    android:imeOptions="flagNoFullscreen" />
 | 
			
		||||
 | 
			
		||||
            </LinearLayout>
 | 
			
		||||
 | 
			
		||||
            <ImageView
 | 
			
		||||
                android:id="@+id/clear_button"
 | 
			
		||||
                android:layout_width="24dp"
 | 
			
		||||
                android:layout_height="24dp"
 | 
			
		||||
                android:layout_gravity="center_vertical|end"
 | 
			
		||||
                android:layout_marginEnd="24dp"
 | 
			
		||||
                android:background="?attr/selectableItemBackground"
 | 
			
		||||
                android:src="@drawable/ic_clear"
 | 
			
		||||
                android:visibility="invisible"
 | 
			
		||||
                app:tint="?attr/colorOnSurfaceVariant"
 | 
			
		||||
                tools:visibility="visible" />
 | 
			
		||||
 | 
			
		||||
        </com.google.android.material.card.MaterialCardView>
 | 
			
		||||
 | 
			
		||||
    </FrameLayout>
 | 
			
		||||
 | 
			
		||||
    <HorizontalScrollView
 | 
			
		||||
        android:id="@+id/horizontalScrollView"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:fadingEdge="horizontal"
 | 
			
		||||
        android:scrollbars="none"
 | 
			
		||||
        app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
        app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
        app:layout_constraintTop_toBottomOf="@+id/frame_search">
 | 
			
		||||
 | 
			
		||||
        <com.google.android.material.chip.ChipGroup
 | 
			
		||||
            android:id="@+id/chip_group"
 | 
			
		||||
            android:layout_width="wrap_content"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:clipToPadding="false"
 | 
			
		||||
            android:paddingVertical="4dp"
 | 
			
		||||
            app:chipSpacingHorizontal="12dp"
 | 
			
		||||
            app:singleLine="true"
 | 
			
		||||
            app:singleSelection="true">
 | 
			
		||||
 | 
			
		||||
            <com.google.android.material.chip.Chip
 | 
			
		||||
                android:id="@+id/chip_recently_played"
 | 
			
		||||
                style="@style/Widget.Material3.Chip.Suggestion.Elevated"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:checked="false"
 | 
			
		||||
                android:text="@string/search_recently_played"
 | 
			
		||||
                app:chipCornerRadius="28dp" />
 | 
			
		||||
 | 
			
		||||
            <com.google.android.material.chip.Chip
 | 
			
		||||
                android:id="@+id/chip_recently_added"
 | 
			
		||||
                style="@style/Widget.Material3.Chip.Suggestion.Elevated"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:checked="false"
 | 
			
		||||
                android:text="@string/search_recently_added"
 | 
			
		||||
                app:chipCornerRadius="28dp" />
 | 
			
		||||
 | 
			
		||||
            <com.google.android.material.chip.Chip
 | 
			
		||||
                android:id="@+id/chip_retail"
 | 
			
		||||
                style="@style/Widget.Material3.Chip.Suggestion.Elevated"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:checked="false"
 | 
			
		||||
                android:text="@string/search_retail"
 | 
			
		||||
                app:chipCornerRadius="28dp" />
 | 
			
		||||
 | 
			
		||||
            <com.google.android.material.chip.Chip
 | 
			
		||||
                android:id="@+id/chip_homebrew"
 | 
			
		||||
                style="@style/Widget.Material3.Chip.Suggestion.Elevated"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:checked="false"
 | 
			
		||||
                android:text="@string/search_homebrew"
 | 
			
		||||
                app:chipCornerRadius="28dp" />
 | 
			
		||||
 | 
			
		||||
        </com.google.android.material.chip.ChipGroup>
 | 
			
		||||
 | 
			
		||||
    </HorizontalScrollView>
 | 
			
		||||
 | 
			
		||||
    <com.google.android.material.divider.MaterialDivider
 | 
			
		||||
        android:id="@+id/divider"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_marginHorizontal="20dp"
 | 
			
		||||
        app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
        app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
        app:layout_constraintTop_toBottomOf="@+id/horizontalScrollView" />
 | 
			
		||||
 | 
			
		||||
</androidx.constraintlayout.widget.ConstraintLayout>
 | 
			
		||||
@@ -6,6 +6,11 @@
 | 
			
		||||
        android:icon="@drawable/ic_controller"
 | 
			
		||||
        android:title="@string/home_games" />
 | 
			
		||||
 | 
			
		||||
    <item
 | 
			
		||||
        android:id="@+id/searchFragment"
 | 
			
		||||
        android:icon="@drawable/ic_search"
 | 
			
		||||
        android:title="@string/home_search" />
 | 
			
		||||
 | 
			
		||||
    <item
 | 
			
		||||
        android:id="@+id/homeSettingsFragment"
 | 
			
		||||
        android:icon="@drawable/ic_settings"
 | 
			
		||||
 
 | 
			
		||||
@@ -25,4 +25,9 @@
 | 
			
		||||
            app:popUpToInclusive="true" />
 | 
			
		||||
    </fragment>
 | 
			
		||||
 | 
			
		||||
    <fragment
 | 
			
		||||
        android:id="@+id/searchFragment"
 | 
			
		||||
        android:name="org.yuzu.yuzu_emu.fragments.SearchFragment"
 | 
			
		||||
        android:label="SearchFragment" />
 | 
			
		||||
 | 
			
		||||
</navigation>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,11 +5,10 @@
 | 
			
		||||
    <dimen name="spacing_large">16dp</dimen>
 | 
			
		||||
    <dimen name="spacing_xtralarge">32dp</dimen>
 | 
			
		||||
    <dimen name="spacing_list">64dp</dimen>
 | 
			
		||||
    <dimen name="spacing_chip">20dp</dimen>
 | 
			
		||||
    <dimen name="spacing_navigation">80dp</dimen>
 | 
			
		||||
    <dimen name="spacing_search">88dp</dimen>
 | 
			
		||||
    <dimen name="spacing_refresh_slingshot">80dp</dimen>
 | 
			
		||||
    <dimen name="spacing_refresh_start">32dp</dimen>
 | 
			
		||||
    <dimen name="spacing_refresh_end">96dp</dimen>
 | 
			
		||||
    <dimen name="spacing_search">128dp</dimen>
 | 
			
		||||
    <dimen name="spacing_refresh_end">72dp</dimen>
 | 
			
		||||
    <dimen name="menu_width">256dp</dimen>
 | 
			
		||||
    <dimen name="card_width">165dp</dimen>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,10 @@
 | 
			
		||||
 | 
			
		||||
    <!-- Home strings -->
 | 
			
		||||
    <string name="home_games">Games</string>
 | 
			
		||||
    <string name="home_search">Search</string>
 | 
			
		||||
    <string name="home_settings">Settings</string>
 | 
			
		||||
    <string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
 | 
			
		||||
    <string name="search_and_filter_games">Search and filter games</string>
 | 
			
		||||
    <string name="select_games_folder">Select games folder</string>
 | 
			
		||||
    <string name="select_games_folder_description">Allows yuzu to populate the games list</string>
 | 
			
		||||
    <string name="add_games_warning">Skip selecting games folder?</string>
 | 
			
		||||
@@ -58,6 +61,10 @@
 | 
			
		||||
    <string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
 | 
			
		||||
    <string name="advanced_settings">Advanced settings</string>
 | 
			
		||||
    <string name="settings_description">Configure emulator settings</string>
 | 
			
		||||
    <string name="search_recently_played">Recently Played</string>
 | 
			
		||||
    <string name="search_recently_added">Recently Added</string>
 | 
			
		||||
    <string name="search_retail">Retail</string>
 | 
			
		||||
    <string name="search_homebrew">Homebrew</string>
 | 
			
		||||
    <string name="open_user_folder">Open yuzu folder</string>
 | 
			
		||||
    <string name="open_user_folder_description">Manage yuzu\'s internal files</string>
 | 
			
		||||
    <string name="no_file_manager">No file manager found</string>
 | 
			
		||||
@@ -151,8 +158,6 @@
 | 
			
		||||
 | 
			
		||||
    <string name="load_settings">Loading Settings…</string>
 | 
			
		||||
 | 
			
		||||
    <string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
 | 
			
		||||
 | 
			
		||||
    <!-- Software keyboard -->
 | 
			
		||||
    <string name="software_keyboard">Software Keyboard</string>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user