summaryrefslogtreecommitdiff
path: root/src/android
diff options
context:
space:
mode:
authorGravatar Charles Lombardo2023-04-29 18:35:28 -0400
committerGravatar bunnei2023-06-03 00:05:57 -0700
commit6df030998a254bdf2a713d7b326bc3dd7f69acae (patch)
tree36c3573678c6fb3cc8d3ec6f512e5a4bd19013ee /src/android
parentandroid: Fix potential zip traversal exploit (diff)
downloadyuzu-6df030998a254bdf2a713d7b326bc3dd7f69acae.tar.gz
yuzu-6df030998a254bdf2a713d7b326bc3dd7f69acae.tar.xz
yuzu-6df030998a254bdf2a713d7b326bc3dd7f69acae.zip
android: Search Fragment
Diffstat (limited to 'src/android')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt10
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt5
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt222
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt7
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt116
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt43
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt15
-rw-r--r--src/android/app/src/main/res/drawable/ic_clear.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_search.xml9
-rw-r--r--src/android/app/src/main/res/layout/activity_main.xml1
-rw-r--r--src/android/app/src/main/res/layout/fragment_games.xml74
-rw-r--r--src/android/app/src/main/res/layout/fragment_search.xml180
-rw-r--r--src/android/app/src/main/res/menu/menu_navigation.xml5
-rw-r--r--src/android/app/src/main/res/navigation/home_navigation.xml5
-rw-r--r--src/android/app/src/main/res/values/dimens.xml7
-rw-r--r--src/android/app/src/main/res/values/strings.xml9
20 files changed, 551 insertions, 189 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
index eca84a694..b9f975e2b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
@@ -13,6 +13,7 @@ import android.view.ViewGroup
13import android.widget.ImageView 13import android.widget.ImageView
14import androidx.appcompat.app.AppCompatActivity 14import androidx.appcompat.app.AppCompatActivity
15import androidx.lifecycle.lifecycleScope 15import androidx.lifecycle.lifecycleScope
16import androidx.preference.PreferenceManager
16import androidx.recyclerview.widget.AsyncDifferConfig 17import androidx.recyclerview.widget.AsyncDifferConfig
17import androidx.recyclerview.widget.DiffUtil 18import androidx.recyclerview.widget.DiffUtil
18import androidx.recyclerview.widget.ListAdapter 19import androidx.recyclerview.widget.ListAdapter
@@ -21,6 +22,7 @@ import coil.load
21import kotlinx.coroutines.launch 22import kotlinx.coroutines.launch
22import org.yuzu.yuzu_emu.NativeLibrary 23import org.yuzu.yuzu_emu.NativeLibrary
23import org.yuzu.yuzu_emu.R 24import org.yuzu.yuzu_emu.R
25import org.yuzu.yuzu_emu.YuzuApplication
24import org.yuzu.yuzu_emu.databinding.CardGameBinding 26import org.yuzu.yuzu_emu.databinding.CardGameBinding
25import org.yuzu.yuzu_emu.activities.EmulationActivity 27import org.yuzu.yuzu_emu.activities.EmulationActivity
26import org.yuzu.yuzu_emu.model.Game 28import org.yuzu.yuzu_emu.model.Game
@@ -51,6 +53,14 @@ class GameAdapter(private val activity: AppCompatActivity) :
51 */ 53 */
52 override fun onClick(view: View) { 54 override fun onClick(view: View) {
53 val holder = view.tag as GameViewHolder 55 val holder = view.tag as GameViewHolder
56 val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
57 preferences.edit()
58 .putLong(
59 holder.game.keyLastPlayedTime,
60 System.currentTimeMillis()
61 )
62 .apply()
63
54 EmulationActivity.launch(activity, holder.game) 64 EmulationActivity.launch(activity, holder.game)
55 } 65 }
56 66
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
index 0e7c181ea..eb29d6c96 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
@@ -21,6 +21,7 @@ import androidx.core.app.NotificationManagerCompat
21import androidx.core.view.ViewCompat 21import androidx.core.view.ViewCompat
22import androidx.core.view.WindowInsetsCompat 22import androidx.core.view.WindowInsetsCompat
23import androidx.fragment.app.Fragment 23import androidx.fragment.app.Fragment
24import androidx.fragment.app.activityViewModels
24import androidx.recyclerview.widget.LinearLayoutManager 25import androidx.recyclerview.widget.LinearLayoutManager
25import com.google.android.material.dialog.MaterialAlertDialogBuilder 26import com.google.android.material.dialog.MaterialAlertDialogBuilder
26import org.yuzu.yuzu_emu.R 27import org.yuzu.yuzu_emu.R
@@ -30,6 +31,7 @@ import org.yuzu.yuzu_emu.features.DocumentProvider
30import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity 31import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
31import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile 32import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
32import org.yuzu.yuzu_emu.model.HomeSetting 33import org.yuzu.yuzu_emu.model.HomeSetting
34import org.yuzu.yuzu_emu.model.HomeViewModel
33import org.yuzu.yuzu_emu.ui.main.MainActivity 35import org.yuzu.yuzu_emu.ui.main.MainActivity
34import org.yuzu.yuzu_emu.utils.GpuDriverHelper 36import org.yuzu.yuzu_emu.utils.GpuDriverHelper
35 37
@@ -39,6 +41,8 @@ class HomeSettingsFragment : Fragment() {
39 41
40 private lateinit var mainActivity: MainActivity 42 private lateinit var mainActivity: MainActivity
41 43
44 private val homeViewModel: HomeViewModel by activityViewModels()
45
42 override fun onCreateView( 46 override fun onCreateView(
43 inflater: LayoutInflater, 47 inflater: LayoutInflater,
44 container: ViewGroup?, 48 container: ViewGroup?,
@@ -49,6 +53,7 @@ class HomeSettingsFragment : Fragment() {
49 } 53 }
50 54
51 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 55 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
56 homeViewModel.setNavigationVisibility(visible = true, animated = false)
52 mainActivity = requireActivity() as MainActivity 57 mainActivity = requireActivity() as MainActivity
53 58
54 val optionsList: List<HomeSetting> = listOf( 59 val optionsList: List<HomeSetting> = listOf(
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
new file mode 100644
index 000000000..5babd9bbf
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
@@ -0,0 +1,222 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.content.Context
7import android.content.SharedPreferences
8import android.os.Bundle
9import android.view.LayoutInflater
10import android.view.View
11import android.view.ViewGroup
12import android.view.inputmethod.InputMethodManager
13import androidx.appcompat.app.AppCompatActivity
14import androidx.core.view.ViewCompat
15import androidx.core.view.WindowInsetsCompat
16import androidx.core.view.updatePadding
17import androidx.core.widget.doOnTextChanged
18import androidx.fragment.app.Fragment
19import androidx.fragment.app.activityViewModels
20import androidx.preference.PreferenceManager
21import info.debatty.java.stringsimilarity.Jaccard
22import org.yuzu.yuzu_emu.R
23import org.yuzu.yuzu_emu.YuzuApplication
24import org.yuzu.yuzu_emu.adapters.GameAdapter
25import org.yuzu.yuzu_emu.databinding.FragmentSearchBinding
26import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager
27import org.yuzu.yuzu_emu.model.Game
28import org.yuzu.yuzu_emu.model.GamesViewModel
29import org.yuzu.yuzu_emu.model.HomeViewModel
30import org.yuzu.yuzu_emu.utils.FileUtil
31import org.yuzu.yuzu_emu.utils.Log
32import java.util.Locale
33
34class SearchFragment : Fragment() {
35 private var _binding: FragmentSearchBinding? = null
36 private val binding get() = _binding!!
37
38 private val gamesViewModel: GamesViewModel by activityViewModels()
39 private val homeViewModel: HomeViewModel by activityViewModels()
40
41 private lateinit var preferences: SharedPreferences
42
43 companion object {
44 private const val SEARCH_TEXT = "SearchText"
45 }
46
47 override fun onCreateView(
48 inflater: LayoutInflater,
49 container: ViewGroup?,
50 savedInstanceState: Bundle?
51 ): View {
52 _binding = FragmentSearchBinding.inflate(layoutInflater)
53 return binding.root
54 }
55
56 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
57 homeViewModel.setNavigationVisibility(visible = true, animated = false)
58 preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
59
60 if (savedInstanceState != null) {
61 binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
62 }
63
64 gamesViewModel.searchFocused.observe(viewLifecycleOwner) { searchFocused ->
65 if (searchFocused) {
66 focusSearch()
67 gamesViewModel.setSearchFocused(false)
68 }
69 }
70
71 binding.gridGamesSearch.apply {
72 layoutManager = AutofitGridLayoutManager(
73 requireContext(),
74 requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
75 )
76 adapter = GameAdapter(requireActivity() as AppCompatActivity)
77 }
78
79 binding.chipGroup.setOnCheckedStateChangeListener { _, _ -> filterAndSearch() }
80
81 binding.searchText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int ->
82 if (text.toString().isNotEmpty()) {
83 binding.clearButton.visibility = View.VISIBLE
84 } else {
85 binding.clearButton.visibility = View.INVISIBLE
86 }
87 filterAndSearch()
88 }
89
90 gamesViewModel.games.observe(viewLifecycleOwner) { filterAndSearch() }
91 gamesViewModel.searchedGames.observe(viewLifecycleOwner) {
92 (binding.gridGamesSearch.adapter as GameAdapter).submitList(it)
93 if (it.isEmpty()) {
94 binding.noResultsView.visibility = View.VISIBLE
95 } else {
96 binding.noResultsView.visibility = View.GONE
97 }
98 }
99
100 binding.clearButton.setOnClickListener { binding.searchText.setText("") }
101
102 binding.searchBackground.setOnClickListener { focusSearch() }
103
104 setInsets()
105 filterAndSearch()
106 }
107
108 private inner class ScoredGame(val score: Double, val item: Game)
109
110 private fun filterAndSearch() {
111 val baseList = gamesViewModel.games.value!!
112 val filteredList: List<Game> = when (binding.chipGroup.checkedChipId) {
113 R.id.chip_recently_played -> {
114 baseList.filter {
115 val lastPlayedTime = preferences.getLong(it.keyLastPlayedTime, 0L)
116 lastPlayedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
117 }
118 }
119
120 R.id.chip_recently_added -> {
121 baseList.filter {
122 val addedTime = preferences.getLong(it.keyAddedToLibraryTime, 0L)
123 addedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
124 }
125 }
126
127 R.id.chip_homebrew -> {
128 baseList.filter {
129 Log.error("Guh - ${it.path}")
130 FileUtil.hasExtension(it.path, "nro")
131 || FileUtil.hasExtension(it.path, "nso")
132 }
133 }
134
135 R.id.chip_retail -> baseList.filter {
136 FileUtil.hasExtension(it.path, "xci")
137 || FileUtil.hasExtension(it.path, "nsp")
138 }
139
140 else -> baseList
141 }
142
143 if (binding.searchText.text.toString().isEmpty()
144 && binding.chipGroup.checkedChipId != View.NO_ID) {
145 gamesViewModel.setSearchedGames(filteredList)
146 return
147 }
148
149 val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault())
150 val searchAlgorithm = Jaccard(2)
151 val sortedList: List<Game> = filteredList.mapNotNull { game ->
152 val title = game.title.lowercase(Locale.getDefault())
153 val score = searchAlgorithm.similarity(searchTerm, title)
154 if (score > 0.03) {
155 ScoredGame(score, game)
156 } else {
157 null
158 }
159 }.sortedByDescending { it.score }.map { it.item }
160 gamesViewModel.setSearchedGames(sortedList)
161 }
162
163 override fun onDestroyView() {
164 super.onDestroyView()
165 _binding = null
166 }
167
168 override fun onSaveInstanceState(outState: Bundle) {
169 super.onSaveInstanceState(outState)
170 if (_binding != null) {
171 outState.putString(SEARCH_TEXT, binding.searchText.text.toString())
172 }
173 }
174
175 private fun focusSearch() {
176 if (_binding != null) {
177 binding.searchText.requestFocus()
178 val imm =
179 requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
180 imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT)
181 }
182 }
183
184 private fun setInsets() =
185 ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _: View, windowInsets: WindowInsetsCompat ->
186 val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
187 val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
188 val navigationSpacing = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
189 val chipSpacing = resources.getDimensionPixelSize(R.dimen.spacing_chip)
190
191 binding.frameSearch.updatePadding(
192 left = insets.left,
193 top = insets.top,
194 right = insets.right
195 )
196
197 binding.gridGamesSearch.setPadding(
198 insets.left,
199 extraListSpacing,
200 insets.right,
201 insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation) + extraListSpacing
202 )
203
204 binding.noResultsView.updatePadding(
205 left = insets.left,
206 right = insets.right,
207 bottom = insets.bottom + navigationSpacing
208 )
209
210 val mlpDivider = binding.divider.layoutParams as ViewGroup.MarginLayoutParams
211 mlpDivider.leftMargin = insets.left + chipSpacing
212 mlpDivider.rightMargin = insets.right + chipSpacing
213 binding.divider.layoutParams = mlpDivider
214
215 binding.chipGroup.updatePadding(
216 left = insets.left + chipSpacing,
217 right = insets.right + chipSpacing
218 )
219
220 windowInsets
221 }
222}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
index 3d2f8719c..13b8315db 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
@@ -71,7 +71,7 @@ class SetupFragment : Fragment() {
71 71
72 mainActivity = requireActivity() as MainActivity 72 mainActivity = requireActivity() as MainActivity
73 73
74 homeViewModel.setNavigationVisibility(false) 74 homeViewModel.setNavigationVisibility(visible = false, animated = false)
75 75
76 requireActivity().onBackPressedDispatcher.addCallback( 76 requireActivity().onBackPressedDispatcher.addCallback(
77 viewLifecycleOwner, 77 viewLifecycleOwner,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
index db494e40f..c5cde9d05 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
@@ -16,6 +16,9 @@ class Game(
16 val gameId: String, 16 val gameId: String,
17 val company: String 17 val company: String
18) : Parcelable { 18) : Parcelable {
19 val keyAddedToLibraryTime get() = "${gameId}_AddedToLibraryTime"
20 val keyLastPlayedTime get() = "${gameId}_LastPlayed"
21
19 companion object { 22 companion object {
20 val extensions: Set<String> = HashSet( 23 val extensions: Set<String> = HashSet(
21 listOf(".xci", ".nsp", ".nca", ".nro") 24 listOf(".xci", ".nsp", ".nca", ".nro")
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
index 95bad38c6..1d0846b08 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
@@ -29,6 +29,9 @@ class GamesViewModel : ViewModel() {
29 private val _shouldScrollToTop = MutableLiveData(false) 29 private val _shouldScrollToTop = MutableLiveData(false)
30 val shouldScrollToTop: LiveData<Boolean> get() = _shouldScrollToTop 30 val shouldScrollToTop: LiveData<Boolean> get() = _shouldScrollToTop
31 31
32 private val _searchFocused = MutableLiveData(false)
33 val searchFocused: LiveData<Boolean> get() = _searchFocused
34
32 init { 35 init {
33 reloadGames(false) 36 reloadGames(false)
34 } 37 }
@@ -45,6 +48,10 @@ class GamesViewModel : ViewModel() {
45 _shouldScrollToTop.postValue(shouldScroll) 48 _shouldScrollToTop.postValue(shouldScroll)
46 } 49 }
47 50
51 fun setSearchFocused(searchFocused: Boolean) {
52 _searchFocused.postValue(searchFocused)
53 }
54
48 fun reloadGames(directoryChanged: Boolean) { 55 fun reloadGames(directoryChanged: Boolean) {
49 if (isReloading.value == true) 56 if (isReloading.value == true)
50 return 57 return
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
index acda8663a..b959ae4ba 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
@@ -5,19 +5,23 @@ import androidx.lifecycle.MutableLiveData
5import androidx.lifecycle.ViewModel 5import androidx.lifecycle.ViewModel
6 6
7class HomeViewModel : ViewModel() { 7class HomeViewModel : ViewModel() {
8 private val _navigationVisible = MutableLiveData(true) 8 private val _navigationVisible = MutableLiveData<Pair<Boolean, Boolean>>()
9 val navigationVisible: LiveData<Boolean> get() = _navigationVisible 9 val navigationVisible: LiveData<Pair<Boolean, Boolean>> get() = _navigationVisible
10 10
11 private val _statusBarShadeVisible = MutableLiveData(true) 11 private val _statusBarShadeVisible = MutableLiveData(true)
12 val statusBarShadeVisible: LiveData<Boolean> get() = _statusBarShadeVisible 12 val statusBarShadeVisible: LiveData<Boolean> get() = _statusBarShadeVisible
13 13
14 var navigatedToSetup = false 14 var navigatedToSetup = false
15 15
16 fun setNavigationVisibility(visible: Boolean) { 16 init {
17 if (_navigationVisible.value == visible) { 17 _navigationVisible.value = Pair(false, false)
18 }
19
20 fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
21 if (_navigationVisible.value?.first == visible) {
18 return 22 return
19 } 23 }
20 _navigationVisible.value = visible 24 _navigationVisible.value = Pair(visible, animated)
21 } 25 }
22 26
23 fun setStatusBarShadeVisibility(visible: Boolean) { 27 fun setStatusBarShadeVisibility(visible: Boolean) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
index 227ca1afc..6f9e04f7e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
@@ -52,19 +52,7 @@ class GamesFragment : Fragment() {
52 } 52 }
53 53
54 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 54 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
55 // Use custom back navigation so the user doesn't back out of the app when trying to back 55 homeViewModel.setNavigationVisibility(visible = true, animated = false)
56 // out of the search view
57 requireActivity().onBackPressedDispatcher.addCallback(
58 viewLifecycleOwner,
59 object : OnBackPressedCallback(true) {
60 override fun handleOnBackPressed() {
61 if (binding.searchView.currentTransitionState == TransitionState.SHOWN) {
62 binding.searchView.hide()
63 } else {
64 requireActivity().finish()
65 }
66 }
67 })
68 56
69 binding.gridGames.apply { 57 binding.gridGames.apply {
70 layoutManager = AutofitGridLayoutManager( 58 layoutManager = AutofitGridLayoutManager(
@@ -73,7 +61,6 @@ class GamesFragment : Fragment() {
73 ) 61 )
74 adapter = GameAdapter(requireActivity() as AppCompatActivity) 62 adapter = GameAdapter(requireActivity() as AppCompatActivity)
75 } 63 }
76 setUpSearch()
77 64
78 // Add swipe down to refresh gesture 65 // Add swipe down to refresh gesture
79 binding.swipeRefresh.setOnRefreshListener { 66 binding.swipeRefresh.setOnRefreshListener {
@@ -91,21 +78,16 @@ class GamesFragment : Fragment() {
91 // Watch for when we get updates to any of our games lists 78 // Watch for when we get updates to any of our games lists
92 gamesViewModel.isReloading.observe(viewLifecycleOwner) { isReloading -> 79 gamesViewModel.isReloading.observe(viewLifecycleOwner) { isReloading ->
93 binding.swipeRefresh.isRefreshing = isReloading 80 binding.swipeRefresh.isRefreshing = isReloading
94
95 if (!isReloading) {
96 if (gamesViewModel.games.value!!.isEmpty()) {
97 binding.noticeText.visibility = View.VISIBLE
98 } else {
99 binding.noticeText.visibility = View.GONE
100 }
101 }
102 } 81 }
103 gamesViewModel.games.observe(viewLifecycleOwner) { 82 gamesViewModel.games.observe(viewLifecycleOwner) {
104 (binding.gridGames.adapter as GameAdapter).submitList(it) 83 (binding.gridGames.adapter as GameAdapter).submitList(it)
84 if (it.isEmpty()) {
85 binding.noticeText.visibility = View.VISIBLE
86 } else {
87 binding.noticeText.visibility = View.GONE
88 }
105 } 89 }
106 gamesViewModel.searchedGames.observe(viewLifecycleOwner) { 90
107 (binding.gridSearch.adapter as GameAdapter).submitList(it)
108 }
109 gamesViewModel.shouldSwapData.observe(viewLifecycleOwner) { shouldSwapData -> 91 gamesViewModel.shouldSwapData.observe(viewLifecycleOwner) { shouldSwapData ->
110 if (shouldSwapData) { 92 if (shouldSwapData) {
111 (binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value) 93 (binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value)
@@ -113,31 +95,6 @@ class GamesFragment : Fragment() {
113 } 95 }
114 } 96 }
115 97
116 // Hide bottom navigation and FAB when using the search view
117 binding.searchView.addTransitionListener { _: SearchView, _: TransitionState, newState: TransitionState ->
118 when (newState) {
119 TransitionState.SHOWING,
120 TransitionState.SHOWN -> {
121 (binding.gridSearch.adapter as GameAdapter).submitList(emptyList())
122 searchShown()
123 }
124 TransitionState.HIDDEN,
125 TransitionState.HIDING -> {
126 gamesViewModel.setSearchedGames(emptyList())
127 searchHidden()
128 binding.appBarSearch.setExpanded(true)
129 }
130 }
131 }
132
133 // Ensure that bottom navigation or FAB don't appear upon recreation
134 val searchState = binding.searchView.currentTransitionState
135 if (searchState == TransitionState.SHOWN) {
136 searchShown()
137 } else if (searchState == TransitionState.HIDDEN) {
138 searchHidden()
139 }
140
141 // Check if the user reselected the games menu item and then scroll to top of the list 98 // Check if the user reselected the games menu item and then scroll to top of the list
142 gamesViewModel.shouldScrollToTop.observe(viewLifecycleOwner) { shouldScroll -> 99 gamesViewModel.shouldScrollToTop.observe(viewLifecycleOwner) { shouldScroll ->
143 if (shouldScroll) { 100 if (shouldScroll) {
@@ -162,71 +119,24 @@ class GamesFragment : Fragment() {
162 _binding = null 119 _binding = null
163 } 120 }
164 121
165 private fun searchShown() { 122 private fun scrollToTop() {
166 homeViewModel.setNavigationVisibility(false)
167 homeViewModel.setStatusBarShadeVisibility(false)
168 }
169
170 private fun searchHidden() {
171 homeViewModel.setNavigationVisibility(true)
172 homeViewModel.setStatusBarShadeVisibility(true)
173 }
174
175 private inner class ScoredGame(val score: Double, val item: Game)
176
177 private fun setUpSearch() {
178 binding.gridSearch.apply {
179 layoutManager = AutofitGridLayoutManager(
180 requireContext(),
181 requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
182 )
183 adapter = GameAdapter(requireActivity() as AppCompatActivity)
184 }
185
186 binding.searchView.editText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int ->
187 val searchTerm = text.toString().lowercase(Locale.getDefault())
188 val searchAlgorithm = Jaccard(2)
189 val sortedList: List<Game> = gamesViewModel.games.value!!.mapNotNull { game ->
190 val title = game.title.lowercase(Locale.getDefault())
191 val score = searchAlgorithm.similarity(searchTerm, title)
192 if (score > 0.03) {
193 ScoredGame(score, game)
194 } else {
195 null
196 }
197 }.sortedByDescending { it.score }.map { it.item }
198 gamesViewModel.setSearchedGames(sortedList)
199 }
200 }
201
202 fun scrollToTop() {
203 if (_binding != null) { 123 if (_binding != null) {
204 binding.gridGames.smoothScrollToPosition(0) 124 binding.gridGames.smoothScrollToPosition(0)
205 } 125 }
206 } 126 }
207 127
208 private fun setInsets() = 128 private fun setInsets() =
209 ViewCompat.setOnApplyWindowInsetsListener(binding.gridGames) { view: View, windowInsets: WindowInsetsCompat -> 129 ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat ->
210 val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) 130 val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
211 val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med) 131 val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_large)
212 132
213 view.updatePadding( 133 binding.gridGames.updatePadding(
214 top = insets.top + resources.getDimensionPixelSize(R.dimen.spacing_search), 134 top = insets.top + extraListSpacing,
215 bottom = insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation) + extraListSpacing 135 bottom = insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation) + extraListSpacing
216 ) 136 )
217 binding.gridSearch.updatePadding(
218 left = insets.left,
219 top = extraListSpacing,
220 right = insets.right,
221 bottom = insets.bottom + extraListSpacing
222 )
223 137
224 binding.swipeRefresh.setSlingshotDistance( 138 binding.swipeRefresh.setProgressViewEndTarget(
225 resources.getDimensionPixelSize(R.dimen.spacing_refresh_slingshot)
226 )
227 binding.swipeRefresh.setProgressViewOffset(
228 false, 139 false,
229 insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_start),
230 insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end) 140 insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end)
231 ) 141 )
232 142
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
index 473d38a29..35b66d1f2 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
@@ -7,6 +7,7 @@ import android.content.Intent
7import android.os.Bundle 7import android.os.Bundle
8import android.view.View 8import android.view.View
9import android.view.ViewGroup.MarginLayoutParams 9import android.view.ViewGroup.MarginLayoutParams
10import android.view.WindowManager
10import android.view.animation.PathInterpolator 11import android.view.animation.PathInterpolator
11import android.widget.Toast 12import android.widget.Toast
12import androidx.activity.result.contract.ActivityResultContracts 13import androidx.activity.result.contract.ActivityResultContracts
@@ -60,6 +61,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
60 setContentView(binding.root) 61 setContentView(binding.root)
61 62
62 WindowCompat.setDecorFitsSystemWindows(window, false) 63 WindowCompat.setDecorFitsSystemWindows(window, false)
64 window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
63 65
64 window.statusBarColor = 66 window.statusBarColor =
65 ContextCompat.getColor(applicationContext, android.R.color.transparent) 67 ContextCompat.getColor(applicationContext, android.R.color.transparent)
@@ -75,26 +77,30 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
75 supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment 77 supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
76 setUpNavigation(navHostFragment.navController) 78 setUpNavigation(navHostFragment.navController)
77 (binding.navigationBar as NavigationBarView).setOnItemReselectedListener { 79 (binding.navigationBar as NavigationBarView).setOnItemReselectedListener {
78 if (it.itemId == R.id.gamesFragment) { 80 when (it.itemId) {
79 gamesViewModel.setShouldScrollToTop(true) 81 R.id.gamesFragment -> gamesViewModel.setShouldScrollToTop(true)
82 R.id.searchFragment -> gamesViewModel.setSearchFocused(true)
80 } 83 }
81 } 84 }
82 85
83 binding.statusBarShade.setBackgroundColor( 86 binding.statusBarShade.setBackgroundColor(
84 MaterialColors.getColor( 87 ThemeHelper.getColorWithOpacity(
85 binding.root, 88 MaterialColors.getColor(
86 R.attr.colorSurface 89 binding.root,
90 R.attr.colorSurface
91 ),
92 ThemeHelper.SYSTEM_BAR_ALPHA
87 ) 93 )
88 ) 94 )
89 95
90 // Prevents navigation from being drawn for a short time on recreation if set to hidden 96 // Prevents navigation from being drawn for a short time on recreation if set to hidden
91 if (homeViewModel.navigationVisible.value == false) { 97 if (!homeViewModel.navigationVisible.value?.first!!) {
92 binding.navigationBar.visibility = View.INVISIBLE 98 binding.navigationBar.visibility = View.INVISIBLE
93 binding.statusBarShade.visibility = View.INVISIBLE 99 binding.statusBarShade.visibility = View.INVISIBLE
94 } 100 }
95 101
96 homeViewModel.navigationVisible.observe(this) { visible -> 102 homeViewModel.navigationVisible.observe(this) {
97 showNavigation(visible) 103 showNavigation(it.first, it.second)
98 } 104 }
99 homeViewModel.statusBarShadeVisible.observe(this) { visible -> 105 homeViewModel.statusBarShadeVisible.observe(this) { visible ->
100 showStatusBarShade(visible) 106 showStatusBarShade(visible)
@@ -109,7 +115,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
109 fun finishSetup(navController: NavController) { 115 fun finishSetup(navController: NavController) {
110 navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment) 116 navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment)
111 binding.navigationBar.setupWithNavController(navController) 117 binding.navigationBar.setupWithNavController(navController)
112 showNavigation(true) 118 showNavigation(visible = true, animated = true)
113 119
114 ThemeHelper.setNavigationBarColor( 120 ThemeHelper.setNavigationBarColor(
115 this, 121 this,
@@ -132,7 +138,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
132 } 138 }
133 } 139 }
134 140
135 private fun showNavigation(visible: Boolean) { 141 private fun showNavigation(visible: Boolean, animated: Boolean) {
142 if (!animated) {
143 if (visible) {
144 binding.navigationBar.visibility = View.VISIBLE
145 } else {
146 binding.navigationBar.visibility = View.INVISIBLE
147 }
148 return
149 }
150
136 binding.navigationBar.animate().apply { 151 binding.navigationBar.animate().apply {
137 if (visible) { 152 if (visible) {
138 binding.navigationBar.visibility = View.VISIBLE 153 binding.navigationBar.visibility = View.VISIBLE
@@ -196,10 +211,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
196 themeId = resId 211 themeId = resId
197 } 212 }
198 213
199 private fun hasExtension(path: String, extension: String): Boolean {
200 return path.substring(path.lastIndexOf(".") + 1).contains(extension)
201 }
202
203 val getGamesDirectory = 214 val getGamesDirectory =
204 registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> 215 registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
205 if (result == null) 216 if (result == null)
@@ -232,7 +243,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
232 if (result == null) 243 if (result == null)
233 return@registerForActivityResult 244 return@registerForActivityResult
234 245
235 if (!hasExtension(result.toString(), "keys")) { 246 if (!FileUtil.hasExtension(result.toString(), "keys")) {
236 Toast.makeText( 247 Toast.makeText(
237 applicationContext, 248 applicationContext,
238 R.string.invalid_keys_file, 249 R.string.invalid_keys_file,
@@ -278,7 +289,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
278 if (result == null) 289 if (result == null)
279 return@registerForActivityResult 290 return@registerForActivityResult
280 291
281 if (!hasExtension(result.toString(), "bin")) { 292 if (!FileUtil.hasExtension(result.toString(), "bin")) {
282 Toast.makeText( 293 Toast.makeText(
283 applicationContext, 294 applicationContext,
284 R.string.invalid_keys_file, 295 R.string.invalid_keys_file,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
index d16ed96ac..0e3305026 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
@@ -292,4 +292,8 @@ object FileUtil {
292 } 292 }
293 } 293 }
294 } 294 }
295
296 fun hasExtension(path: String, extension: String): Boolean {
297 return path.substring(path.lastIndexOf(".") + 1).contains(extension)
298 }
295} 299}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
index c463a66d8..9dd43343f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
@@ -3,6 +3,7 @@
3 3
4package org.yuzu.yuzu_emu.utils 4package org.yuzu.yuzu_emu.utils
5 5
6import android.content.SharedPreferences
6import android.net.Uri 7import android.net.Uri
7import androidx.preference.PreferenceManager 8import androidx.preference.PreferenceManager
8import org.yuzu.yuzu_emu.NativeLibrary 9import org.yuzu.yuzu_emu.NativeLibrary
@@ -14,12 +15,15 @@ import kotlin.collections.ArrayList
14object GameHelper { 15object GameHelper {
15 const val KEY_GAME_PATH = "game_path" 16 const val KEY_GAME_PATH = "game_path"
16 17
18 private lateinit var preferences: SharedPreferences
19
17 fun getGames(): ArrayList<Game> { 20 fun getGames(): ArrayList<Game> {
18 val games = ArrayList<Game>() 21 val games = ArrayList<Game>()
19 val context = YuzuApplication.appContext 22 val context = YuzuApplication.appContext
20 val gamesDir = 23 val gamesDir =
21 PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "") 24 PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "")
22 val gamesUri = Uri.parse(gamesDir) 25 val gamesUri = Uri.parse(gamesDir)
26 preferences = PreferenceManager.getDefaultSharedPreferences(context)
23 27
24 // Ensure keys are loaded so that ROM metadata can be decrypted. 28 // Ensure keys are loaded so that ROM metadata can be decrypted.
25 NativeLibrary.reloadKeys() 29 NativeLibrary.reloadKeys()
@@ -60,7 +64,7 @@ object GameHelper {
60 ) 64 )
61 } 65 }
62 66
63 return Game( 67 val newGame = Game(
64 name, 68 name,
65 NativeLibrary.getDescription(filePath).replace("\n", " "), 69 NativeLibrary.getDescription(filePath).replace("\n", " "),
66 NativeLibrary.getRegions(filePath), 70 NativeLibrary.getRegions(filePath),
@@ -68,5 +72,14 @@ object GameHelper {
68 gameId, 72 gameId,
69 NativeLibrary.getCompany(filePath) 73 NativeLibrary.getCompany(filePath)
70 ) 74 )
75
76 val addedTime = preferences.getLong(newGame.keyAddedToLibraryTime, 0L)
77 if (addedTime == 0L) {
78 preferences.edit()
79 .putLong(newGame.keyAddedToLibraryTime, System.currentTimeMillis())
80 .apply()
81 }
82
83 return newGame
71 } 84 }
72} 85}
diff --git a/src/android/app/src/main/res/drawable/ic_clear.xml b/src/android/app/src/main/res/drawable/ic_clear.xml
new file mode 100644
index 000000000..b6edb1d32
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_clear.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 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" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_search.xml b/src/android/app/src/main/res/drawable/ic_search.xml
new file mode 100644
index 000000000..bb0726851
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_search.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportWidth="24"
5 android:viewportHeight="24">
6 <path
7 android:fillColor="?attr/colorControlNormal"
8 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" />
9</vector>
diff --git a/src/android/app/src/main/res/layout/activity_main.xml b/src/android/app/src/main/res/layout/activity_main.xml
index 59812ab8e..6ca426b54 100644
--- a/src/android/app/src/main/res/layout/activity_main.xml
+++ b/src/android/app/src/main/res/layout/activity_main.xml
@@ -29,6 +29,7 @@
29 app:layout_constraintLeft_toLeftOf="parent" 29 app:layout_constraintLeft_toLeftOf="parent"
30 app:layout_constraintRight_toRightOf="parent" 30 app:layout_constraintRight_toRightOf="parent"
31 app:menu="@menu/menu_navigation" 31 app:menu="@menu/menu_navigation"
32 app:labelVisibilityMode="selected"
32 tools:visibility="visible" /> 33 tools:visibility="visible" />
33 34
34 <View 35 <View
diff --git a/src/android/app/src/main/res/layout/fragment_games.xml b/src/android/app/src/main/res/layout/fragment_games.xml
index c4c3eacf4..8b6d0b3b6 100644
--- a/src/android/app/src/main/res/layout/fragment_games.xml
+++ b/src/android/app/src/main/res/layout/fragment_games.xml
@@ -1,74 +1,34 @@
1<?xml version="1.0" encoding="utf-8"?> 1<?xml version="1.0" encoding="utf-8"?>
2<androidx.coordinatorlayout.widget.CoordinatorLayout 2<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
3 xmlns:android="http://schemas.android.com/apk/res/android" 3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools" 4 xmlns:tools="http://schemas.android.com/tools"
6 android:id="@+id/coordinator_main" 5 android:id="@+id/swipe_refresh"
7 android:layout_width="match_parent" 6 android:layout_width="match_parent"
8 android:layout_height="match_parent" 7 android:layout_height="match_parent"
9 android:background="?attr/colorSurface"> 8 android:background="?attr/colorSurface"
9 android:clipToPadding="false">
10 10
11 <androidx.swiperefreshlayout.widget.SwipeRefreshLayout 11 <RelativeLayout
12 android:id="@+id/swipe_refresh"
13 android:layout_width="match_parent" 12 android:layout_width="match_parent"
14 android:layout_height="match_parent" 13 android:layout_height="match_parent">
15 android:clipToPadding="false"
16 app:layout_behavior="@string/searchbar_scrolling_view_behavior">
17 14
18 <RelativeLayout 15 <com.google.android.material.textview.MaterialTextView
16 android:id="@+id/notice_text"
17 style="@style/TextAppearance.Material3.BodyLarge"
19 android:layout_width="match_parent" 18 android:layout_width="match_parent"
20 android:layout_height="match_parent"> 19 android:layout_height="match_parent"
21 20 android:gravity="center"
22 <com.google.android.material.textview.MaterialTextView 21 android:padding="@dimen/spacing_large"
23 android:id="@+id/notice_text" 22 android:text="@string/empty_gamelist"
24 style="@style/TextAppearance.Material3.BodyLarge" 23 tools:visibility="gone" />
25 android:layout_width="match_parent"
26 android:layout_height="match_parent"
27 android:gravity="center"
28 android:padding="@dimen/spacing_large"
29 android:text="@string/empty_gamelist"
30 tools:visibility="gone" />
31
32 <androidx.recyclerview.widget.RecyclerView
33 android:id="@+id/grid_games"
34 android:layout_width="match_parent"
35 android:layout_height="match_parent"
36 android:clipToPadding="false"
37 tools:listitem="@layout/card_game" />
38
39 </RelativeLayout>
40
41 </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
42
43 <com.google.android.material.appbar.AppBarLayout
44 android:id="@+id/app_bar_search"
45 android:layout_width="match_parent"
46 android:layout_height="wrap_content"
47 android:fitsSystemWindows="true"
48 app:liftOnScrollTargetViewId="@id/grid_games">
49
50 <com.google.android.material.search.SearchBar
51 android:id="@+id/search_bar"
52 android:layout_width="match_parent"
53 android:layout_height="wrap_content"
54 android:hint="@string/home_search_games" />
55
56 </com.google.android.material.appbar.AppBarLayout>
57
58 <com.google.android.material.search.SearchView
59 android:id="@+id/search_view"
60 android:layout_width="match_parent"
61 android:layout_height="match_parent"
62 android:hint="@string/home_search_games"
63 app:layout_anchor="@id/search_bar">
64 24
65 <androidx.recyclerview.widget.RecyclerView 25 <androidx.recyclerview.widget.RecyclerView
66 android:id="@+id/grid_search" 26 android:id="@+id/grid_games"
67 android:layout_width="match_parent" 27 android:layout_width="match_parent"
68 android:layout_height="match_parent" 28 android:layout_height="match_parent"
69 android:clipToPadding="false" 29 android:clipToPadding="false"
70 tools:listitem="@layout/card_game" /> 30 tools:listitem="@layout/card_game" />
71 31
72 </com.google.android.material.search.SearchView> 32 </RelativeLayout>
73 33
74</androidx.coordinatorlayout.widget.CoordinatorLayout> 34</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_search.xml b/src/android/app/src/main/res/layout/fragment_search.xml
new file mode 100644
index 000000000..3b1aefdfb
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_search.xml
@@ -0,0 +1,180 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent"
8 android:background="?attr/colorSurface">
9
10 <RelativeLayout
11 android:layout_width="0dp"
12 android:layout_height="0dp"
13 app:layout_constraintBottom_toBottomOf="parent"
14 app:layout_constraintEnd_toEndOf="parent"
15 app:layout_constraintStart_toStartOf="parent"
16 app:layout_constraintTop_toBottomOf="@+id/divider">
17
18 <LinearLayout
19 android:id="@+id/no_results_view"
20 android:layout_width="match_parent"
21 android:layout_height="match_parent"
22 android:orientation="vertical"
23 android:gravity="center">
24
25 <ImageView
26 android:id="@+id/icon_no_results"
27 android:layout_width="match_parent"
28 android:layout_height="80dp"
29 android:src="@drawable/ic_search" />
30
31 <com.google.android.material.textview.MaterialTextView
32 android:id="@+id/notice_text"
33 style="@style/TextAppearance.Material3.TitleLarge"
34 android:layout_width="match_parent"
35 android:layout_height="wrap_content"
36 android:gravity="center"
37 android:paddingTop="8dp"
38 android:text="@string/search_and_filter_games"
39 tools:visibility="visible" />
40
41 </LinearLayout>
42
43 <androidx.recyclerview.widget.RecyclerView
44 android:id="@+id/grid_games_search"
45 android:layout_width="match_parent"
46 android:layout_height="match_parent"
47 android:clipToPadding="false" />
48
49 </RelativeLayout>
50
51 <FrameLayout
52 android:id="@+id/frame_search"
53 android:layout_width="match_parent"
54 android:layout_height="wrap_content"
55 android:layout_margin="20dp"
56 app:layout_constraintEnd_toEndOf="parent"
57 app:layout_constraintStart_toStartOf="parent"
58 app:layout_constraintTop_toTopOf="parent">
59
60 <com.google.android.material.card.MaterialCardView
61 android:id="@+id/search_background"
62 style="?attr/materialCardViewFilledStyle"
63 android:layout_width="match_parent"
64 android:layout_height="56dp"
65 app:cardCornerRadius="28dp">
66
67 <LinearLayout
68 android:id="@+id/search_container"
69 android:layout_width="match_parent"
70 android:layout_height="match_parent"
71 android:layout_marginStart="24dp"
72 android:layout_marginEnd="56dp"
73 android:orientation="horizontal">
74
75 <ImageView
76 android:layout_width="28dp"
77 android:layout_height="28dp"
78 android:layout_gravity="center_vertical"
79 android:layout_marginEnd="24dp"
80 android:src="@drawable/ic_search"
81 app:tint="?attr/colorOnSurfaceVariant" />
82
83 <EditText
84 android:id="@+id/search_text"
85 android:layout_width="match_parent"
86 android:layout_height="match_parent"
87 android:background="@android:color/transparent"
88 android:hint="@string/home_search_games"
89 android:inputType="text"
90 android:maxLines="1"
91 android:imeOptions="flagNoFullscreen" />
92
93 </LinearLayout>
94
95 <ImageView
96 android:id="@+id/clear_button"
97 android:layout_width="24dp"
98 android:layout_height="24dp"
99 android:layout_gravity="center_vertical|end"
100 android:layout_marginEnd="24dp"
101 android:background="?attr/selectableItemBackground"
102 android:src="@drawable/ic_clear"
103 android:visibility="invisible"
104 app:tint="?attr/colorOnSurfaceVariant"
105 tools:visibility="visible" />
106
107 </com.google.android.material.card.MaterialCardView>
108
109 </FrameLayout>
110
111 <HorizontalScrollView
112 android:id="@+id/horizontalScrollView"
113 android:layout_width="match_parent"
114 android:layout_height="wrap_content"
115 android:fadingEdge="horizontal"
116 android:scrollbars="none"
117 app:layout_constraintEnd_toEndOf="parent"
118 app:layout_constraintStart_toStartOf="parent"
119 app:layout_constraintTop_toBottomOf="@+id/frame_search">
120
121 <com.google.android.material.chip.ChipGroup
122 android:id="@+id/chip_group"
123 android:layout_width="wrap_content"
124 android:layout_height="wrap_content"
125 android:clipToPadding="false"
126 android:paddingVertical="4dp"
127 app:chipSpacingHorizontal="12dp"
128 app:singleLine="true"
129 app:singleSelection="true">
130
131 <com.google.android.material.chip.Chip
132 android:id="@+id/chip_recently_played"
133 style="@style/Widget.Material3.Chip.Suggestion.Elevated"
134 android:layout_width="wrap_content"
135 android:layout_height="wrap_content"
136 android:checked="false"
137 android:text="@string/search_recently_played"
138 app:chipCornerRadius="28dp" />
139
140 <com.google.android.material.chip.Chip
141 android:id="@+id/chip_recently_added"
142 style="@style/Widget.Material3.Chip.Suggestion.Elevated"
143 android:layout_width="wrap_content"
144 android:layout_height="wrap_content"
145 android:checked="false"
146 android:text="@string/search_recently_added"
147 app:chipCornerRadius="28dp" />
148
149 <com.google.android.material.chip.Chip
150 android:id="@+id/chip_retail"
151 style="@style/Widget.Material3.Chip.Suggestion.Elevated"
152 android:layout_width="wrap_content"
153 android:layout_height="wrap_content"
154 android:checked="false"
155 android:text="@string/search_retail"
156 app:chipCornerRadius="28dp" />
157
158 <com.google.android.material.chip.Chip
159 android:id="@+id/chip_homebrew"
160 style="@style/Widget.Material3.Chip.Suggestion.Elevated"
161 android:layout_width="wrap_content"
162 android:layout_height="wrap_content"
163 android:checked="false"
164 android:text="@string/search_homebrew"
165 app:chipCornerRadius="28dp" />
166
167 </com.google.android.material.chip.ChipGroup>
168
169 </HorizontalScrollView>
170
171 <com.google.android.material.divider.MaterialDivider
172 android:id="@+id/divider"
173 android:layout_width="match_parent"
174 android:layout_height="wrap_content"
175 android:layout_marginHorizontal="20dp"
176 app:layout_constraintEnd_toEndOf="parent"
177 app:layout_constraintStart_toStartOf="parent"
178 app:layout_constraintTop_toBottomOf="@+id/horizontalScrollView" />
179
180</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/menu/menu_navigation.xml b/src/android/app/src/main/res/menu/menu_navigation.xml
index e46133604..ed10e6e51 100644
--- a/src/android/app/src/main/res/menu/menu_navigation.xml
+++ b/src/android/app/src/main/res/menu/menu_navigation.xml
@@ -7,6 +7,11 @@
7 android:title="@string/home_games" /> 7 android:title="@string/home_games" />
8 8
9 <item 9 <item
10 android:id="@+id/searchFragment"
11 android:icon="@drawable/ic_search"
12 android:title="@string/home_search" />
13
14 <item
10 android:id="@+id/homeSettingsFragment" 15 android:id="@+id/homeSettingsFragment"
11 android:icon="@drawable/ic_settings" 16 android:icon="@drawable/ic_settings"
12 android:title="@string/home_settings" /> 17 android:title="@string/home_settings" />
diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml
index d500d165b..0f43ba556 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -25,4 +25,9 @@
25 app:popUpToInclusive="true" /> 25 app:popUpToInclusive="true" />
26 </fragment> 26 </fragment>
27 27
28 <fragment
29 android:id="@+id/searchFragment"
30 android:name="org.yuzu.yuzu_emu.fragments.SearchFragment"
31 android:label="SearchFragment" />
32
28</navigation> 33</navigation>
diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml
index ab2583938..28a6d25cf 100644
--- a/src/android/app/src/main/res/values/dimens.xml
+++ b/src/android/app/src/main/res/values/dimens.xml
@@ -5,11 +5,10 @@
5 <dimen name="spacing_large">16dp</dimen> 5 <dimen name="spacing_large">16dp</dimen>
6 <dimen name="spacing_xtralarge">32dp</dimen> 6 <dimen name="spacing_xtralarge">32dp</dimen>
7 <dimen name="spacing_list">64dp</dimen> 7 <dimen name="spacing_list">64dp</dimen>
8 <dimen name="spacing_chip">20dp</dimen>
8 <dimen name="spacing_navigation">80dp</dimen> 9 <dimen name="spacing_navigation">80dp</dimen>
9 <dimen name="spacing_search">88dp</dimen> 10 <dimen name="spacing_search">128dp</dimen>
10 <dimen name="spacing_refresh_slingshot">80dp</dimen> 11 <dimen name="spacing_refresh_end">72dp</dimen>
11 <dimen name="spacing_refresh_start">32dp</dimen>
12 <dimen name="spacing_refresh_end">96dp</dimen>
13 <dimen name="menu_width">256dp</dimen> 12 <dimen name="menu_width">256dp</dimen>
14 <dimen name="card_width">165dp</dimen> 13 <dimen name="card_width">165dp</dimen>
15 14
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index c55b9e06b..9c7ab3c26 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -32,7 +32,10 @@
32 32
33 <!-- Home strings --> 33 <!-- Home strings -->
34 <string name="home_games">Games</string> 34 <string name="home_games">Games</string>
35 <string name="home_search">Search</string>
35 <string name="home_settings">Settings</string> 36 <string name="home_settings">Settings</string>
37 <string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
38 <string name="search_and_filter_games">Search and filter games</string>
36 <string name="select_games_folder">Select games folder</string> 39 <string name="select_games_folder">Select games folder</string>
37 <string name="select_games_folder_description">Allows yuzu to populate the games list</string> 40 <string name="select_games_folder_description">Allows yuzu to populate the games list</string>
38 <string name="add_games_warning">Skip selecting games folder?</string> 41 <string name="add_games_warning">Skip selecting games folder?</string>
@@ -58,6 +61,10 @@
58 <string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string> 61 <string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
59 <string name="advanced_settings">Advanced settings</string> 62 <string name="advanced_settings">Advanced settings</string>
60 <string name="settings_description">Configure emulator settings</string> 63 <string name="settings_description">Configure emulator settings</string>
64 <string name="search_recently_played">Recently Played</string>
65 <string name="search_recently_added">Recently Added</string>
66 <string name="search_retail">Retail</string>
67 <string name="search_homebrew">Homebrew</string>
61 <string name="open_user_folder">Open yuzu folder</string> 68 <string name="open_user_folder">Open yuzu folder</string>
62 <string name="open_user_folder_description">Manage yuzu\'s internal files</string> 69 <string name="open_user_folder_description">Manage yuzu\'s internal files</string>
63 <string name="no_file_manager">No file manager found</string> 70 <string name="no_file_manager">No file manager found</string>
@@ -151,8 +158,6 @@
151 158
152 <string name="load_settings">Loading Settings…</string> 159 <string name="load_settings">Loading Settings…</string>
153 160
154 <string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
155
156 <!-- Software keyboard --> 161 <!-- Software keyboard -->
157 <string name="software_keyboard">Software Keyboard</string> 162 <string name="software_keyboard">Software Keyboard</string>
158 163