diff options
| author | 2023-04-29 18:35:28 -0400 | |
|---|---|---|
| committer | 2023-06-03 00:05:57 -0700 | |
| commit | 6df030998a254bdf2a713d7b326bc3dd7f69acae (patch) | |
| tree | 36c3573678c6fb3cc8d3ec6f512e5a4bd19013ee /src/android | |
| parent | android: Fix potential zip traversal exploit (diff) | |
| download | yuzu-6df030998a254bdf2a713d7b326bc3dd7f69acae.tar.gz yuzu-6df030998a254bdf2a713d7b326bc3dd7f69acae.tar.xz yuzu-6df030998a254bdf2a713d7b326bc3dd7f69acae.zip | |
android: Search Fragment
Diffstat (limited to 'src/android')
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 | |||
| 13 | import android.widget.ImageView | 13 | import android.widget.ImageView |
| 14 | import androidx.appcompat.app.AppCompatActivity | 14 | import androidx.appcompat.app.AppCompatActivity |
| 15 | import androidx.lifecycle.lifecycleScope | 15 | import androidx.lifecycle.lifecycleScope |
| 16 | import androidx.preference.PreferenceManager | ||
| 16 | import androidx.recyclerview.widget.AsyncDifferConfig | 17 | import androidx.recyclerview.widget.AsyncDifferConfig |
| 17 | import androidx.recyclerview.widget.DiffUtil | 18 | import androidx.recyclerview.widget.DiffUtil |
| 18 | import androidx.recyclerview.widget.ListAdapter | 19 | import androidx.recyclerview.widget.ListAdapter |
| @@ -21,6 +22,7 @@ import coil.load | |||
| 21 | import kotlinx.coroutines.launch | 22 | import kotlinx.coroutines.launch |
| 22 | import org.yuzu.yuzu_emu.NativeLibrary | 23 | import org.yuzu.yuzu_emu.NativeLibrary |
| 23 | import org.yuzu.yuzu_emu.R | 24 | import org.yuzu.yuzu_emu.R |
| 25 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 24 | import org.yuzu.yuzu_emu.databinding.CardGameBinding | 26 | import org.yuzu.yuzu_emu.databinding.CardGameBinding |
| 25 | import org.yuzu.yuzu_emu.activities.EmulationActivity | 27 | import org.yuzu.yuzu_emu.activities.EmulationActivity |
| 26 | import org.yuzu.yuzu_emu.model.Game | 28 | import 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 | |||
| 21 | import androidx.core.view.ViewCompat | 21 | import androidx.core.view.ViewCompat |
| 22 | import androidx.core.view.WindowInsetsCompat | 22 | import androidx.core.view.WindowInsetsCompat |
| 23 | import androidx.fragment.app.Fragment | 23 | import androidx.fragment.app.Fragment |
| 24 | import androidx.fragment.app.activityViewModels | ||
| 24 | import androidx.recyclerview.widget.LinearLayoutManager | 25 | import androidx.recyclerview.widget.LinearLayoutManager |
| 25 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | 26 | import com.google.android.material.dialog.MaterialAlertDialogBuilder |
| 26 | import org.yuzu.yuzu_emu.R | 27 | import org.yuzu.yuzu_emu.R |
| @@ -30,6 +31,7 @@ import org.yuzu.yuzu_emu.features.DocumentProvider | |||
| 30 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity | 31 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity |
| 31 | import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile | 32 | import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile |
| 32 | import org.yuzu.yuzu_emu.model.HomeSetting | 33 | import org.yuzu.yuzu_emu.model.HomeSetting |
| 34 | import org.yuzu.yuzu_emu.model.HomeViewModel | ||
| 33 | import org.yuzu.yuzu_emu.ui.main.MainActivity | 35 | import org.yuzu.yuzu_emu.ui.main.MainActivity |
| 34 | import org.yuzu.yuzu_emu.utils.GpuDriverHelper | 36 | import 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 | |||
| 4 | package org.yuzu.yuzu_emu.fragments | ||
| 5 | |||
| 6 | import android.content.Context | ||
| 7 | import android.content.SharedPreferences | ||
| 8 | import android.os.Bundle | ||
| 9 | import android.view.LayoutInflater | ||
| 10 | import android.view.View | ||
| 11 | import android.view.ViewGroup | ||
| 12 | import android.view.inputmethod.InputMethodManager | ||
| 13 | import androidx.appcompat.app.AppCompatActivity | ||
| 14 | import androidx.core.view.ViewCompat | ||
| 15 | import androidx.core.view.WindowInsetsCompat | ||
| 16 | import androidx.core.view.updatePadding | ||
| 17 | import androidx.core.widget.doOnTextChanged | ||
| 18 | import androidx.fragment.app.Fragment | ||
| 19 | import androidx.fragment.app.activityViewModels | ||
| 20 | import androidx.preference.PreferenceManager | ||
| 21 | import info.debatty.java.stringsimilarity.Jaccard | ||
| 22 | import org.yuzu.yuzu_emu.R | ||
| 23 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 24 | import org.yuzu.yuzu_emu.adapters.GameAdapter | ||
| 25 | import org.yuzu.yuzu_emu.databinding.FragmentSearchBinding | ||
| 26 | import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager | ||
| 27 | import org.yuzu.yuzu_emu.model.Game | ||
| 28 | import org.yuzu.yuzu_emu.model.GamesViewModel | ||
| 29 | import org.yuzu.yuzu_emu.model.HomeViewModel | ||
| 30 | import org.yuzu.yuzu_emu.utils.FileUtil | ||
| 31 | import org.yuzu.yuzu_emu.utils.Log | ||
| 32 | import java.util.Locale | ||
| 33 | |||
| 34 | class 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 | |||
| 5 | import androidx.lifecycle.ViewModel | 5 | import androidx.lifecycle.ViewModel |
| 6 | 6 | ||
| 7 | class HomeViewModel : ViewModel() { | 7 | class 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 | |||
| 7 | import android.os.Bundle | 7 | import android.os.Bundle |
| 8 | import android.view.View | 8 | import android.view.View |
| 9 | import android.view.ViewGroup.MarginLayoutParams | 9 | import android.view.ViewGroup.MarginLayoutParams |
| 10 | import android.view.WindowManager | ||
| 10 | import android.view.animation.PathInterpolator | 11 | import android.view.animation.PathInterpolator |
| 11 | import android.widget.Toast | 12 | import android.widget.Toast |
| 12 | import androidx.activity.result.contract.ActivityResultContracts | 13 | import 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 | ||
| 4 | package org.yuzu.yuzu_emu.utils | 4 | package org.yuzu.yuzu_emu.utils |
| 5 | 5 | ||
| 6 | import android.content.SharedPreferences | ||
| 6 | import android.net.Uri | 7 | import android.net.Uri |
| 7 | import androidx.preference.PreferenceManager | 8 | import androidx.preference.PreferenceManager |
| 8 | import org.yuzu.yuzu_emu.NativeLibrary | 9 | import org.yuzu.yuzu_emu.NativeLibrary |
| @@ -14,12 +15,15 @@ import kotlin.collections.ArrayList | |||
| 14 | object GameHelper { | 15 | object 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 | ||