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-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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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-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,
|
||||||
|
|
|
@ -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) =
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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"
|
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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
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.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>
|
|
@ -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: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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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.<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="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_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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue