android: Search Fragment

master
Charles Lombardo 2023-04-29 18:35:28 +07:00 committed by bunnei
parent 3281dc597e
commit 6df030998a
20 changed files with 551 additions and 189 deletions

@ -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()) {
}
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.games.observe(viewLifecycleOwner) {
(binding.gridGames.adapter as GameAdapter).submitList(it)
}
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(
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
}
}

@ -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>

@ -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,19 +1,12 @@
<?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:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
app:layout_behavior="@string/searchbar_scrolling_view_behavior">
android:background="?attr/colorSurface"
android:clipToPadding="false">
<RelativeLayout
android:layout_width="match_parent"
@ -38,37 +31,4 @@
</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">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/grid_search"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
tools:listitem="@layout/card_game" />
</com.google.android.material.search.SearchView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

@ -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>