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:
parent
08ac410558
commit
2dd8d09c7b
34 changed files with 719 additions and 694 deletions
|
@ -1,5 +1,5 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.adapters
|
||||
|
||||
|
@ -16,13 +16,15 @@ import androidx.lifecycle.ViewModelProvider
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
||||
import org.yuzu.yuzu_emu.R
|
||||
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.GamesViewModel
|
||||
import org.yuzu.yuzu_emu.utils.GameIconUtils
|
||||
|
@ -31,22 +33,70 @@ import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
|||
|
||||
class GameAdapter(private val activity: AppCompatActivity) :
|
||||
AbstractDiffAdapter<Game, GameAdapter.GameViewHolder>(exact = false) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
|
||||
CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
.also { return GameViewHolder(it) }
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE_GRID = 0
|
||||
const val VIEW_TYPE_LIST = 1
|
||||
}
|
||||
|
||||
inner class GameViewHolder(val binding: CardGameBinding) :
|
||||
AbstractViewHolder<Game>(binding) {
|
||||
private var viewType = 0
|
||||
|
||||
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) {
|
||||
binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
GameIconUtils.loadGameIcon(model, binding.imageGameScreen)
|
||||
when (viewType) {
|
||||
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()
|
||||
binding.cardGame.setOnClickListener { onClick(model) }
|
||||
binding.cardGame.setOnLongClickListener { onLongClick(model) }
|
||||
listBinding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
GameIconUtils.loadGameIcon(model, listBinding.imageGameScreen)
|
||||
|
||||
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) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
|
@ -50,7 +51,6 @@ class AboutFragment : Fragment() {
|
|||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||
|
||||
binding.toolbarAbout.setNavigationOnClickListener {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
|
@ -33,7 +33,8 @@ class AddGameFolderDialogFragment : DialogFragment() {
|
|||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||
val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked)
|
||||
homeViewModel.setGamesDirSelected(true)
|
||||
gamesViewModel.addFolder(newGameDir)
|
||||
val calledFromGameFragment = requireArguments().getBoolean("calledFromGameFragment", false)
|
||||
gamesViewModel.addFolder(newGameDir, calledFromGameFragment)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setView(binding.root)
|
||||
|
@ -45,9 +46,10 @@ class AddGameFolderDialogFragment : DialogFragment() {
|
|||
|
||||
private const val FOLDER_URI_STRING = "FolderUriString"
|
||||
|
||||
fun newInstance(folderUriString: String): AddGameFolderDialogFragment {
|
||||
fun newInstance(folderUriString: String, calledFromGameFragment: Boolean): AddGameFolderDialogFragment {
|
||||
val args = Bundle()
|
||||
args.putString(FOLDER_URI_STRING, folderUriString)
|
||||
args.putBoolean("calledFromGameFragment", calledFromGameFragment)
|
||||
val fragment = AddGameFolderDialogFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
|
@ -59,7 +59,6 @@ class AddonsFragment : Fragment() {
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
homeViewModel.setNavigationVisibility(visible = false, animated = false)
|
||||
homeViewModel.setStatusBarShadeVisibility(false)
|
||||
|
||||
binding.toolbarAddons.setNavigationOnClickListener {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
|
@ -47,7 +47,6 @@ class AppletLauncherFragment : Fragment() {
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||
|
||||
binding.toolbarApplets.setNavigationOnClickListener {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
|
@ -62,7 +62,6 @@ class DriverManagerFragment : Fragment() {
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||
|
||||
driverViewModel.onOpenDriverManager(args.game)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
|
@ -53,7 +53,6 @@ class GameFoldersFragment : Fragment() {
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||
|
||||
binding.toolbarFolders.setNavigationOnClickListener {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
|
@ -59,7 +59,6 @@ class GameInfoFragment : Fragment() {
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
homeViewModel.setNavigationVisibility(visible = false, animated = false)
|
||||
homeViewModel.setStatusBarShadeVisibility(false)
|
||||
|
||||
binding.apply {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
|
@ -76,7 +76,6 @@ class GamePropertiesFragment : Fragment() {
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||
homeViewModel.setStatusBarShadeVisibility(true)
|
||||
|
||||
binding.buttonBack.setOnClickListener {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
|
@ -70,7 +70,6 @@ class HomeSettingsFragment : Fragment() {
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
homeViewModel.setNavigationVisibility(visible = true, animated = true)
|
||||
homeViewModel.setStatusBarShadeVisibility(visible = true)
|
||||
mainActivity = requireActivity() as MainActivity
|
||||
|
||||
|
@ -389,32 +388,20 @@ class HomeSettingsFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun setInsets() =
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.root
|
||||
) { view: View, windowInsets: WindowInsetsCompat ->
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
|
||||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
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(
|
||||
top = barInsets.top,
|
||||
bottom = barInsets.bottom
|
||||
)
|
||||
|
||||
binding.scrollViewSettings.updateMargins(left = leftInsets, right = rightInsets)
|
||||
|
||||
binding.linearLayoutSettings.updatePadding(bottom = spacingNavigation)
|
||||
|
||||
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
|
||||
binding.linearLayoutSettings.updatePadding(left = spacingNavigationRail)
|
||||
} else {
|
||||
binding.linearLayoutSettings.updatePadding(right = spacingNavigationRail)
|
||||
}
|
||||
binding.homeSettingsList.updatePadding(
|
||||
left = barInsets.left + cutoutInsets.left,
|
||||
top = cutoutInsets.top,
|
||||
right = barInsets.right + cutoutInsets.right,
|
||||
bottom = barInsets.bottom
|
||||
)
|
||||
|
||||
windowInsets
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
|
@ -66,7 +66,6 @@ class InstallableFragment : Fragment() {
|
|||
|
||||
val mainActivity = requireActivity() as MainActivity
|
||||
|
||||
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||
|
||||
binding.toolbarInstallables.setNavigationOnClickListener {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
|
@ -45,7 +46,6 @@ class LicensesFragment : Fragment() {
|
|||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||
|
||||
binding.toolbarLicenses.setNavigationOnClickListener {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
|
@ -78,7 +78,6 @@ class SetupFragment : Fragment() {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
mainActivity = requireActivity() as MainActivity
|
||||
|
||||
homeViewModel.setNavigationVisibility(visible = false, animated = false)
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
@ -15,9 +16,9 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.utils.GameHelper
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
@ -27,9 +28,6 @@ class GamesViewModel : ViewModel() {
|
|||
val games: StateFlow<List<Game>> get() = _games
|
||||
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
|
||||
private val _isReloading = MutableStateFlow(false)
|
||||
|
||||
|
@ -47,6 +45,8 @@ class GamesViewModel : ViewModel() {
|
|||
private val _folders = MutableStateFlow(mutableListOf<GameDir>())
|
||||
val folders = _folders.asStateFlow()
|
||||
|
||||
private val _filteredGames = MutableStateFlow<List<Game>>(emptyList())
|
||||
|
||||
init {
|
||||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
||||
NativeLibrary.reloadKeys()
|
||||
|
@ -66,10 +66,6 @@ class GamesViewModel : ViewModel() {
|
|||
_games.value = sortedList
|
||||
}
|
||||
|
||||
fun setSearchedGames(games: List<Game>) {
|
||||
_searchedGames.value = games
|
||||
}
|
||||
|
||||
fun setShouldSwapData(shouldSwap: Boolean) {
|
||||
_shouldSwapData.value = shouldSwap
|
||||
}
|
||||
|
@ -82,6 +78,10 @@ class GamesViewModel : ViewModel() {
|
|||
_searchFocused.value = searchFocused
|
||||
}
|
||||
|
||||
fun setFilteredGames(games: List<Game>) {
|
||||
_filteredGames.value = games
|
||||
}
|
||||
|
||||
fun reloadGames(directoriesChanged: Boolean, firstStartup: Boolean = false) {
|
||||
if (reloading.get()) {
|
||||
return
|
||||
|
@ -131,12 +131,21 @@ class GamesViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
fun addFolder(gameDir: GameDir) =
|
||||
fun addFolder(gameDir: GameDir, savedFromGameFragment: Boolean) =
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
NativeConfig.addGameDir(gameDir)
|
||||
getGameDirs(true)
|
||||
}
|
||||
|
||||
if (savedFromGameFragment) {
|
||||
NativeConfig.saveGlobalConfig()
|
||||
Toast.makeText(
|
||||
YuzuApplication.appContext,
|
||||
R.string.add_directory_success,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFolder(gameDir: GameDir) =
|
||||
|
|
|
@ -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
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
@ -10,9 +10,6 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
class HomeViewModel : ViewModel() {
|
||||
val navigationVisible: StateFlow<Pair<Boolean, Boolean>> get() = _navigationVisible
|
||||
private val _navigationVisible = MutableStateFlow(Pair(false, false))
|
||||
|
||||
val statusBarShadeVisible: StateFlow<Boolean> get() = _statusBarShadeVisible
|
||||
private val _statusBarShadeVisible = MutableStateFlow(true)
|
||||
|
||||
|
@ -36,13 +33,6 @@ class HomeViewModel : ViewModel() {
|
|||
|
||||
var navigatedToSetup = false
|
||||
|
||||
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
|
||||
if (navigationVisible.value.first == visible) {
|
||||
return
|
||||
}
|
||||
_navigationVisible.value = Pair(visible, animated)
|
||||
}
|
||||
|
||||
fun setStatusBarShadeVisibility(visible: Boolean) {
|
||||
if (statusBarShadeVisible.value == visible) {
|
||||
return
|
||||
|
|
|
@ -1,35 +1,69 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
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.view.LayoutInflater
|
||||
import android.view.View
|
||||
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.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.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 info.debatty.java.stringsimilarity.Jaccard
|
||||
import info.debatty.java.stringsimilarity.JaroWinkler
|
||||
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.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.HomeViewModel
|
||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
|
||||
import org.yuzu.yuzu_emu.utils.collect
|
||||
import java.util.Locale
|
||||
import androidx.core.content.edit
|
||||
|
||||
class GamesFragment : Fragment() {
|
||||
private var _binding: FragmentGamesBinding? = null
|
||||
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 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(
|
||||
inflater: LayoutInflater,
|
||||
|
@ -42,17 +76,19 @@ class GamesFragment : Fragment() {
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
homeViewModel.setNavigationVisibility(visible = true, animated = true)
|
||||
homeViewModel.setStatusBarShadeVisibility(true)
|
||||
mainActivity = requireActivity() as MainActivity
|
||||
|
||||
binding.gridGames.apply {
|
||||
layoutManager = AutofitGridLayoutManager(
|
||||
requireContext(),
|
||||
requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
|
||||
)
|
||||
adapter = GameAdapter(requireActivity() as AppCompatActivity)
|
||||
if (savedInstanceState != null) {
|
||||
binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
|
||||
}
|
||||
|
||||
gameAdapter = GameAdapter(
|
||||
requireActivity() as AppCompatActivity,
|
||||
)
|
||||
|
||||
applyGridGamesBinding()
|
||||
|
||||
binding.swipeRefresh.apply {
|
||||
// Add swipe down to refresh gesture
|
||||
setOnRefreshListener {
|
||||
|
@ -90,14 +126,14 @@ class GamesFragment : Fragment() {
|
|||
)
|
||||
}
|
||||
gamesViewModel.games.collect(viewLifecycleOwner) {
|
||||
(binding.gridGames.adapter as GameAdapter).submitList(it)
|
||||
setAdapter(it)
|
||||
}
|
||||
gamesViewModel.shouldSwapData.collect(
|
||||
viewLifecycleOwner,
|
||||
resetState = { gamesViewModel.setShouldSwapData(false) }
|
||||
) {
|
||||
if (it) {
|
||||
(binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value)
|
||||
setAdapter(gamesViewModel.games.value)
|
||||
}
|
||||
}
|
||||
gamesViewModel.shouldScrollToTop.collect(
|
||||
|
@ -105,9 +141,193 @@ class GamesFragment : Fragment() {
|
|||
resetState = { gamesViewModel.setShouldScrollToTop(false) }
|
||||
) { 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() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
|
@ -125,15 +345,10 @@ class GamesFragment : Fragment() {
|
|||
) { 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_large)
|
||||
val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
|
||||
val spacingNavigationRail =
|
||||
resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
|
||||
|
||||
binding.gridGames.updatePadding(
|
||||
top = barInsets.top + extraListSpacing,
|
||||
bottom = barInsets.bottom + spacingNavigation + extraListSpacing
|
||||
)
|
||||
val isLandscape =
|
||||
resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
|
||||
binding.swipeRefresh.setProgressViewEndTarget(
|
||||
false,
|
||||
|
@ -142,18 +357,28 @@ class GamesFragment : Fragment() {
|
|||
|
||||
val leftInsets = barInsets.left + cutoutInsets.left
|
||||
val rightInsets = barInsets.right + cutoutInsets.right
|
||||
val left: Int
|
||||
val right: Int
|
||||
val mlpSwipe = binding.swipeRefresh.layoutParams as ViewGroup.MarginLayoutParams
|
||||
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
|
||||
left = leftInsets + spacingNavigationRail
|
||||
right = rightInsets
|
||||
mlpSwipe.leftMargin = leftInsets
|
||||
mlpSwipe.rightMargin = rightInsets
|
||||
} else {
|
||||
left = leftInsets
|
||||
right = rightInsets + spacingNavigationRail
|
||||
mlpSwipe.leftMargin = leftInsets
|
||||
mlpSwipe.rightMargin = rightInsets
|
||||
}
|
||||
binding.swipeRefresh.updateMargins(left = left, right = right)
|
||||
binding.swipeRefresh.layoutParams = mlpSwipe
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -121,27 +121,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
val navHostFragment =
|
||||
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
|
||||
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.contentToInstall.collect(
|
||||
this,
|
||||
|
@ -175,8 +155,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
|
||||
fun finishSetup(navController: NavController) {
|
||||
navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment)
|
||||
(binding.navigationView as NavigationBarView).setupWithNavController(navController)
|
||||
showNavigation(visible = true, animated = true)
|
||||
}
|
||||
|
||||
private fun setUpNavigation(navController: NavController) {
|
||||
|
@ -186,64 +164,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
if (firstTimeSetup && !homeViewModel.navigatedToSetup) {
|
||||
navController.navigate(R.id.firstTimeSetupFragment)
|
||||
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) {
|
||||
binding.statusBarShade.animate().apply {
|
||||
if (visible) {
|
||||
|
@ -254,7 +177,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f)
|
||||
} else {
|
||||
duration = 300
|
||||
translationY(binding.navigationView.height.toFloat() * -2)
|
||||
translationY(binding.statusBarShade.height.toFloat() * -2)
|
||||
interpolator = PathInterpolator(0.3f, 0f, 0.8f, 0.15f)
|
||||
}
|
||||
}.withEndAction {
|
||||
|
@ -299,7 +222,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
}
|
||||
}
|
||||
|
||||
fun processGamesDir(result: Uri) {
|
||||
fun processGamesDir(result: Uri, calledFromGameFragment: Boolean = false) {
|
||||
contentResolver.takePersistableUriPermission(
|
||||
result,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
|
@ -316,7 +239,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
return
|
||||
}
|
||||
|
||||
AddGameFolderDialogFragment.newInstance(uriString)
|
||||
AddGameFolderDialogFragment.newInstance(uriString, calledFromGameFragment)
|
||||
.show(supportFragmentManager, AddGameFolderDialogFragment.TAG)
|
||||
}
|
||||
|
||||
|
|
9
src/android/app/src/main/res/drawable/ic_eye.xml
Normal file
9
src/android/app/src/main/res/drawable/ic_eye.xml
Normal 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>
|
9
src/android/app/src/main/res/drawable/ic_filter.xml
Normal file
9
src/android/app/src/main/res/drawable/ic_filter.xml
Normal 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>
|
|
@ -21,18 +21,6 @@
|
|||
app:navGraph="@navigation/home_navigation"
|
||||
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
|
||||
android:id="@+id/status_bar_shade"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
@ -21,19 +21,6 @@
|
|||
app:navGraph="@navigation/home_navigation"
|
||||
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
|
||||
android:id="@+id/status_bar_shade"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_game"
|
||||
android:id="@+id/card_game_grid"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
67
src/android/app/src/main/res/layout/card_game_list.xml
Normal file
67
src/android/app/src/main/res/layout/card_game_list.xml
Normal 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>
|
|
@ -1,35 +1,224 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/swipe_refresh"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface"
|
||||
android:clipToPadding="false">
|
||||
>
|
||||
|
||||
<RelativeLayout
|
||||
<LinearLayout
|
||||
android:id="@+id/header"
|
||||
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
|
||||
android:id="@+id/notice_text"
|
||||
style="@style/TextAppearance.Material3.BodyLarge"
|
||||
android:id="@+id/title"
|
||||
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_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:padding="@dimen/spacing_large"
|
||||
android:text="@string/empty_gamelist"
|
||||
android:visibility="gone" />
|
||||
>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/grid_games"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
tools:listitem="@layout/card_game" />
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/notice_text"
|
||||
style="@style/TextAppearance.Material3.BodyLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
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>
|
|
@ -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>
|
|
@ -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>
|
14
src/android/app/src/main/res/menu/menu_game_filters.xml
Normal file
14
src/android/app/src/main/res/menu/menu_game_filters.xml
Normal 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>
|
12
src/android/app/src/main/res/menu/menu_game_views.xml
Normal file
12
src/android/app/src/main/res/menu/menu_game_views.xml
Normal 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>
|
|
@ -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>
|
|
@ -8,6 +8,13 @@
|
|||
android:id="@+id/gamesFragment"
|
||||
android:name="org.yuzu.yuzu_emu.ui.GamesFragment"
|
||||
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
|
||||
android:id="@+id/homeSettingsFragment"
|
||||
|
@ -41,11 +48,6 @@
|
|||
app:popUpToInclusive="true" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/searchFragment"
|
||||
android:name="org.yuzu.yuzu_emu.fragments.SearchFragment"
|
||||
android:label="SearchFragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/aboutFragment"
|
||||
android:name="org.yuzu.yuzu_emu.fragments.AboutFragment"
|
||||
|
|
|
@ -2,5 +2,8 @@
|
|||
<resources>
|
||||
|
||||
<integer name="grid_columns">2</integer>
|
||||
<integer name="game_columns_list">2</integer>
|
||||
<integer name="game_columns_grid">4</integer>
|
||||
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="grid_columns">1</integer>
|
||||
<integer name="game_columns_list">1</integer>
|
||||
<integer name="game_columns_grid">2</integer>
|
||||
|
||||
<!-- Default SWITCH landscape layout -->
|
||||
<integer name="BUTTON_A_X">760</integer>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||
|
||||
<!-- 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.<br /><br />Before you begin, please locate your <![CDATA[<b> prod.keys </b>]]> file on your device storage.<br /><br /><![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_id" translatable="false">noticesAndErrors</string>
|
||||
|
@ -28,6 +28,11 @@
|
|||
<string name="step_complete">Complete!</string>
|
||||
|
||||
<!-- 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_search">Search</string>
|
||||
<string name="home_settings">Settings</string>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue