From 6df030998a254bdf2a713d7b326bc3dd7f69acae Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Sat, 29 Apr 2023 18:35:28 -0400 Subject: [PATCH] android: Search Fragment --- .../org/yuzu/yuzu_emu/adapters/GameAdapter.kt | 10 + .../fragments/HomeSettingsFragment.kt | 5 + .../yuzu/yuzu_emu/fragments/SearchFragment.kt | 222 ++++++++++++++++++ .../yuzu/yuzu_emu/fragments/SetupFragment.kt | 2 +- .../main/java/org/yuzu/yuzu_emu/model/Game.kt | 3 + .../org/yuzu/yuzu_emu/model/GamesViewModel.kt | 7 + .../org/yuzu/yuzu_emu/model/HomeViewModel.kt | 14 +- .../org/yuzu/yuzu_emu/ui/GamesFragment.kt | 116 +-------- .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 43 ++-- .../java/org/yuzu/yuzu_emu/utils/FileUtil.kt | 4 + .../org/yuzu/yuzu_emu/utils/GameHelper.kt | 15 +- .../app/src/main/res/drawable/ic_clear.xml | 9 + .../app/src/main/res/drawable/ic_search.xml | 9 + .../app/src/main/res/layout/activity_main.xml | 1 + .../src/main/res/layout/fragment_games.xml | 74 ++---- .../src/main/res/layout/fragment_search.xml | 180 ++++++++++++++ .../app/src/main/res/menu/menu_navigation.xml | 5 + .../main/res/navigation/home_navigation.xml | 5 + .../app/src/main/res/values/dimens.xml | 7 +- .../app/src/main/res/values/strings.xml | 9 +- 20 files changed, 551 insertions(+), 189 deletions(-) create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt create mode 100644 src/android/app/src/main/res/drawable/ic_clear.xml create mode 100644 src/android/app/src/main/res/drawable/ic_search.xml create mode 100644 src/android/app/src/main/res/layout/fragment_search.xml 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 import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope +import androidx.preference.PreferenceManager import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter @@ -21,6 +22,7 @@ import coil.load import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.databinding.CardGameBinding import org.yuzu.yuzu_emu.activities.EmulationActivity import org.yuzu.yuzu_emu.model.Game @@ -51,6 +53,14 @@ class GameAdapter(private val activity: AppCompatActivity) : */ override fun onClick(view: View) { val holder = view.tag as GameViewHolder + val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + preferences.edit() + .putLong( + holder.game.keyLastPlayedTime, + System.currentTimeMillis() + ) + .apply() + EmulationActivity.launch(activity, holder.game) } 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 import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.yuzu.yuzu_emu.R @@ -30,6 +31,7 @@ import org.yuzu.yuzu_emu.features.DocumentProvider import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.model.HomeSetting +import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.ui.main.MainActivity import org.yuzu.yuzu_emu.utils.GpuDriverHelper @@ -39,6 +41,8 @@ class HomeSettingsFragment : Fragment() { private lateinit var mainActivity: MainActivity + private val homeViewModel: HomeViewModel by activityViewModels() + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -49,6 +53,7 @@ class HomeSettingsFragment : Fragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + homeViewModel.setNavigationVisibility(visible = true, animated = false) mainActivity = requireActivity() as MainActivity val optionsList: List = 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 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.content.Context +import android.content.SharedPreferences +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.preference.PreferenceManager +import info.debatty.java.stringsimilarity.Jaccard +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.adapters.GameAdapter +import org.yuzu.yuzu_emu.databinding.FragmentSearchBinding +import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager +import org.yuzu.yuzu_emu.model.Game +import org.yuzu.yuzu_emu.model.GamesViewModel +import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.utils.FileUtil +import org.yuzu.yuzu_emu.utils.Log +import java.util.Locale + +class SearchFragment : Fragment() { + private var _binding: FragmentSearchBinding? = null + private val binding get() = _binding!! + + private val gamesViewModel: GamesViewModel by activityViewModels() + private val homeViewModel: HomeViewModel by activityViewModels() + + private lateinit var preferences: SharedPreferences + + companion object { + private const val SEARCH_TEXT = "SearchText" + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentSearchBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + homeViewModel.setNavigationVisibility(visible = true, animated = false) + preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + + if (savedInstanceState != null) { + binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT)) + } + + gamesViewModel.searchFocused.observe(viewLifecycleOwner) { searchFocused -> + if (searchFocused) { + focusSearch() + gamesViewModel.setSearchFocused(false) + } + } + + binding.gridGamesSearch.apply { + layoutManager = AutofitGridLayoutManager( + requireContext(), + requireContext().resources.getDimensionPixelSize(R.dimen.card_width) + ) + adapter = GameAdapter(requireActivity() as AppCompatActivity) + } + + binding.chipGroup.setOnCheckedStateChangeListener { _, _ -> filterAndSearch() } + + binding.searchText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int -> + if (text.toString().isNotEmpty()) { + binding.clearButton.visibility = View.VISIBLE + } else { + binding.clearButton.visibility = View.INVISIBLE + } + filterAndSearch() + } + + gamesViewModel.games.observe(viewLifecycleOwner) { filterAndSearch() } + gamesViewModel.searchedGames.observe(viewLifecycleOwner) { + (binding.gridGamesSearch.adapter as GameAdapter).submitList(it) + if (it.isEmpty()) { + binding.noResultsView.visibility = View.VISIBLE + } else { + binding.noResultsView.visibility = View.GONE + } + } + + binding.clearButton.setOnClickListener { binding.searchText.setText("") } + + binding.searchBackground.setOnClickListener { focusSearch() } + + setInsets() + filterAndSearch() + } + + private inner class ScoredGame(val score: Double, val item: Game) + + private fun filterAndSearch() { + val baseList = gamesViewModel.games.value!! + val filteredList: List = when (binding.chipGroup.checkedChipId) { + R.id.chip_recently_played -> { + baseList.filter { + val lastPlayedTime = preferences.getLong(it.keyLastPlayedTime, 0L) + lastPlayedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000) + } + } + + R.id.chip_recently_added -> { + baseList.filter { + val addedTime = preferences.getLong(it.keyAddedToLibraryTime, 0L) + addedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000) + } + } + + R.id.chip_homebrew -> { + baseList.filter { + Log.error("Guh - ${it.path}") + FileUtil.hasExtension(it.path, "nro") + || FileUtil.hasExtension(it.path, "nso") + } + } + + R.id.chip_retail -> baseList.filter { + FileUtil.hasExtension(it.path, "xci") + || FileUtil.hasExtension(it.path, "nsp") + } + + else -> baseList + } + + if (binding.searchText.text.toString().isEmpty() + && binding.chipGroup.checkedChipId != View.NO_ID) { + gamesViewModel.setSearchedGames(filteredList) + return + } + + val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault()) + val searchAlgorithm = Jaccard(2) + val sortedList: List = filteredList.mapNotNull { game -> + val title = game.title.lowercase(Locale.getDefault()) + val score = searchAlgorithm.similarity(searchTerm, title) + if (score > 0.03) { + ScoredGame(score, game) + } else { + null + } + }.sortedByDescending { it.score }.map { it.item } + gamesViewModel.setSearchedGames(sortedList) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + if (_binding != null) { + outState.putString(SEARCH_TEXT, binding.searchText.text.toString()) + } + } + + private fun focusSearch() { + if (_binding != null) { + binding.searchText.requestFocus() + val imm = + requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager? + imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT) + } + } + + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _: View, windowInsets: WindowInsetsCompat -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med) + val navigationSpacing = resources.getDimensionPixelSize(R.dimen.spacing_navigation) + val chipSpacing = resources.getDimensionPixelSize(R.dimen.spacing_chip) + + binding.frameSearch.updatePadding( + left = insets.left, + top = insets.top, + right = insets.right + ) + + binding.gridGamesSearch.setPadding( + insets.left, + extraListSpacing, + insets.right, + insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation) + extraListSpacing + ) + + binding.noResultsView.updatePadding( + left = insets.left, + right = insets.right, + bottom = insets.bottom + navigationSpacing + ) + + val mlpDivider = binding.divider.layoutParams as ViewGroup.MarginLayoutParams + mlpDivider.leftMargin = insets.left + chipSpacing + mlpDivider.rightMargin = insets.right + chipSpacing + binding.divider.layoutParams = mlpDivider + + binding.chipGroup.updatePadding( + left = insets.left + chipSpacing, + right = insets.right + chipSpacing + ) + + windowInsets + } +} 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() { mainActivity = requireActivity() as MainActivity - homeViewModel.setNavigationVisibility(false) + homeViewModel.setNavigationVisibility(visible = false, animated = false) requireActivity().onBackPressedDispatcher.addCallback( 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( val gameId: String, val company: String ) : Parcelable { + val keyAddedToLibraryTime get() = "${gameId}_AddedToLibraryTime" + val keyLastPlayedTime get() = "${gameId}_LastPlayed" + companion object { val extensions: Set = HashSet( 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() { private val _shouldScrollToTop = MutableLiveData(false) val shouldScrollToTop: LiveData get() = _shouldScrollToTop + private val _searchFocused = MutableLiveData(false) + val searchFocused: LiveData get() = _searchFocused + init { reloadGames(false) } @@ -45,6 +48,10 @@ class GamesViewModel : ViewModel() { _shouldScrollToTop.postValue(shouldScroll) } + fun setSearchFocused(searchFocused: Boolean) { + _searchFocused.postValue(searchFocused) + } + fun reloadGames(directoryChanged: Boolean) { if (isReloading.value == true) return 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 import androidx.lifecycle.ViewModel class HomeViewModel : ViewModel() { - private val _navigationVisible = MutableLiveData(true) - val navigationVisible: LiveData get() = _navigationVisible + private val _navigationVisible = MutableLiveData>() + val navigationVisible: LiveData> get() = _navigationVisible private val _statusBarShadeVisible = MutableLiveData(true) val statusBarShadeVisible: LiveData get() = _statusBarShadeVisible var navigatedToSetup = false - fun setNavigationVisibility(visible: Boolean) { - if (_navigationVisible.value == visible) { + init { + _navigationVisible.value = Pair(false, false) + } + + fun setNavigationVisibility(visible: Boolean, animated: Boolean) { + if (_navigationVisible.value?.first == visible) { return } - _navigationVisible.value = visible + _navigationVisible.value = Pair(visible, animated) } fun setStatusBarShadeVisibility(visible: Boolean) { 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() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - // Use custom back navigation so the user doesn't back out of the app when trying to back - // out of the search view - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - if (binding.searchView.currentTransitionState == TransitionState.SHOWN) { - binding.searchView.hide() - } else { - requireActivity().finish() - } - } - }) + homeViewModel.setNavigationVisibility(visible = true, animated = false) binding.gridGames.apply { layoutManager = AutofitGridLayoutManager( @@ -73,7 +61,6 @@ class GamesFragment : Fragment() { ) adapter = GameAdapter(requireActivity() as AppCompatActivity) } - setUpSearch() // Add swipe down to refresh gesture binding.swipeRefresh.setOnRefreshListener { @@ -91,21 +78,16 @@ class GamesFragment : Fragment() { // Watch for when we get updates to any of our games lists gamesViewModel.isReloading.observe(viewLifecycleOwner) { isReloading -> binding.swipeRefresh.isRefreshing = isReloading - - if (!isReloading) { - if (gamesViewModel.games.value!!.isEmpty()) { - binding.noticeText.visibility = View.VISIBLE - } else { - binding.noticeText.visibility = View.GONE - } - } } gamesViewModel.games.observe(viewLifecycleOwner) { (binding.gridGames.adapter as GameAdapter).submitList(it) + if (it.isEmpty()) { + binding.noticeText.visibility = View.VISIBLE + } else { + binding.noticeText.visibility = View.GONE + } } - gamesViewModel.searchedGames.observe(viewLifecycleOwner) { - (binding.gridSearch.adapter as GameAdapter).submitList(it) - } + gamesViewModel.shouldSwapData.observe(viewLifecycleOwner) { shouldSwapData -> if (shouldSwapData) { (binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value) @@ -113,31 +95,6 @@ class GamesFragment : Fragment() { } } - // Hide bottom navigation and FAB when using the search view - binding.searchView.addTransitionListener { _: SearchView, _: TransitionState, newState: TransitionState -> - when (newState) { - TransitionState.SHOWING, - TransitionState.SHOWN -> { - (binding.gridSearch.adapter as GameAdapter).submitList(emptyList()) - searchShown() - } - TransitionState.HIDDEN, - TransitionState.HIDING -> { - gamesViewModel.setSearchedGames(emptyList()) - searchHidden() - binding.appBarSearch.setExpanded(true) - } - } - } - - // Ensure that bottom navigation or FAB don't appear upon recreation - val searchState = binding.searchView.currentTransitionState - if (searchState == TransitionState.SHOWN) { - searchShown() - } else if (searchState == TransitionState.HIDDEN) { - searchHidden() - } - // Check if the user reselected the games menu item and then scroll to top of the list gamesViewModel.shouldScrollToTop.observe(viewLifecycleOwner) { shouldScroll -> if (shouldScroll) { @@ -162,71 +119,24 @@ class GamesFragment : Fragment() { _binding = null } - private fun searchShown() { - homeViewModel.setNavigationVisibility(false) - homeViewModel.setStatusBarShadeVisibility(false) - } - - private fun searchHidden() { - homeViewModel.setNavigationVisibility(true) - homeViewModel.setStatusBarShadeVisibility(true) - } - - private inner class ScoredGame(val score: Double, val item: Game) - - private fun setUpSearch() { - binding.gridSearch.apply { - layoutManager = AutofitGridLayoutManager( - requireContext(), - requireContext().resources.getDimensionPixelSize(R.dimen.card_width) - ) - adapter = GameAdapter(requireActivity() as AppCompatActivity) - } - - binding.searchView.editText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int -> - val searchTerm = text.toString().lowercase(Locale.getDefault()) - val searchAlgorithm = Jaccard(2) - val sortedList: List = gamesViewModel.games.value!!.mapNotNull { game -> - val title = game.title.lowercase(Locale.getDefault()) - val score = searchAlgorithm.similarity(searchTerm, title) - if (score > 0.03) { - ScoredGame(score, game) - } else { - null - } - }.sortedByDescending { it.score }.map { it.item } - gamesViewModel.setSearchedGames(sortedList) - } - } - - fun scrollToTop() { + private fun scrollToTop() { if (_binding != null) { binding.gridGames.smoothScrollToPosition(0) } } private fun setInsets() = - ViewCompat.setOnApplyWindowInsetsListener(binding.gridGames) { view: View, windowInsets: WindowInsetsCompat -> + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat -> val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) - val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med) + val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_large) - view.updatePadding( - top = insets.top + resources.getDimensionPixelSize(R.dimen.spacing_search), + binding.gridGames.updatePadding( + top = insets.top + extraListSpacing, bottom = insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation) + extraListSpacing ) - binding.gridSearch.updatePadding( - left = insets.left, - top = extraListSpacing, - right = insets.right, - bottom = insets.bottom + extraListSpacing - ) - binding.swipeRefresh.setSlingshotDistance( - resources.getDimensionPixelSize(R.dimen.spacing_refresh_slingshot) - ) - binding.swipeRefresh.setProgressViewOffset( + binding.swipeRefresh.setProgressViewEndTarget( false, - insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_start), insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end) ) 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 import android.os.Bundle import android.view.View import android.view.ViewGroup.MarginLayoutParams +import android.view.WindowManager import android.view.animation.PathInterpolator import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts @@ -60,6 +61,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { setContentView(binding.root) WindowCompat.setDecorFitsSystemWindows(window, false) + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) window.statusBarColor = ContextCompat.getColor(applicationContext, android.R.color.transparent) @@ -75,26 +77,30 @@ class MainActivity : AppCompatActivity(), ThemeProvider { supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment setUpNavigation(navHostFragment.navController) (binding.navigationBar as NavigationBarView).setOnItemReselectedListener { - if (it.itemId == R.id.gamesFragment) { - gamesViewModel.setShouldScrollToTop(true) + when (it.itemId) { + R.id.gamesFragment -> gamesViewModel.setShouldScrollToTop(true) + R.id.searchFragment -> gamesViewModel.setSearchFocused(true) } } binding.statusBarShade.setBackgroundColor( - MaterialColors.getColor( - binding.root, - R.attr.colorSurface + ThemeHelper.getColorWithOpacity( + MaterialColors.getColor( + binding.root, + R.attr.colorSurface + ), + ThemeHelper.SYSTEM_BAR_ALPHA ) ) // Prevents navigation from being drawn for a short time on recreation if set to hidden - if (homeViewModel.navigationVisible.value == false) { + if (!homeViewModel.navigationVisible.value?.first!!) { binding.navigationBar.visibility = View.INVISIBLE binding.statusBarShade.visibility = View.INVISIBLE } - homeViewModel.navigationVisible.observe(this) { visible -> - showNavigation(visible) + homeViewModel.navigationVisible.observe(this) { + showNavigation(it.first, it.second) } homeViewModel.statusBarShadeVisible.observe(this) { visible -> showStatusBarShade(visible) @@ -109,7 +115,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { fun finishSetup(navController: NavController) { navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment) binding.navigationBar.setupWithNavController(navController) - showNavigation(true) + showNavigation(visible = true, animated = true) ThemeHelper.setNavigationBarColor( this, @@ -132,7 +138,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } } - private fun showNavigation(visible: Boolean) { + private fun showNavigation(visible: Boolean, animated: Boolean) { + if (!animated) { + if (visible) { + binding.navigationBar.visibility = View.VISIBLE + } else { + binding.navigationBar.visibility = View.INVISIBLE + } + return + } + binding.navigationBar.animate().apply { if (visible) { binding.navigationBar.visibility = View.VISIBLE @@ -196,10 +211,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider { themeId = resId } - private fun hasExtension(path: String, extension: String): Boolean { - return path.substring(path.lastIndexOf(".") + 1).contains(extension) - } - val getGamesDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> if (result == null) @@ -232,7 +243,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { if (result == null) return@registerForActivityResult - if (!hasExtension(result.toString(), "keys")) { + if (!FileUtil.hasExtension(result.toString(), "keys")) { Toast.makeText( applicationContext, R.string.invalid_keys_file, @@ -278,7 +289,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { if (result == null) return@registerForActivityResult - if (!hasExtension(result.toString(), "bin")) { + if (!FileUtil.hasExtension(result.toString(), "bin")) { Toast.makeText( applicationContext, R.string.invalid_keys_file, 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 { } } } + + fun hasExtension(path: String, extension: String): Boolean { + return path.substring(path.lastIndexOf(".") + 1).contains(extension) + } } 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 @@ package org.yuzu.yuzu_emu.utils +import android.content.SharedPreferences import android.net.Uri import androidx.preference.PreferenceManager import org.yuzu.yuzu_emu.NativeLibrary @@ -14,12 +15,15 @@ import kotlin.collections.ArrayList object GameHelper { const val KEY_GAME_PATH = "game_path" + private lateinit var preferences: SharedPreferences + fun getGames(): ArrayList { val games = ArrayList() val context = YuzuApplication.appContext val gamesDir = PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "") val gamesUri = Uri.parse(gamesDir) + preferences = PreferenceManager.getDefaultSharedPreferences(context) // Ensure keys are loaded so that ROM metadata can be decrypted. NativeLibrary.reloadKeys() @@ -60,7 +64,7 @@ object GameHelper { ) } - return Game( + val newGame = Game( name, NativeLibrary.getDescription(filePath).replace("\n", " "), NativeLibrary.getRegions(filePath), @@ -68,5 +72,14 @@ object GameHelper { gameId, NativeLibrary.getCompany(filePath) ) + + val addedTime = preferences.getLong(newGame.keyAddedToLibraryTime, 0L) + if (addedTime == 0L) { + preferences.edit() + .putLong(newGame.keyAddedToLibraryTime, System.currentTimeMillis()) + .apply() + } + + return newGame } } 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 @@ + + + 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 @@ + + + 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 @@ app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:menu="@menu/menu_navigation" + app:labelVisibilityMode="selected" tools:visibility="visible" /> - + android:background="?attr/colorSurface" + android:clipToPadding="false"> - + android:layout_height="match_parent"> - - - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:gravity="center" + android:padding="@dimen/spacing_large" + android:text="@string/empty_gamelist" + tools:visibility="gone" /> - + - + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ -6,6 +6,11 @@ android:icon="@drawable/ic_controller" android:title="@string/home_games" /> + + + + 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 @@ 16dp 32dp 64dp + 20dp 80dp - 88dp - 80dp - 32dp - 96dp + 128dp + 72dp 256dp 165dp 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 @@ Games + Search Settings + No files were found or no game directory has been selected yet. + Search and filter games Select games folder Allows yuzu to populate the games list Skip selecting games folder? @@ -58,6 +61,10 @@ Install alternative drivers for potentially better performance or accuracy Advanced settings Configure emulator settings + Recently Played + Recently Added + Retail + Homebrew Open yuzu folder Manage yuzu\'s internal files No file manager found @@ -151,8 +158,6 @@ Loading Settingsā€¦ - No files were found or no game directory has been selected yet. - Software Keyboard