Refactor Android Game Screen UI

* Port SearchFragment functionality to GameFragment @kleidis

Co-authored-by: Briar <205427297+icybriarr@users.noreply.github.com>

* Remove the bottom navigation bar and SearchFragment remaining code @ishan09811

* Add 2 new game view types `Grid & `List` to GameAdapter @kleidis

* Fix padding on header

* Change app name to uppercase

---------

Co-authored-by: Kleidis <167202775+kleidis@users.noreply.github.com>
Co-authored-by: Briar <205427297+icybriarr@users.noreply.github.com>
Co-authored-by: Ishan09811 <156402647+Ishan09811@users.noreply.github.com>
This commit is contained in:
Briar 2025-04-09 00:01:32 +02:00 committed by GitHub
parent 08ac410558
commit 2dd8d09c7b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 719 additions and 694 deletions

View file

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.adapters package org.yuzu.yuzu_emu.adapters
@ -16,13 +16,15 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.viewbinding.ViewBinding
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.databinding.CardGameBinding import org.yuzu.yuzu_emu.databinding.CardGameListBinding
import org.yuzu.yuzu_emu.databinding.CardGameGridBinding
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.utils.GameIconUtils import org.yuzu.yuzu_emu.utils.GameIconUtils
@ -31,22 +33,70 @@ import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
class GameAdapter(private val activity: AppCompatActivity) : class GameAdapter(private val activity: AppCompatActivity) :
AbstractDiffAdapter<Game, GameAdapter.GameViewHolder>(exact = false) { AbstractDiffAdapter<Game, GameAdapter.GameViewHolder>(exact = false) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) companion object {
.also { return GameViewHolder(it) } const val VIEW_TYPE_GRID = 0
const val VIEW_TYPE_LIST = 1
} }
inner class GameViewHolder(val binding: CardGameBinding) : private var viewType = 0
AbstractViewHolder<Game>(binding) {
fun setViewType(type: Int) {
viewType = type
notifyDataSetChanged()
}
override fun getItemViewType(position: Int): Int = viewType
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
val binding = when (viewType) {
VIEW_TYPE_LIST -> CardGameListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
VIEW_TYPE_GRID -> CardGameGridBinding.inflate(LayoutInflater.from(parent.context), parent, false)
else -> throw IllegalArgumentException("Invalid view type")
}
return GameViewHolder(binding, viewType)
}
inner class GameViewHolder(
private val binding: ViewBinding,
private val viewType: Int
) : AbstractViewHolder<Game>(binding) {
override fun bind(model: Game) { override fun bind(model: Game) {
binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP when (viewType) {
GameIconUtils.loadGameIcon(model, binding.imageGameScreen) VIEW_TYPE_LIST -> bindListView(model)
VIEW_TYPE_GRID -> bindGridView(model)
}
}
binding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ") private fun bindListView(model: Game) {
val listBinding = binding as CardGameListBinding
binding.textGameTitle.marquee() listBinding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
binding.cardGame.setOnClickListener { onClick(model) } GameIconUtils.loadGameIcon(model, listBinding.imageGameScreen)
binding.cardGame.setOnLongClickListener { onLongClick(model) }
listBinding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ")
listBinding.textGameDeveloper.text = model.developer
listBinding.textGameTitle.marquee()
listBinding.cardGameList.setOnClickListener { onClick(model) }
listBinding.cardGameList.setOnLongClickListener { onLongClick(model) }
}
private fun bindGridView(model: Game) {
val gridBinding = binding as CardGameGridBinding
gridBinding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
GameIconUtils.loadGameIcon(model, gridBinding.imageGameScreen)
gridBinding.textGameTitle.text = model.title.replace("[\\t\\n\\r]+".toRegex(), " ")
gridBinding.textGameTitle.marquee()
gridBinding.cardGameGrid.setOnClickListener { onClick(model) }
gridBinding.cardGameGrid.setOnLongClickListener { onLongClick(model) }
} }
fun onClick(game: Game) { fun onClick(game: Game) {

View file

@ -1,5 +1,6 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.fragments
@ -50,7 +51,6 @@ class AboutFragment : Fragment() {
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
homeViewModel.setNavigationVisibility(visible = false, animated = true)
homeViewModel.setStatusBarShadeVisibility(visible = false) homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarAbout.setNavigationOnClickListener { binding.toolbarAbout.setNavigationOnClickListener {

View file

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.fragments
@ -33,7 +33,8 @@ class AddGameFolderDialogFragment : DialogFragment() {
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked) val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked)
homeViewModel.setGamesDirSelected(true) homeViewModel.setGamesDirSelected(true)
gamesViewModel.addFolder(newGameDir) val calledFromGameFragment = requireArguments().getBoolean("calledFromGameFragment", false)
gamesViewModel.addFolder(newGameDir, calledFromGameFragment)
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.setView(binding.root) .setView(binding.root)
@ -45,9 +46,10 @@ class AddGameFolderDialogFragment : DialogFragment() {
private const val FOLDER_URI_STRING = "FolderUriString" private const val FOLDER_URI_STRING = "FolderUriString"
fun newInstance(folderUriString: String): AddGameFolderDialogFragment { fun newInstance(folderUriString: String, calledFromGameFragment: Boolean): AddGameFolderDialogFragment {
val args = Bundle() val args = Bundle()
args.putString(FOLDER_URI_STRING, folderUriString) args.putString(FOLDER_URI_STRING, folderUriString)
args.putBoolean("calledFromGameFragment", calledFromGameFragment)
val fragment = AddGameFolderDialogFragment() val fragment = AddGameFolderDialogFragment()
fragment.arguments = args fragment.arguments = args
return fragment return fragment

View file

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.fragments
@ -59,7 +59,6 @@ class AddonsFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = false, animated = false)
homeViewModel.setStatusBarShadeVisibility(false) homeViewModel.setStatusBarShadeVisibility(false)
binding.toolbarAddons.setNavigationOnClickListener { binding.toolbarAddons.setNavigationOnClickListener {

View file

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.fragments
@ -47,7 +47,6 @@ class AppletLauncherFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = false, animated = true)
homeViewModel.setStatusBarShadeVisibility(visible = false) homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarApplets.setNavigationOnClickListener { binding.toolbarApplets.setNavigationOnClickListener {

View file

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.fragments
@ -62,7 +62,6 @@ class DriverManagerFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = false, animated = true)
homeViewModel.setStatusBarShadeVisibility(visible = false) homeViewModel.setStatusBarShadeVisibility(visible = false)
driverViewModel.onOpenDriverManager(args.game) driverViewModel.onOpenDriverManager(args.game)

View file

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.fragments
@ -53,7 +53,6 @@ class GameFoldersFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = false, animated = true)
homeViewModel.setStatusBarShadeVisibility(visible = false) homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarFolders.setNavigationOnClickListener { binding.toolbarFolders.setNavigationOnClickListener {

View file

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.fragments
@ -59,7 +59,6 @@ class GameInfoFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = false, animated = false)
homeViewModel.setStatusBarShadeVisibility(false) homeViewModel.setStatusBarShadeVisibility(false)
binding.apply { binding.apply {

View file

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.fragments
@ -76,7 +76,6 @@ class GamePropertiesFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = false, animated = true)
homeViewModel.setStatusBarShadeVisibility(true) homeViewModel.setStatusBarShadeVisibility(true)
binding.buttonBack.setOnClickListener { binding.buttonBack.setOnClickListener {

View file

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.fragments
@ -70,7 +70,6 @@ class HomeSettingsFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = true, animated = true)
homeViewModel.setStatusBarShadeVisibility(visible = true) homeViewModel.setStatusBarShadeVisibility(visible = true)
mainActivity = requireActivity() as MainActivity mainActivity = requireActivity() as MainActivity
@ -389,32 +388,20 @@ class HomeSettingsFragment : Fragment() {
} }
private fun setInsets() = private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener( ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
binding.root
) { view: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
val spacingNavigationRail =
resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right
binding.scrollViewSettings.updatePadding( binding.scrollViewSettings.updatePadding(
top = barInsets.top, top = barInsets.top,
bottom = barInsets.bottom
) )
binding.scrollViewSettings.updateMargins(left = leftInsets, right = rightInsets) binding.homeSettingsList.updatePadding(
left = barInsets.left + cutoutInsets.left,
binding.linearLayoutSettings.updatePadding(bottom = spacingNavigation) top = cutoutInsets.top,
right = barInsets.right + cutoutInsets.right,
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) { bottom = barInsets.bottom
binding.linearLayoutSettings.updatePadding(left = spacingNavigationRail) )
} else {
binding.linearLayoutSettings.updatePadding(right = spacingNavigationRail)
}
windowInsets windowInsets
} }

View file

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.fragments
@ -66,7 +66,6 @@ class InstallableFragment : Fragment() {
val mainActivity = requireActivity() as MainActivity val mainActivity = requireActivity() as MainActivity
homeViewModel.setNavigationVisibility(visible = false, animated = true)
homeViewModel.setStatusBarShadeVisibility(visible = false) homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarInstallables.setNavigationOnClickListener { binding.toolbarInstallables.setNavigationOnClickListener {

View file

@ -1,5 +1,6 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.fragments
@ -45,7 +46,6 @@ class LicensesFragment : Fragment() {
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
homeViewModel.setNavigationVisibility(visible = false, animated = true)
homeViewModel.setStatusBarShadeVisibility(visible = false) homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarLicenses.setNavigationOnClickListener { binding.toolbarLicenses.setNavigationOnClickListener {

View file

@ -1,218 +0,0 @@
// 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 info.debatty.java.stringsimilarity.JaroWinkler
import java.util.Locale
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.ViewUtils.setVisible
import org.yuzu.yuzu_emu.utils.collect
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?) {
super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = true, animated = true)
homeViewModel.setStatusBarShadeVisibility(true)
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
if (savedInstanceState != null) {
binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
}
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 ->
binding.clearButton.setVisible(text.toString().isNotEmpty())
filterAndSearch()
}
gamesViewModel.searchFocused.collect(
viewLifecycleOwner,
resetState = { gamesViewModel.setSearchFocused(false) }
) { if (it) focusSearch() }
gamesViewModel.games.collect(viewLifecycleOwner) { filterAndSearch() }
gamesViewModel.searchedGames.collect(viewLifecycleOwner) {
(binding.gridGamesSearch.adapter as GameAdapter).submitList(it)
binding.noResultsView.setVisible(it.isNotEmpty())
}
binding.clearButton.setOnClickListener { binding.searchText.setText("") }
binding.searchBackground.setOnClickListener { focusSearch() }
setInsets()
filterAndSearch()
}
private inner class ScoredGame(val score: Double, val item: Game)
private fun filterAndSearch() {
val baseList = gamesViewModel.games.value
val filteredList: List<Game> = when (binding.chipGroup.checkedChipId) {
R.id.chip_recently_played -> {
baseList.filter {
val lastPlayedTime = preferences.getLong(it.keyLastPlayedTime, 0L)
lastPlayedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
}.sortedByDescending { preferences.getLong(it.keyLastPlayedTime, 0L) }
}
R.id.chip_recently_added -> {
baseList.filter {
val addedTime = preferences.getLong(it.keyAddedToLibraryTime, 0L)
addedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
}.sortedByDescending { preferences.getLong(it.keyAddedToLibraryTime, 0L) }
}
R.id.chip_homebrew -> baseList.filter { it.isHomebrew }
R.id.chip_retail -> baseList.filter { !it.isHomebrew }
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 = if (searchTerm.length > 1) Jaccard(2) else JaroWinkler()
val sortedList: List<Game> = filteredList.mapNotNull { game ->
val title = game.title.lowercase(Locale.getDefault())
val score = searchAlgorithm.similarity(searchTerm, title)
if (score > 0.03) {
ScoredGame(score, game)
} else {
null
}
}.sortedByDescending { it.score }.map { it.item }
gamesViewModel.setSearchedGames(sortedList)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
if (_binding != null) {
outState.putString(SEARCH_TEXT, binding.searchText.text.toString())
}
}
private fun focusSearch() {
if (_binding != null) {
binding.searchText.requestFocus()
val imm = requireActivity()
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT)
}
}
private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { view: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
val spacingNavigationRail =
resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
val chipSpacing = resources.getDimensionPixelSize(R.dimen.spacing_chip)
binding.constraintSearch.updatePadding(
left = barInsets.left + cutoutInsets.left,
top = barInsets.top,
right = barInsets.right + cutoutInsets.right
)
binding.gridGamesSearch.updatePadding(
top = extraListSpacing,
bottom = barInsets.bottom + spacingNavigation + extraListSpacing
)
binding.noResultsView.updatePadding(bottom = spacingNavigation + barInsets.bottom)
val mlpDivider = binding.divider.layoutParams as ViewGroup.MarginLayoutParams
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
binding.frameSearch.updatePadding(left = spacingNavigationRail)
binding.gridGamesSearch.updatePadding(left = spacingNavigationRail)
binding.noResultsView.updatePadding(left = spacingNavigationRail)
binding.chipGroup.updatePadding(
left = chipSpacing + spacingNavigationRail,
right = chipSpacing
)
mlpDivider.leftMargin = chipSpacing + spacingNavigationRail
mlpDivider.rightMargin = chipSpacing
} else {
binding.frameSearch.updatePadding(right = spacingNavigationRail)
binding.gridGamesSearch.updatePadding(right = spacingNavigationRail)
binding.noResultsView.updatePadding(right = spacingNavigationRail)
binding.chipGroup.updatePadding(
left = chipSpacing,
right = chipSpacing + spacingNavigationRail
)
mlpDivider.leftMargin = chipSpacing
mlpDivider.rightMargin = chipSpacing + spacingNavigationRail
}
binding.divider.layoutParams = mlpDivider
windowInsets
}
}

View file

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.fragments
@ -78,7 +78,6 @@ class SetupFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mainActivity = requireActivity() as MainActivity mainActivity = requireActivity() as MainActivity
homeViewModel.setNavigationVisibility(visible = false, animated = false)
requireActivity().onBackPressedDispatcher.addCallback( requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner, viewLifecycleOwner,

View file

@ -1,9 +1,10 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.model package org.yuzu.yuzu_emu.model
import android.net.Uri import android.net.Uri
import android.widget.Toast
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -15,9 +16,9 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.utils.GameHelper import org.yuzu.yuzu_emu.utils.GameHelper
import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NativeConfig
@ -27,9 +28,6 @@ class GamesViewModel : ViewModel() {
val games: StateFlow<List<Game>> get() = _games val games: StateFlow<List<Game>> get() = _games
private val _games = MutableStateFlow(emptyList<Game>()) private val _games = MutableStateFlow(emptyList<Game>())
val searchedGames: StateFlow<List<Game>> get() = _searchedGames
private val _searchedGames = MutableStateFlow(emptyList<Game>())
val isReloading: StateFlow<Boolean> get() = _isReloading val isReloading: StateFlow<Boolean> get() = _isReloading
private val _isReloading = MutableStateFlow(false) private val _isReloading = MutableStateFlow(false)
@ -47,6 +45,8 @@ class GamesViewModel : ViewModel() {
private val _folders = MutableStateFlow(mutableListOf<GameDir>()) private val _folders = MutableStateFlow(mutableListOf<GameDir>())
val folders = _folders.asStateFlow() val folders = _folders.asStateFlow()
private val _filteredGames = MutableStateFlow<List<Game>>(emptyList())
init { init {
// Ensure keys are loaded so that ROM metadata can be decrypted. // Ensure keys are loaded so that ROM metadata can be decrypted.
NativeLibrary.reloadKeys() NativeLibrary.reloadKeys()
@ -66,10 +66,6 @@ class GamesViewModel : ViewModel() {
_games.value = sortedList _games.value = sortedList
} }
fun setSearchedGames(games: List<Game>) {
_searchedGames.value = games
}
fun setShouldSwapData(shouldSwap: Boolean) { fun setShouldSwapData(shouldSwap: Boolean) {
_shouldSwapData.value = shouldSwap _shouldSwapData.value = shouldSwap
} }
@ -82,6 +78,10 @@ class GamesViewModel : ViewModel() {
_searchFocused.value = searchFocused _searchFocused.value = searchFocused
} }
fun setFilteredGames(games: List<Game>) {
_filteredGames.value = games
}
fun reloadGames(directoriesChanged: Boolean, firstStartup: Boolean = false) { fun reloadGames(directoriesChanged: Boolean, firstStartup: Boolean = false) {
if (reloading.get()) { if (reloading.get()) {
return return
@ -131,12 +131,21 @@ class GamesViewModel : ViewModel() {
} }
} }
fun addFolder(gameDir: GameDir) = fun addFolder(gameDir: GameDir, savedFromGameFragment: Boolean) =
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
NativeConfig.addGameDir(gameDir) NativeConfig.addGameDir(gameDir)
getGameDirs(true) getGameDirs(true)
} }
if (savedFromGameFragment) {
NativeConfig.saveGlobalConfig()
Toast.makeText(
YuzuApplication.appContext,
R.string.add_directory_success,
Toast.LENGTH_SHORT
).show()
}
} }
fun removeFolder(gameDir: GameDir) = fun removeFolder(gameDir: GameDir) =

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.model package org.yuzu.yuzu_emu.model
@ -10,9 +10,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
class HomeViewModel : ViewModel() { class HomeViewModel : ViewModel() {
val navigationVisible: StateFlow<Pair<Boolean, Boolean>> get() = _navigationVisible
private val _navigationVisible = MutableStateFlow(Pair(false, false))
val statusBarShadeVisible: StateFlow<Boolean> get() = _statusBarShadeVisible val statusBarShadeVisible: StateFlow<Boolean> get() = _statusBarShadeVisible
private val _statusBarShadeVisible = MutableStateFlow(true) private val _statusBarShadeVisible = MutableStateFlow(true)
@ -36,13 +33,6 @@ class HomeViewModel : ViewModel() {
var navigatedToSetup = false var navigatedToSetup = false
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
if (navigationVisible.value.first == visible) {
return
}
_navigationVisible.value = Pair(visible, animated)
}
fun setStatusBarShadeVisibility(visible: Boolean) { fun setStatusBarShadeVisibility(visible: Boolean) {
if (statusBarShadeVisible.value == visible) { if (statusBarShadeVisible.value == visible) {
return return

View file

@ -1,35 +1,69 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.ui package org.yuzu.yuzu_emu.ui
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.PopupMenu
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import info.debatty.java.stringsimilarity.Jaccard
import info.debatty.java.stringsimilarity.JaroWinkler
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.adapters.GameAdapter import org.yuzu.yuzu_emu.adapters.GameAdapter
import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding
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.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import org.yuzu.yuzu_emu.utils.collect import org.yuzu.yuzu_emu.utils.collect
import java.util.Locale
import androidx.core.content.edit
class GamesFragment : Fragment() { class GamesFragment : Fragment() {
private var _binding: FragmentGamesBinding? = null private var _binding: FragmentGamesBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
companion object {
private const val SEARCH_TEXT = "SearchText"
private const val PREF_VIEW_TYPE = "GamesViewType"
private const val PREF_SORT_TYPE = "GamesSortType"
}
private val gamesViewModel: GamesViewModel by activityViewModels() private val gamesViewModel: GamesViewModel by activityViewModels()
private val homeViewModel: HomeViewModel by activityViewModels() private val homeViewModel: HomeViewModel by activityViewModels()
private lateinit var gameAdapter: GameAdapter
private val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
private lateinit var mainActivity: MainActivity
private val getGamesDirectory =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
if (result != null) {
mainActivity.processGamesDir(result, true)
}
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -42,17 +76,19 @@ class GamesFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = true, animated = true)
homeViewModel.setStatusBarShadeVisibility(true) homeViewModel.setStatusBarShadeVisibility(true)
mainActivity = requireActivity() as MainActivity
binding.gridGames.apply { if (savedInstanceState != null) {
layoutManager = AutofitGridLayoutManager( binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
requireContext(),
requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
)
adapter = GameAdapter(requireActivity() as AppCompatActivity)
} }
gameAdapter = GameAdapter(
requireActivity() as AppCompatActivity,
)
applyGridGamesBinding()
binding.swipeRefresh.apply { binding.swipeRefresh.apply {
// Add swipe down to refresh gesture // Add swipe down to refresh gesture
setOnRefreshListener { setOnRefreshListener {
@ -90,14 +126,14 @@ class GamesFragment : Fragment() {
) )
} }
gamesViewModel.games.collect(viewLifecycleOwner) { gamesViewModel.games.collect(viewLifecycleOwner) {
(binding.gridGames.adapter as GameAdapter).submitList(it) setAdapter(it)
} }
gamesViewModel.shouldSwapData.collect( gamesViewModel.shouldSwapData.collect(
viewLifecycleOwner, viewLifecycleOwner,
resetState = { gamesViewModel.setShouldSwapData(false) } resetState = { gamesViewModel.setShouldSwapData(false) }
) { ) {
if (it) { if (it) {
(binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value) setAdapter(gamesViewModel.games.value)
} }
} }
gamesViewModel.shouldScrollToTop.collect( gamesViewModel.shouldScrollToTop.collect(
@ -105,9 +141,193 @@ class GamesFragment : Fragment() {
resetState = { gamesViewModel.setShouldScrollToTop(false) } resetState = { gamesViewModel.setShouldScrollToTop(false) }
) { if (it) scrollToTop() } ) { if (it) scrollToTop() }
setInsets() setupTopView()
binding.addDirectory.setOnClickListener {
getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
}
setInsets()
} }
val applyGridGamesBinding = {
binding.gridGames.apply {
val savedViewType = preferences.getInt(PREF_VIEW_TYPE, GameAdapter.VIEW_TYPE_GRID)
gameAdapter.setViewType(savedViewType)
currentFilter = preferences.getInt(PREF_SORT_TYPE, View.NO_ID)
adapter = gameAdapter
val gameGrid = when (savedViewType) {
GameAdapter.VIEW_TYPE_LIST -> R.integer.game_columns_list
GameAdapter.VIEW_TYPE_GRID -> R.integer.game_columns_grid
else -> 0
}
layoutManager = GridLayoutManager(requireContext(), resources.getInteger(gameGrid))
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
if (_binding != null) {
outState.putString(SEARCH_TEXT, binding.searchText.text.toString())
}
}
private fun setAdapter(games: List<Game>) {
val currentSearchText = binding.searchText.text.toString()
val currentFilter = binding.filterButton.id
if (currentSearchText.isNotEmpty() || currentFilter != View.NO_ID) {
filterAndSearch(games)
} else {
(binding.gridGames.adapter as GameAdapter).submitList(games)
gamesViewModel.setFilteredGames(games)
}
}
private fun setupTopView() {
binding.searchText.doOnTextChanged() { text: CharSequence?, _: Int, _: Int, _: Int ->
if (text.toString().isNotEmpty()) {
binding.clearButton.visibility = View.VISIBLE
} else {
binding.clearButton.visibility = View.INVISIBLE
}
filterAndSearch()
}
binding.clearButton.setOnClickListener { binding.searchText.setText("") }
binding.searchBackground.setOnClickListener { focusSearch() }
// Setup view button
binding.viewButton.setOnClickListener { showViewMenu(it) }
// Setup filter button
binding.filterButton.setOnClickListener { view ->
showFilterMenu(view)
}
// Setup settings button
binding.settingsButton.setOnClickListener { navigateToSettings() }
}
private fun navigateToSettings() {
val navController = findNavController()
navController.navigate(R.id.action_gamesFragment_to_homeSettingsFragment)
}
private fun showViewMenu(anchor: View) {
val popup = PopupMenu(requireContext(), anchor)
popup.menuInflater.inflate(R.menu.menu_game_views, popup.menu)
val currentViewType = (preferences.getInt(PREF_VIEW_TYPE, GameAdapter.VIEW_TYPE_GRID))
when (currentViewType) {
GameAdapter.VIEW_TYPE_LIST -> popup.menu.findItem(R.id.view_list).isChecked = true
GameAdapter.VIEW_TYPE_GRID -> popup.menu.findItem(R.id.view_grid).isChecked = true
}
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.view_grid -> {
preferences.edit() { putInt(PREF_VIEW_TYPE, GameAdapter.VIEW_TYPE_GRID) }
applyGridGamesBinding()
item.isChecked = true
true
}
R.id.view_list -> {
preferences.edit() { putInt(PREF_VIEW_TYPE, GameAdapter.VIEW_TYPE_LIST) }
applyGridGamesBinding()
item.isChecked = true
true
}
else -> false
}
}
popup.show()
}
private fun showFilterMenu(anchor: View) {
val popup = PopupMenu(requireContext(), anchor)
popup.menuInflater.inflate(R.menu.menu_game_filters, popup.menu)
// Set checked state based on current filter
when (currentFilter) {
R.id.alphabetical -> popup.menu.findItem(R.id.alphabetical).isChecked = true
R.id.filter_recently_played -> popup.menu.findItem(R.id.filter_recently_played).isChecked =
true
R.id.filter_recently_added -> popup.menu.findItem(R.id.filter_recently_added).isChecked =
true
}
popup.setOnMenuItemClickListener { item ->
currentFilter = item.itemId
preferences.edit().putInt(PREF_SORT_TYPE, currentFilter).apply()
filterAndSearch()
true
}
popup.show()
}
// Track current filter
private var currentFilter = View.NO_ID
private fun filterAndSearch(baseList: List<Game> = gamesViewModel.games.value) {
val filteredList: List<Game> = when (currentFilter) {
R.id.alphabetical -> baseList.sortedBy { it.title }
R.id.filter_recently_played -> {
baseList.filter {
val lastPlayedTime = preferences.getLong(it.keyLastPlayedTime, 0L)
lastPlayedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
}.sortedByDescending { preferences.getLong(it.keyLastPlayedTime, 0L) }
}
R.id.filter_recently_added -> {
baseList.filter {
val addedTime = preferences.getLong(it.keyAddedToLibraryTime, 0L)
addedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
}.sortedByDescending { preferences.getLong(it.keyAddedToLibraryTime, 0L) }
}
else -> baseList
}
val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault())
if (searchTerm.isEmpty()) {
(binding.gridGames.adapter as GameAdapter).submitList(filteredList)
gamesViewModel.setFilteredGames(filteredList)
return
}
val searchAlgorithm = if (searchTerm.length > 1) Jaccard(2) else JaroWinkler()
val sortedList = 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 }
(binding.gridGames.adapter as GameAdapter).submitList(sortedList)
gamesViewModel.setFilteredGames(sortedList)
}
private inner class ScoredGame(val score: Double, val item: Game)
private fun focusSearch() {
binding.searchText.requestFocus()
val imm = requireActivity()
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT)
}
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
_binding = null _binding = null
@ -125,15 +345,10 @@ class GamesFragment : Fragment() {
) { view: View, windowInsets: WindowInsetsCompat -> ) { view: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_large)
val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation) val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
val spacingNavigationRail =
resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail) resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
val isLandscape =
binding.gridGames.updatePadding( resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
top = barInsets.top + extraListSpacing,
bottom = barInsets.bottom + spacingNavigation + extraListSpacing
)
binding.swipeRefresh.setProgressViewEndTarget( binding.swipeRefresh.setProgressViewEndTarget(
false, false,
@ -142,18 +357,28 @@ class GamesFragment : Fragment() {
val leftInsets = barInsets.left + cutoutInsets.left val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right val rightInsets = barInsets.right + cutoutInsets.right
val left: Int val mlpSwipe = binding.swipeRefresh.layoutParams as ViewGroup.MarginLayoutParams
val right: Int
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) { if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
left = leftInsets + spacingNavigationRail mlpSwipe.leftMargin = leftInsets
right = rightInsets mlpSwipe.rightMargin = rightInsets
} else { } else {
left = leftInsets mlpSwipe.leftMargin = leftInsets
right = rightInsets + spacingNavigationRail mlpSwipe.rightMargin = rightInsets
} }
binding.swipeRefresh.updateMargins(left = left, right = right) binding.swipeRefresh.layoutParams = mlpSwipe
binding.noticeText.updatePadding(bottom = spacingNavigation) binding.noticeText.updatePadding(bottom = spacingNavigation)
binding.header.updatePadding(top = cutoutInsets.top + resources.getDimensionPixelSize(R.dimen.spacing_large) + if (isLandscape) barInsets.top else 0)
binding.gridGames.updatePadding(
top = resources.getDimensionPixelSize(R.dimen.spacing_med)
)
val mlpFab = binding.addDirectory.layoutParams as ViewGroup.MarginLayoutParams
val fabPadding = resources.getDimensionPixelSize(R.dimen.spacing_large)
mlpFab.leftMargin = leftInsets + fabPadding
mlpFab.bottomMargin = barInsets.bottom + fabPadding
mlpFab.rightMargin = rightInsets + fabPadding
binding.addDirectory.layoutParams = mlpFab
windowInsets windowInsets
} }

View file

@ -121,27 +121,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val navHostFragment = val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
setUpNavigation(navHostFragment.navController) setUpNavigation(navHostFragment.navController)
(binding.navigationView as NavigationBarView).setOnItemReselectedListener {
when (it.itemId) {
R.id.gamesFragment -> gamesViewModel.setShouldScrollToTop(true)
R.id.searchFragment -> gamesViewModel.setSearchFocused(true)
R.id.homeSettingsFragment -> {
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
null,
Settings.MenuTag.SECTION_ROOT
)
navHostFragment.navController.navigate(action)
}
}
}
// Prevents navigation from being drawn for a short time on recreation if set to hidden
if (!homeViewModel.navigationVisible.value.first) {
binding.navigationView.setVisible(visible = false, gone = false)
binding.statusBarShade.setVisible(visible = false, gone = false)
}
homeViewModel.navigationVisible.collect(this) { showNavigation(it.first, it.second) }
homeViewModel.statusBarShadeVisible.collect(this) { showStatusBarShade(it) } homeViewModel.statusBarShadeVisible.collect(this) { showStatusBarShade(it) }
homeViewModel.contentToInstall.collect( homeViewModel.contentToInstall.collect(
this, this,
@ -175,8 +155,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
fun finishSetup(navController: NavController) { fun finishSetup(navController: NavController) {
navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment) navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment)
(binding.navigationView as NavigationBarView).setupWithNavController(navController)
showNavigation(visible = true, animated = true)
} }
private fun setUpNavigation(navController: NavController) { private fun setUpNavigation(navController: NavController) {
@ -186,64 +164,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
if (firstTimeSetup && !homeViewModel.navigatedToSetup) { if (firstTimeSetup && !homeViewModel.navigatedToSetup) {
navController.navigate(R.id.firstTimeSetupFragment) navController.navigate(R.id.firstTimeSetupFragment)
homeViewModel.navigatedToSetup = true homeViewModel.navigatedToSetup = true
} else {
(binding.navigationView as NavigationBarView).setupWithNavController(navController)
} }
} }
private fun showNavigation(visible: Boolean, animated: Boolean) {
if (!animated) {
binding.navigationView.setVisible(visible)
return
}
val smallLayout = resources.getBoolean(R.bool.small_layout)
binding.navigationView.animate().apply {
if (visible) {
binding.navigationView.setVisible(true)
duration = 300
interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f)
if (smallLayout) {
binding.navigationView.translationY =
binding.navigationView.height.toFloat() * 2
translationY(0f)
} else {
if (ViewCompat.getLayoutDirection(binding.navigationView) ==
ViewCompat.LAYOUT_DIRECTION_LTR
) {
binding.navigationView.translationX =
binding.navigationView.width.toFloat() * -2
translationX(0f)
} else {
binding.navigationView.translationX =
binding.navigationView.width.toFloat() * 2
translationX(0f)
}
}
} else {
duration = 300
interpolator = PathInterpolator(0.3f, 0f, 0.8f, 0.15f)
if (smallLayout) {
translationY(binding.navigationView.height.toFloat() * 2)
} else {
if (ViewCompat.getLayoutDirection(binding.navigationView) ==
ViewCompat.LAYOUT_DIRECTION_LTR
) {
translationX(binding.navigationView.width.toFloat() * -2)
} else {
translationX(binding.navigationView.width.toFloat() * 2)
}
}
}
}.withEndAction {
if (!visible) {
binding.navigationView.setVisible(visible = false, gone = false)
}
}.start()
}
private fun showStatusBarShade(visible: Boolean) { private fun showStatusBarShade(visible: Boolean) {
binding.statusBarShade.animate().apply { binding.statusBarShade.animate().apply {
if (visible) { if (visible) {
@ -254,7 +177,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f) interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f)
} else { } else {
duration = 300 duration = 300
translationY(binding.navigationView.height.toFloat() * -2) translationY(binding.statusBarShade.height.toFloat() * -2)
interpolator = PathInterpolator(0.3f, 0f, 0.8f, 0.15f) interpolator = PathInterpolator(0.3f, 0f, 0.8f, 0.15f)
} }
}.withEndAction { }.withEndAction {
@ -299,7 +222,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
} }
} }
fun processGamesDir(result: Uri) { fun processGamesDir(result: Uri, calledFromGameFragment: Boolean = false) {
contentResolver.takePersistableUriPermission( contentResolver.takePersistableUriPermission(
result, result,
Intent.FLAG_GRANT_READ_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION
@ -316,7 +239,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return return
} }
AddGameFolderDialogFragment.newInstance(uriString) AddGameFolderDialogFragment.newInstance(uriString, calledFromGameFragment)
.show(supportFragmentManager, AddGameFolderDialogFragment.TAG) .show(supportFragmentManager, AddGameFolderDialogFragment.TAG)
} }

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,4.5C7.05,4.5 2.73,7.61 1,12c1.73,4.39 6.05,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6.05,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z"/>
</vector>

View file

@ -21,18 +21,6 @@
app:navGraph="@navigation/home_navigation" app:navGraph="@navigation/home_navigation"
tools:layout="@layout/fragment_games" /> tools:layout="@layout/fragment_games" />
<com.google.android.material.navigationrail.NavigationRailView
android:id="@+id/navigation_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:labelVisibilityMode="selected"
app:menu="@menu/menu_navigation"
tools:visibility="visible" />
<View <View
android:id="@+id/status_bar_shade" android:id="@+id/status_bar_shade"
android:layout_width="0dp" android:layout_width="0dp"

View file

@ -21,19 +21,6 @@
app:navGraph="@navigation/home_navigation" app:navGraph="@navigation/home_navigation"
tools:layout="@layout/fragment_games" /> tools:layout="@layout/fragment_games" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/navigation_view"
android:background="?attr/colorSurface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/menu_navigation"
app:labelVisibilityMode="selected"
tools:visibility="visible" />
<View <View
android:id="@+id/status_bar_shade" android:id="@+id/status_bar_shade"
android:layout_width="0dp" android:layout_width="0dp"

View file

@ -6,7 +6,7 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/card_game" android:id="@+id/card_game_grid"
style="?attr/materialCardViewElevatedStyle" style="?attr/materialCardViewElevatedStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card_game_list"
style="?attr/materialCardViewStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:transitionName="card_game"
app:cardCornerRadius="14dp"
app:cardElevation="0dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image_game_screen"
android:layout_width="66dp"
android:layout_height="66dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Medium"
tools:src="@drawable/default_icon" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_game_title"
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:requiresFadingEdge="horizontal"
android:textAlignment="viewStart"
android:singleLine="true"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@+id/text_game_developer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_game_screen"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="The Legend of Zelda: Skyward Sword" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_game_developer"
style="@style/TextAppearance.Material3.BodySmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:requiresFadingEdge="horizontal"
android:textAlignment="viewStart"
android:singleLine="true"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_game_screen"
app:layout_constraintTop_toBottomOf="@+id/text_game_title"
tools:text="Generic" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -1,35 +1,224 @@
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/swipe_refresh"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurface" android:background="?attr/colorSurface"
android:clipToPadding="false"> >
<RelativeLayout <LinearLayout
android:id="@+id/header"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginHorizontal="20dp"
app:layout_constraintTop_toTopOf="parent"
>
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/notice_text" android:id="@+id/title"
style="@style/TextAppearance.Material3.BodyLarge" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.Material3.HeadlineLarge"
android:textSize="27sp"
android:textStyle="bold"
/>
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
/>
<com.google.android.material.card.MaterialCardView
android:id="@+id/view_button"
style="?attr/materialCardViewFilledStyle"
android:layout_width="42dp"
android:layout_height="42dp"
app:cardCornerRadius="21dp"
>
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_gravity="center"
android:src="@drawable/ic_eye"
app:tint="?attr/colorOnSurfaceVariant"
/>
</com.google.android.material.card.MaterialCardView>
<Space
android:layout_width="15dp"
android:layout_height="wrap_content"
/>
<com.google.android.material.card.MaterialCardView
android:id="@+id/filter_button"
style="?attr/materialCardViewFilledStyle"
android:layout_width="42dp"
android:layout_height="42dp"
app:cardCornerRadius="21dp"
>
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_gravity="center"
android:src="@drawable/ic_filter"
app:tint="?attr/colorOnSurfaceVariant"
/>
</com.google.android.material.card.MaterialCardView>
<Space
android:layout_width="15dp"
android:layout_height="wrap_content"
/>
<com.google.android.material.card.MaterialCardView
android:id="@+id/settings_button"
style="?attr/materialCardViewFilledStyle"
android:layout_width="42dp"
android:layout_height="42dp"
app:cardCornerRadius="21dp"
>
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_gravity="center"
android:src="@drawable/ic_settings"
app:tint="?attr/colorOnSurfaceVariant"
/>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<FrameLayout
android:id="@+id/frame_search"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginHorizontal="15dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/header"
>
<com.google.android.material.card.MaterialCardView
android:id="@+id/search_background"
style="?attr/materialCardViewFilledStyle"
android:layout_width="match_parent"
android:layout_height="48dp"
app:cardCornerRadius="21dp"
>
<LinearLayout
android:id="@+id/search_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="18dp"
android:layout_marginEnd="42dp"
android:orientation="horizontal"
>
<ImageView
android:layout_width="21dp"
android:layout_height="21dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="18dp"
android:src="@drawable/ic_search"
app:tint="?attr/colorOnSurfaceVariant"
/>
<EditText
android:id="@+id/search_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:hint="@string/home_search_games"
android:inputType="text"
android:maxLines="1"
android:imeOptions="flagNoFullscreen"
/>
</LinearLayout>
<ImageView
android:id="@+id/clear_button"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_gravity="center_vertical|end"
android:layout_marginEnd="18dp"
android:background="?attr/selectableItemBackground"
android:src="@drawable/ic_clear"
android:visibility="invisible"
app:tint="?attr/colorOnSurfaceVariant"
/>
</com.google.android.material.card.MaterialCardView>
</FrameLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@id/frame_search"
app:layout_constraintBottom_toBottomOf="parent"
>
<RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center" >
android:padding="@dimen/spacing_large"
android:text="@string/empty_gamelist"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView <com.google.android.material.textview.MaterialTextView
android:id="@+id/grid_games" android:id="@+id/notice_text"
android:layout_width="match_parent" style="@style/TextAppearance.Material3.BodyLarge"
android:layout_height="match_parent" android:layout_width="match_parent"
android:clipToPadding="false" android:layout_height="match_parent"
android:defaultFocusHighlightEnabled="false" android:gravity="center"
tools:listitem="@layout/card_game" /> android:padding="@dimen/spacing_large"
android:text="@string/empty_gamelist"
android:visibility="gone"
/>
</RelativeLayout> <androidx.recyclerview.widget.RecyclerView
android:id="@+id/grid_games"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical"
android:fadeScrollbars="true"
android:paddingHorizontal="4dp"
android:paddingVertical="4dp"
/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</RelativeLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/add_directory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="Select_game_folder"
android:text="@string/folder"
app:icon="@drawable/ic_cartridge_outline"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:textColor="?attr/colorOnPrimaryContainer"
app:backgroundTint="?attr/colorPrimaryContainer"
app:iconTint="?attr/colorOnPrimaryContainer"
/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,184 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraint_search"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:clipToPadding="false">
<RelativeLayout
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/divider">
<LinearLayout
android:id="@+id/no_results_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<ImageView
android:id="@+id/icon_no_results"
android:layout_width="match_parent"
android:layout_height="80dp"
android:src="@drawable/ic_search" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/notice_text"
style="@style/TextAppearance.Material3.TitleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="8dp"
android:text="@string/search_and_filter_games"
tools:visibility="visible" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/grid_games_search"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
</RelativeLayout>
<FrameLayout
android:id="@+id/frame_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginHorizontal="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.card.MaterialCardView
android:id="@+id/search_background"
style="?attr/materialCardViewFilledStyle"
android:layout_width="match_parent"
android:layout_height="56dp"
app:cardCornerRadius="28dp">
<LinearLayout
android:id="@+id/search_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="24dp"
android:layout_marginEnd="56dp"
android:orientation="horizontal">
<ImageView
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="24dp"
android:src="@drawable/ic_search"
app:tint="?attr/colorOnSurfaceVariant" />
<EditText
android:id="@+id/search_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:hint="@string/home_search_games"
android:inputType="text"
android:maxLines="1"
android:imeOptions="flagNoFullscreen" />
</LinearLayout>
<ImageView
android:id="@+id/clear_button"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical|end"
android:layout_marginEnd="24dp"
android:background="?attr/selectableItemBackground"
android:src="@drawable/ic_clear"
android:visibility="invisible"
app:tint="?attr/colorOnSurfaceVariant"
tools:visibility="visible" />
</com.google.android.material.card.MaterialCardView>
</FrameLayout>
<HorizontalScrollView
android:id="@+id/horizontalScrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fadingEdge="horizontal"
android:scrollbars="none"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/frame_search">
<com.google.android.material.chip.ChipGroup
android:id="@+id/chip_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingVertical="4dp"
app:checkedChip="@id/chip_recently_played"
app:chipSpacingHorizontal="12dp"
app:singleLine="true"
app:singleSelection="true">
<com.google.android.material.chip.Chip
android:id="@+id/chip_recently_played"
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/search_recently_played"
app:chipCornerRadius="28dp" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_recently_added"
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/search_recently_added"
app:chipCornerRadius="28dp" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_retail"
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/search_retail"
app:chipCornerRadius="28dp" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_homebrew"
style="@style/Widget.Material3.Chip.Suggestion.Elevated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:text="@string/search_homebrew"
app:chipCornerRadius="28dp" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<com.google.android.material.divider.MaterialDivider
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/horizontalScrollView" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/homeSettingsFragment"
android:icon="@drawable/selector_settings"
android:title="@string/home_settings" />
<item
android:id="@+id/searchFragment"
android:icon="@drawable/ic_search"
android:title="@string/home_search" />
<item
android:id="@+id/gamesFragment"
android:icon="@drawable/selector_cartridge"
android:title="@string/home_games" />
</menu>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item android:id="@+id/alphabetical"
android:title="@string/alphabetical"
android:checked="true"/>
<item
android:id="@+id/filter_recently_played"
android:title="@string/search_recently_played" />
<item
android:id="@+id/filter_recently_added"
android:title="@string/search_recently_added" />
</group>
</menu>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/view_grid"
android:title="@string/view_grid"/>
<item
android:id="@+id/view_list"
android:title="@string/view_list"
android:checked="true"/>
</group>
</menu>

View file

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/gamesFragment"
android:icon="@drawable/selector_cartridge"
android:title="@string/home_games" />
<item
android:id="@+id/searchFragment"
android:icon="@drawable/ic_search"
android:title="@string/home_search" />
<item
android:id="@+id/homeSettingsFragment"
android:icon="@drawable/selector_settings"
android:title="@string/home_settings" />
</menu>

View file

@ -8,6 +8,13 @@
android:id="@+id/gamesFragment" android:id="@+id/gamesFragment"
android:name="org.yuzu.yuzu_emu.ui.GamesFragment" android:name="org.yuzu.yuzu_emu.ui.GamesFragment"
android:label="PlatformGamesFragment" /> android:label="PlatformGamesFragment" />
<action
android:id="@+id/action_gamesFragment_to_homeSettingsFragment"
app:destination="@id/homeSettingsFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<fragment <fragment
android:id="@+id/homeSettingsFragment" android:id="@+id/homeSettingsFragment"
@ -41,11 +48,6 @@
app:popUpToInclusive="true" /> app:popUpToInclusive="true" />
</fragment> </fragment>
<fragment
android:id="@+id/searchFragment"
android:name="org.yuzu.yuzu_emu.fragments.SearchFragment"
android:label="SearchFragment" />
<fragment <fragment
android:id="@+id/aboutFragment" android:id="@+id/aboutFragment"
android:name="org.yuzu.yuzu_emu.fragments.AboutFragment" android:name="org.yuzu.yuzu_emu.fragments.AboutFragment"

View file

@ -2,5 +2,8 @@
<resources> <resources>
<integer name="grid_columns">2</integer> <integer name="grid_columns">2</integer>
<integer name="game_columns_list">2</integer>
<integer name="game_columns_grid">4</integer>
</resources> </resources>

View file

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<integer name="grid_columns">1</integer> <integer name="grid_columns">1</integer>
<integer name="game_columns_list">1</integer>
<integer name="game_columns_grid">2</integer>
<!-- Default SWITCH landscape layout --> <!-- Default SWITCH landscape layout -->
<integer name="BUTTON_A_X">760</integer> <integer name="BUTTON_A_X">760</integer>

View file

@ -2,7 +2,7 @@
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"> <resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<!-- General application strings --> <!-- General application strings -->
<string name="app_name" translatable="false">eden</string> <string name="app_name" translatable="false">Eden</string>
<string name="app_disclaimer">This software will run games for the Nintendo Switch game console. No game titles or keys are included.&lt;br /&gt;&lt;br /&gt;Before you begin, please locate your <![CDATA[<b> prod.keys </b>]]> file on your device storage.&lt;br /&gt;&lt;br /&gt;<![CDATA[<a href="https://yuzu-emu.org/help/quickstart">Learn more</a>]]></string> <string name="app_disclaimer">This software will run games for the Nintendo Switch game console. No game titles or keys are included.&lt;br /&gt;&lt;br /&gt;Before you begin, please locate your <![CDATA[<b> prod.keys </b>]]> file on your device storage.&lt;br /&gt;&lt;br /&gt;<![CDATA[<a href="https://yuzu-emu.org/help/quickstart">Learn more</a>]]></string>
<string name="notice_notification_channel_name">Notices and errors</string> <string name="notice_notification_channel_name">Notices and errors</string>
<string name="notice_notification_channel_id" translatable="false">noticesAndErrors</string> <string name="notice_notification_channel_id" translatable="false">noticesAndErrors</string>
@ -28,6 +28,11 @@
<string name="step_complete">Complete!</string> <string name="step_complete">Complete!</string>
<!-- Home strings --> <!-- Home strings -->
<string name="alphabetical">Alphabetical</string>
<string name="view_list">List</string>
<string name="view_grid">Grid</string>
<string name="folder">Folder</string>
<string name="add_directory_success">"New game directory added successfully "</string>
<string name="home_games">Games</string> <string name="home_games">Games</string>
<string name="home_search">Search</string> <string name="home_search">Search</string>
<string name="home_settings">Settings</string> <string name="home_settings">Settings</string>