android: Search Fragment
This commit is contained in:
parent
3281dc597e
commit
6df030998a
|
@ -13,6 +13,7 @@ import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
@ -21,6 +22,7 @@ import coil.load
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.yuzu.yuzu_emu.NativeLibrary
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.databinding.CardGameBinding
|
import org.yuzu.yuzu_emu.databinding.CardGameBinding
|
||||||
import org.yuzu.yuzu_emu.activities.EmulationActivity
|
import org.yuzu.yuzu_emu.activities.EmulationActivity
|
||||||
import org.yuzu.yuzu_emu.model.Game
|
import org.yuzu.yuzu_emu.model.Game
|
||||||
|
@ -51,6 +53,14 @@ class GameAdapter(private val activity: AppCompatActivity) :
|
||||||
*/
|
*/
|
||||||
override fun onClick(view: View) {
|
override fun onClick(view: View) {
|
||||||
val holder = view.tag as GameViewHolder
|
val holder = view.tag as GameViewHolder
|
||||||
|
val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||||
|
preferences.edit()
|
||||||
|
.putLong(
|
||||||
|
holder.game.keyLastPlayedTime,
|
||||||
|
System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
.apply()
|
||||||
|
|
||||||
EmulationActivity.launch(activity, holder.game)
|
EmulationActivity.launch(activity, holder.game)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
|
@ -30,6 +31,7 @@ import org.yuzu.yuzu_emu.features.DocumentProvider
|
||||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
|
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
|
||||||
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
|
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
|
||||||
import org.yuzu.yuzu_emu.model.HomeSetting
|
import org.yuzu.yuzu_emu.model.HomeSetting
|
||||||
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||||
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
|
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
|
||||||
|
|
||||||
|
@ -39,6 +41,8 @@ class HomeSettingsFragment : Fragment() {
|
||||||
|
|
||||||
private lateinit var mainActivity: MainActivity
|
private lateinit var mainActivity: MainActivity
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
|
@ -49,6 +53,7 @@ class HomeSettingsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
homeViewModel.setNavigationVisibility(visible = true, animated = false)
|
||||||
mainActivity = requireActivity() as MainActivity
|
mainActivity = requireActivity() as MainActivity
|
||||||
|
|
||||||
val optionsList: List<HomeSetting> = listOf(
|
val optionsList: List<HomeSetting> = listOf(
|
||||||
|
|
|
@ -0,0 +1,222 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.core.widget.doOnTextChanged
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import info.debatty.java.stringsimilarity.Jaccard
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
|
import org.yuzu.yuzu_emu.adapters.GameAdapter
|
||||||
|
import org.yuzu.yuzu_emu.databinding.FragmentSearchBinding
|
||||||
|
import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager
|
||||||
|
import org.yuzu.yuzu_emu.model.Game
|
||||||
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
|
import org.yuzu.yuzu_emu.utils.FileUtil
|
||||||
|
import org.yuzu.yuzu_emu.utils.Log
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class SearchFragment : Fragment() {
|
||||||
|
private var _binding: FragmentSearchBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private lateinit var preferences: SharedPreferences
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SEARCH_TEXT = "SearchText"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentSearchBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
homeViewModel.setNavigationVisibility(visible = true, animated = false)
|
||||||
|
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
|
||||||
|
}
|
||||||
|
|
||||||
|
gamesViewModel.searchFocused.observe(viewLifecycleOwner) { searchFocused ->
|
||||||
|
if (searchFocused) {
|
||||||
|
focusSearch()
|
||||||
|
gamesViewModel.setSearchFocused(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.gridGamesSearch.apply {
|
||||||
|
layoutManager = AutofitGridLayoutManager(
|
||||||
|
requireContext(),
|
||||||
|
requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
|
||||||
|
)
|
||||||
|
adapter = GameAdapter(requireActivity() as AppCompatActivity)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.chipGroup.setOnCheckedStateChangeListener { _, _ -> filterAndSearch() }
|
||||||
|
|
||||||
|
binding.searchText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int ->
|
||||||
|
if (text.toString().isNotEmpty()) {
|
||||||
|
binding.clearButton.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.clearButton.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
filterAndSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
gamesViewModel.games.observe(viewLifecycleOwner) { filterAndSearch() }
|
||||||
|
gamesViewModel.searchedGames.observe(viewLifecycleOwner) {
|
||||||
|
(binding.gridGamesSearch.adapter as GameAdapter).submitList(it)
|
||||||
|
if (it.isEmpty()) {
|
||||||
|
binding.noResultsView.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.noResultsView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.clearButton.setOnClickListener { binding.searchText.setText("") }
|
||||||
|
|
||||||
|
binding.searchBackground.setOnClickListener { focusSearch() }
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
filterAndSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ScoredGame(val score: Double, val item: Game)
|
||||||
|
|
||||||
|
private fun filterAndSearch() {
|
||||||
|
val baseList = gamesViewModel.games.value!!
|
||||||
|
val filteredList: List<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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.chip_recently_added -> {
|
||||||
|
baseList.filter {
|
||||||
|
val addedTime = preferences.getLong(it.keyAddedToLibraryTime, 0L)
|
||||||
|
addedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.chip_homebrew -> {
|
||||||
|
baseList.filter {
|
||||||
|
Log.error("Guh - ${it.path}")
|
||||||
|
FileUtil.hasExtension(it.path, "nro")
|
||||||
|
|| FileUtil.hasExtension(it.path, "nso")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.chip_retail -> baseList.filter {
|
||||||
|
FileUtil.hasExtension(it.path, "xci")
|
||||||
|
|| FileUtil.hasExtension(it.path, "nsp")
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> baseList
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding.searchText.text.toString().isEmpty()
|
||||||
|
&& binding.chipGroup.checkedChipId != View.NO_ID) {
|
||||||
|
gamesViewModel.setSearchedGames(filteredList)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault())
|
||||||
|
val searchAlgorithm = Jaccard(2)
|
||||||
|
val sortedList: List<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, windowInsets: WindowInsetsCompat ->
|
||||||
|
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
|
||||||
|
val navigationSpacing = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
|
||||||
|
val chipSpacing = resources.getDimensionPixelSize(R.dimen.spacing_chip)
|
||||||
|
|
||||||
|
binding.frameSearch.updatePadding(
|
||||||
|
left = insets.left,
|
||||||
|
top = insets.top,
|
||||||
|
right = insets.right
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.gridGamesSearch.setPadding(
|
||||||
|
insets.left,
|
||||||
|
extraListSpacing,
|
||||||
|
insets.right,
|
||||||
|
insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation) + extraListSpacing
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.noResultsView.updatePadding(
|
||||||
|
left = insets.left,
|
||||||
|
right = insets.right,
|
||||||
|
bottom = insets.bottom + navigationSpacing
|
||||||
|
)
|
||||||
|
|
||||||
|
val mlpDivider = binding.divider.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpDivider.leftMargin = insets.left + chipSpacing
|
||||||
|
mlpDivider.rightMargin = insets.right + chipSpacing
|
||||||
|
binding.divider.layoutParams = mlpDivider
|
||||||
|
|
||||||
|
binding.chipGroup.updatePadding(
|
||||||
|
left = insets.left + chipSpacing,
|
||||||
|
right = insets.right + chipSpacing
|
||||||
|
)
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
}
|
|
@ -71,7 +71,7 @@ class SetupFragment : Fragment() {
|
||||||
|
|
||||||
mainActivity = requireActivity() as MainActivity
|
mainActivity = requireActivity() as MainActivity
|
||||||
|
|
||||||
homeViewModel.setNavigationVisibility(false)
|
homeViewModel.setNavigationVisibility(visible = false, animated = false)
|
||||||
|
|
||||||
requireActivity().onBackPressedDispatcher.addCallback(
|
requireActivity().onBackPressedDispatcher.addCallback(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner,
|
||||||
|
|
|
@ -16,6 +16,9 @@ class Game(
|
||||||
val gameId: String,
|
val gameId: String,
|
||||||
val company: String
|
val company: String
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
val keyAddedToLibraryTime get() = "${gameId}_AddedToLibraryTime"
|
||||||
|
val keyLastPlayedTime get() = "${gameId}_LastPlayed"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val extensions: Set<String> = HashSet(
|
val extensions: Set<String> = HashSet(
|
||||||
listOf(".xci", ".nsp", ".nca", ".nro")
|
listOf(".xci", ".nsp", ".nca", ".nro")
|
||||||
|
|
|
@ -29,6 +29,9 @@ class GamesViewModel : ViewModel() {
|
||||||
private val _shouldScrollToTop = MutableLiveData(false)
|
private val _shouldScrollToTop = MutableLiveData(false)
|
||||||
val shouldScrollToTop: LiveData<Boolean> get() = _shouldScrollToTop
|
val shouldScrollToTop: LiveData<Boolean> get() = _shouldScrollToTop
|
||||||
|
|
||||||
|
private val _searchFocused = MutableLiveData(false)
|
||||||
|
val searchFocused: LiveData<Boolean> get() = _searchFocused
|
||||||
|
|
||||||
init {
|
init {
|
||||||
reloadGames(false)
|
reloadGames(false)
|
||||||
}
|
}
|
||||||
|
@ -45,6 +48,10 @@ class GamesViewModel : ViewModel() {
|
||||||
_shouldScrollToTop.postValue(shouldScroll)
|
_shouldScrollToTop.postValue(shouldScroll)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setSearchFocused(searchFocused: Boolean) {
|
||||||
|
_searchFocused.postValue(searchFocused)
|
||||||
|
}
|
||||||
|
|
||||||
fun reloadGames(directoryChanged: Boolean) {
|
fun reloadGames(directoryChanged: Boolean) {
|
||||||
if (isReloading.value == true)
|
if (isReloading.value == true)
|
||||||
return
|
return
|
||||||
|
|
|
@ -5,19 +5,23 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
||||||
class HomeViewModel : ViewModel() {
|
class HomeViewModel : ViewModel() {
|
||||||
private val _navigationVisible = MutableLiveData(true)
|
private val _navigationVisible = MutableLiveData<Pair<Boolean, Boolean>>()
|
||||||
val navigationVisible: LiveData<Boolean> get() = _navigationVisible
|
val navigationVisible: LiveData<Pair<Boolean, Boolean>> get() = _navigationVisible
|
||||||
|
|
||||||
private val _statusBarShadeVisible = MutableLiveData(true)
|
private val _statusBarShadeVisible = MutableLiveData(true)
|
||||||
val statusBarShadeVisible: LiveData<Boolean> get() = _statusBarShadeVisible
|
val statusBarShadeVisible: LiveData<Boolean> get() = _statusBarShadeVisible
|
||||||
|
|
||||||
var navigatedToSetup = false
|
var navigatedToSetup = false
|
||||||
|
|
||||||
fun setNavigationVisibility(visible: Boolean) {
|
init {
|
||||||
if (_navigationVisible.value == visible) {
|
_navigationVisible.value = Pair(false, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
|
||||||
|
if (_navigationVisible.value?.first == visible) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_navigationVisible.value = visible
|
_navigationVisible.value = Pair(visible, animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setStatusBarShadeVisibility(visible: Boolean) {
|
fun setStatusBarShadeVisibility(visible: Boolean) {
|
||||||
|
|
|
@ -52,19 +52,7 @@ class GamesFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
// Use custom back navigation so the user doesn't back out of the app when trying to back
|
homeViewModel.setNavigationVisibility(visible = true, animated = false)
|
||||||
// out of the search view
|
|
||||||
requireActivity().onBackPressedDispatcher.addCallback(
|
|
||||||
viewLifecycleOwner,
|
|
||||||
object : OnBackPressedCallback(true) {
|
|
||||||
override fun handleOnBackPressed() {
|
|
||||||
if (binding.searchView.currentTransitionState == TransitionState.SHOWN) {
|
|
||||||
binding.searchView.hide()
|
|
||||||
} else {
|
|
||||||
requireActivity().finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
binding.gridGames.apply {
|
binding.gridGames.apply {
|
||||||
layoutManager = AutofitGridLayoutManager(
|
layoutManager = AutofitGridLayoutManager(
|
||||||
|
@ -73,7 +61,6 @@ class GamesFragment : Fragment() {
|
||||||
)
|
)
|
||||||
adapter = GameAdapter(requireActivity() as AppCompatActivity)
|
adapter = GameAdapter(requireActivity() as AppCompatActivity)
|
||||||
}
|
}
|
||||||
setUpSearch()
|
|
||||||
|
|
||||||
// Add swipe down to refresh gesture
|
// Add swipe down to refresh gesture
|
||||||
binding.swipeRefresh.setOnRefreshListener {
|
binding.swipeRefresh.setOnRefreshListener {
|
||||||
|
@ -91,21 +78,16 @@ class GamesFragment : Fragment() {
|
||||||
// Watch for when we get updates to any of our games lists
|
// Watch for when we get updates to any of our games lists
|
||||||
gamesViewModel.isReloading.observe(viewLifecycleOwner) { isReloading ->
|
gamesViewModel.isReloading.observe(viewLifecycleOwner) { isReloading ->
|
||||||
binding.swipeRefresh.isRefreshing = isReloading
|
binding.swipeRefresh.isRefreshing = isReloading
|
||||||
|
|
||||||
if (!isReloading) {
|
|
||||||
if (gamesViewModel.games.value!!.isEmpty()) {
|
|
||||||
binding.noticeText.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
binding.noticeText.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
gamesViewModel.games.observe(viewLifecycleOwner) {
|
gamesViewModel.games.observe(viewLifecycleOwner) {
|
||||||
(binding.gridGames.adapter as GameAdapter).submitList(it)
|
(binding.gridGames.adapter as GameAdapter).submitList(it)
|
||||||
|
if (it.isEmpty()) {
|
||||||
|
binding.noticeText.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.noticeText.visibility = View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
gamesViewModel.searchedGames.observe(viewLifecycleOwner) {
|
|
||||||
(binding.gridSearch.adapter as GameAdapter).submitList(it)
|
|
||||||
}
|
|
||||||
gamesViewModel.shouldSwapData.observe(viewLifecycleOwner) { shouldSwapData ->
|
gamesViewModel.shouldSwapData.observe(viewLifecycleOwner) { shouldSwapData ->
|
||||||
if (shouldSwapData) {
|
if (shouldSwapData) {
|
||||||
(binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value)
|
(binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value)
|
||||||
|
@ -113,31 +95,6 @@ class GamesFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide bottom navigation and FAB when using the search view
|
|
||||||
binding.searchView.addTransitionListener { _: SearchView, _: TransitionState, newState: TransitionState ->
|
|
||||||
when (newState) {
|
|
||||||
TransitionState.SHOWING,
|
|
||||||
TransitionState.SHOWN -> {
|
|
||||||
(binding.gridSearch.adapter as GameAdapter).submitList(emptyList())
|
|
||||||
searchShown()
|
|
||||||
}
|
|
||||||
TransitionState.HIDDEN,
|
|
||||||
TransitionState.HIDING -> {
|
|
||||||
gamesViewModel.setSearchedGames(emptyList())
|
|
||||||
searchHidden()
|
|
||||||
binding.appBarSearch.setExpanded(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that bottom navigation or FAB don't appear upon recreation
|
|
||||||
val searchState = binding.searchView.currentTransitionState
|
|
||||||
if (searchState == TransitionState.SHOWN) {
|
|
||||||
searchShown()
|
|
||||||
} else if (searchState == TransitionState.HIDDEN) {
|
|
||||||
searchHidden()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the user reselected the games menu item and then scroll to top of the list
|
// Check if the user reselected the games menu item and then scroll to top of the list
|
||||||
gamesViewModel.shouldScrollToTop.observe(viewLifecycleOwner) { shouldScroll ->
|
gamesViewModel.shouldScrollToTop.observe(viewLifecycleOwner) { shouldScroll ->
|
||||||
if (shouldScroll) {
|
if (shouldScroll) {
|
||||||
|
@ -162,71 +119,24 @@ class GamesFragment : Fragment() {
|
||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchShown() {
|
private fun scrollToTop() {
|
||||||
homeViewModel.setNavigationVisibility(false)
|
|
||||||
homeViewModel.setStatusBarShadeVisibility(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchHidden() {
|
|
||||||
homeViewModel.setNavigationVisibility(true)
|
|
||||||
homeViewModel.setStatusBarShadeVisibility(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class ScoredGame(val score: Double, val item: Game)
|
|
||||||
|
|
||||||
private fun setUpSearch() {
|
|
||||||
binding.gridSearch.apply {
|
|
||||||
layoutManager = AutofitGridLayoutManager(
|
|
||||||
requireContext(),
|
|
||||||
requireContext().resources.getDimensionPixelSize(R.dimen.card_width)
|
|
||||||
)
|
|
||||||
adapter = GameAdapter(requireActivity() as AppCompatActivity)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.searchView.editText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int ->
|
|
||||||
val searchTerm = text.toString().lowercase(Locale.getDefault())
|
|
||||||
val searchAlgorithm = Jaccard(2)
|
|
||||||
val sortedList: List<Game> = gamesViewModel.games.value!!.mapNotNull { game ->
|
|
||||||
val title = game.title.lowercase(Locale.getDefault())
|
|
||||||
val score = searchAlgorithm.similarity(searchTerm, title)
|
|
||||||
if (score > 0.03) {
|
|
||||||
ScoredGame(score, game)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}.sortedByDescending { it.score }.map { it.item }
|
|
||||||
gamesViewModel.setSearchedGames(sortedList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun scrollToTop() {
|
|
||||||
if (_binding != null) {
|
if (_binding != null) {
|
||||||
binding.gridGames.smoothScrollToPosition(0)
|
binding.gridGames.smoothScrollToPosition(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setInsets() =
|
private fun setInsets() =
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(binding.gridGames) { view: View, windowInsets: WindowInsetsCompat ->
|
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat ->
|
||||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med)
|
val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_large)
|
||||||
|
|
||||||
view.updatePadding(
|
binding.gridGames.updatePadding(
|
||||||
top = insets.top + resources.getDimensionPixelSize(R.dimen.spacing_search),
|
top = insets.top + extraListSpacing,
|
||||||
bottom = insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation) + extraListSpacing
|
bottom = insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation) + extraListSpacing
|
||||||
)
|
)
|
||||||
binding.gridSearch.updatePadding(
|
|
||||||
left = insets.left,
|
|
||||||
top = extraListSpacing,
|
|
||||||
right = insets.right,
|
|
||||||
bottom = insets.bottom + extraListSpacing
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.swipeRefresh.setSlingshotDistance(
|
binding.swipeRefresh.setProgressViewEndTarget(
|
||||||
resources.getDimensionPixelSize(R.dimen.spacing_refresh_slingshot)
|
|
||||||
)
|
|
||||||
binding.swipeRefresh.setProgressViewOffset(
|
|
||||||
false,
|
false,
|
||||||
insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_start),
|
|
||||||
insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end)
|
insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup.MarginLayoutParams
|
import android.view.ViewGroup.MarginLayoutParams
|
||||||
|
import android.view.WindowManager
|
||||||
import android.view.animation.PathInterpolator
|
import android.view.animation.PathInterpolator
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
@ -60,6 +61,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
|
||||||
|
|
||||||
window.statusBarColor =
|
window.statusBarColor =
|
||||||
ContextCompat.getColor(applicationContext, android.R.color.transparent)
|
ContextCompat.getColor(applicationContext, android.R.color.transparent)
|
||||||
|
@ -75,26 +77,30 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
|
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
|
||||||
setUpNavigation(navHostFragment.navController)
|
setUpNavigation(navHostFragment.navController)
|
||||||
(binding.navigationBar as NavigationBarView).setOnItemReselectedListener {
|
(binding.navigationBar as NavigationBarView).setOnItemReselectedListener {
|
||||||
if (it.itemId == R.id.gamesFragment) {
|
when (it.itemId) {
|
||||||
gamesViewModel.setShouldScrollToTop(true)
|
R.id.gamesFragment -> gamesViewModel.setShouldScrollToTop(true)
|
||||||
|
R.id.searchFragment -> gamesViewModel.setSearchFocused(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.statusBarShade.setBackgroundColor(
|
binding.statusBarShade.setBackgroundColor(
|
||||||
MaterialColors.getColor(
|
ThemeHelper.getColorWithOpacity(
|
||||||
binding.root,
|
MaterialColors.getColor(
|
||||||
R.attr.colorSurface
|
binding.root,
|
||||||
|
R.attr.colorSurface
|
||||||
|
),
|
||||||
|
ThemeHelper.SYSTEM_BAR_ALPHA
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Prevents navigation from being drawn for a short time on recreation if set to hidden
|
// Prevents navigation from being drawn for a short time on recreation if set to hidden
|
||||||
if (homeViewModel.navigationVisible.value == false) {
|
if (!homeViewModel.navigationVisible.value?.first!!) {
|
||||||
binding.navigationBar.visibility = View.INVISIBLE
|
binding.navigationBar.visibility = View.INVISIBLE
|
||||||
binding.statusBarShade.visibility = View.INVISIBLE
|
binding.statusBarShade.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
homeViewModel.navigationVisible.observe(this) { visible ->
|
homeViewModel.navigationVisible.observe(this) {
|
||||||
showNavigation(visible)
|
showNavigation(it.first, it.second)
|
||||||
}
|
}
|
||||||
homeViewModel.statusBarShadeVisible.observe(this) { visible ->
|
homeViewModel.statusBarShadeVisible.observe(this) { visible ->
|
||||||
showStatusBarShade(visible)
|
showStatusBarShade(visible)
|
||||||
|
@ -109,7 +115,7 @@ 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.navigationBar.setupWithNavController(navController)
|
binding.navigationBar.setupWithNavController(navController)
|
||||||
showNavigation(true)
|
showNavigation(visible = true, animated = true)
|
||||||
|
|
||||||
ThemeHelper.setNavigationBarColor(
|
ThemeHelper.setNavigationBarColor(
|
||||||
this,
|
this,
|
||||||
|
@ -132,7 +138,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showNavigation(visible: Boolean) {
|
private fun showNavigation(visible: Boolean, animated: Boolean) {
|
||||||
|
if (!animated) {
|
||||||
|
if (visible) {
|
||||||
|
binding.navigationBar.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.navigationBar.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
binding.navigationBar.animate().apply {
|
binding.navigationBar.animate().apply {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
binding.navigationBar.visibility = View.VISIBLE
|
binding.navigationBar.visibility = View.VISIBLE
|
||||||
|
@ -196,10 +211,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
themeId = resId
|
themeId = resId
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hasExtension(path: String, extension: String): Boolean {
|
|
||||||
return path.substring(path.lastIndexOf(".") + 1).contains(extension)
|
|
||||||
}
|
|
||||||
|
|
||||||
val getGamesDirectory =
|
val getGamesDirectory =
|
||||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
||||||
if (result == null)
|
if (result == null)
|
||||||
|
@ -232,7 +243,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
if (result == null)
|
if (result == null)
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
|
|
||||||
if (!hasExtension(result.toString(), "keys")) {
|
if (!FileUtil.hasExtension(result.toString(), "keys")) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
R.string.invalid_keys_file,
|
R.string.invalid_keys_file,
|
||||||
|
@ -278,7 +289,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
if (result == null)
|
if (result == null)
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
|
|
||||||
if (!hasExtension(result.toString(), "bin")) {
|
if (!FileUtil.hasExtension(result.toString(), "bin")) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
R.string.invalid_keys_file,
|
R.string.invalid_keys_file,
|
||||||
|
|
|
@ -292,4 +292,8 @@ object FileUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hasExtension(path: String, extension: String): Boolean {
|
||||||
|
return path.substring(path.lastIndexOf(".") + 1).contains(extension)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.utils
|
package org.yuzu.yuzu_emu.utils
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import org.yuzu.yuzu_emu.NativeLibrary
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
|
@ -14,12 +15,15 @@ import kotlin.collections.ArrayList
|
||||||
object GameHelper {
|
object GameHelper {
|
||||||
const val KEY_GAME_PATH = "game_path"
|
const val KEY_GAME_PATH = "game_path"
|
||||||
|
|
||||||
|
private lateinit var preferences: SharedPreferences
|
||||||
|
|
||||||
fun getGames(): ArrayList<Game> {
|
fun getGames(): ArrayList<Game> {
|
||||||
val games = ArrayList<Game>()
|
val games = ArrayList<Game>()
|
||||||
val context = YuzuApplication.appContext
|
val context = YuzuApplication.appContext
|
||||||
val gamesDir =
|
val gamesDir =
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "")
|
PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "")
|
||||||
val gamesUri = Uri.parse(gamesDir)
|
val gamesUri = Uri.parse(gamesDir)
|
||||||
|
preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
// 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()
|
||||||
|
@ -60,7 +64,7 @@ object GameHelper {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Game(
|
val newGame = Game(
|
||||||
name,
|
name,
|
||||||
NativeLibrary.getDescription(filePath).replace("\n", " "),
|
NativeLibrary.getDescription(filePath).replace("\n", " "),
|
||||||
NativeLibrary.getRegions(filePath),
|
NativeLibrary.getRegions(filePath),
|
||||||
|
@ -68,5 +72,14 @@ object GameHelper {
|
||||||
gameId,
|
gameId,
|
||||||
NativeLibrary.getCompany(filePath)
|
NativeLibrary.getCompany(filePath)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val addedTime = preferences.getLong(newGame.keyAddedToLibraryTime, 0L)
|
||||||
|
if (addedTime == 0L) {
|
||||||
|
preferences.edit()
|
||||||
|
.putLong(newGame.keyAddedToLibraryTime, System.currentTimeMillis())
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
return newGame
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
9
src/android/app/src/main/res/drawable/ic_clear.xml
Normal file
9
src/android/app/src/main/res/drawable/ic_clear.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="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
|
||||||
|
</vector>
|
9
src/android/app/src/main/res/drawable/ic_search.xml
Normal file
9
src/android/app/src/main/res/drawable/ic_search.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="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
|
||||||
|
</vector>
|
|
@ -29,6 +29,7 @@
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:menu="@menu/menu_navigation"
|
app:menu="@menu/menu_navigation"
|
||||||
|
app:labelVisibilityMode="selected"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
|
|
@ -1,74 +1,34 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/coordinator_main"
|
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">
|
||||||
|
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
<RelativeLayout
|
||||||
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:clipToPadding="false"
|
|
||||||
app:layout_behavior="@string/searchbar_scrolling_view_behavior">
|
|
||||||
|
|
||||||
<RelativeLayout
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/notice_text"
|
||||||
|
style="@style/TextAppearance.Material3.BodyLarge"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
<com.google.android.material.textview.MaterialTextView
|
android:padding="@dimen/spacing_large"
|
||||||
android:id="@+id/notice_text"
|
android:text="@string/empty_gamelist"
|
||||||
style="@style/TextAppearance.Material3.BodyLarge"
|
tools:visibility="gone" />
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:padding="@dimen/spacing_large"
|
|
||||||
android:text="@string/empty_gamelist"
|
|
||||||
tools:visibility="gone" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/grid_games"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
tools:listitem="@layout/card_game" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
|
||||||
android:id="@+id/app_bar_search"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:fitsSystemWindows="true"
|
|
||||||
app:liftOnScrollTargetViewId="@id/grid_games">
|
|
||||||
|
|
||||||
<com.google.android.material.search.SearchBar
|
|
||||||
android:id="@+id/search_bar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/home_search_games" />
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.search.SearchView
|
|
||||||
android:id="@+id/search_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:hint="@string/home_search_games"
|
|
||||||
app:layout_anchor="@id/search_bar">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/grid_search"
|
android:id="@+id/grid_games"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
tools:listitem="@layout/card_game" />
|
tools:listitem="@layout/card_game" />
|
||||||
|
|
||||||
</com.google.android.material.search.SearchView>
|
</RelativeLayout>
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
180
src/android/app/src/main/res/layout/fragment_search.xml
Normal file
180
src/android/app/src/main/res/layout/fragment_search.xml
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
<?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:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/colorSurface">
|
||||||
|
|
||||||
|
<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_margin="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: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>
|
|
@ -6,6 +6,11 @@
|
||||||
android:icon="@drawable/ic_controller"
|
android:icon="@drawable/ic_controller"
|
||||||
android:title="@string/home_games" />
|
android:title="@string/home_games" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/searchFragment"
|
||||||
|
android:icon="@drawable/ic_search"
|
||||||
|
android:title="@string/home_search" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/homeSettingsFragment"
|
android:id="@+id/homeSettingsFragment"
|
||||||
android:icon="@drawable/ic_settings"
|
android:icon="@drawable/ic_settings"
|
||||||
|
|
|
@ -25,4 +25,9 @@
|
||||||
app:popUpToInclusive="true" />
|
app:popUpToInclusive="true" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/searchFragment"
|
||||||
|
android:name="org.yuzu.yuzu_emu.fragments.SearchFragment"
|
||||||
|
android:label="SearchFragment" />
|
||||||
|
|
||||||
</navigation>
|
</navigation>
|
||||||
|
|
|
@ -5,11 +5,10 @@
|
||||||
<dimen name="spacing_large">16dp</dimen>
|
<dimen name="spacing_large">16dp</dimen>
|
||||||
<dimen name="spacing_xtralarge">32dp</dimen>
|
<dimen name="spacing_xtralarge">32dp</dimen>
|
||||||
<dimen name="spacing_list">64dp</dimen>
|
<dimen name="spacing_list">64dp</dimen>
|
||||||
|
<dimen name="spacing_chip">20dp</dimen>
|
||||||
<dimen name="spacing_navigation">80dp</dimen>
|
<dimen name="spacing_navigation">80dp</dimen>
|
||||||
<dimen name="spacing_search">88dp</dimen>
|
<dimen name="spacing_search">128dp</dimen>
|
||||||
<dimen name="spacing_refresh_slingshot">80dp</dimen>
|
<dimen name="spacing_refresh_end">72dp</dimen>
|
||||||
<dimen name="spacing_refresh_start">32dp</dimen>
|
|
||||||
<dimen name="spacing_refresh_end">96dp</dimen>
|
|
||||||
<dimen name="menu_width">256dp</dimen>
|
<dimen name="menu_width">256dp</dimen>
|
||||||
<dimen name="card_width">165dp</dimen>
|
<dimen name="card_width">165dp</dimen>
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,10 @@
|
||||||
|
|
||||||
<!-- Home strings -->
|
<!-- Home strings -->
|
||||||
<string name="home_games">Games</string>
|
<string name="home_games">Games</string>
|
||||||
|
<string name="home_search">Search</string>
|
||||||
<string name="home_settings">Settings</string>
|
<string name="home_settings">Settings</string>
|
||||||
|
<string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
|
||||||
|
<string name="search_and_filter_games">Search and filter games</string>
|
||||||
<string name="select_games_folder">Select games folder</string>
|
<string name="select_games_folder">Select games folder</string>
|
||||||
<string name="select_games_folder_description">Allows yuzu to populate the games list</string>
|
<string name="select_games_folder_description">Allows yuzu to populate the games list</string>
|
||||||
<string name="add_games_warning">Skip selecting games folder?</string>
|
<string name="add_games_warning">Skip selecting games folder?</string>
|
||||||
|
@ -58,6 +61,10 @@
|
||||||
<string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
|
<string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
|
||||||
<string name="advanced_settings">Advanced settings</string>
|
<string name="advanced_settings">Advanced settings</string>
|
||||||
<string name="settings_description">Configure emulator settings</string>
|
<string name="settings_description">Configure emulator settings</string>
|
||||||
|
<string name="search_recently_played">Recently Played</string>
|
||||||
|
<string name="search_recently_added">Recently Added</string>
|
||||||
|
<string name="search_retail">Retail</string>
|
||||||
|
<string name="search_homebrew">Homebrew</string>
|
||||||
<string name="open_user_folder">Open yuzu folder</string>
|
<string name="open_user_folder">Open yuzu folder</string>
|
||||||
<string name="open_user_folder_description">Manage yuzu\'s internal files</string>
|
<string name="open_user_folder_description">Manage yuzu\'s internal files</string>
|
||||||
<string name="no_file_manager">No file manager found</string>
|
<string name="no_file_manager">No file manager found</string>
|
||||||
|
@ -151,8 +158,6 @@
|
||||||
|
|
||||||
<string name="load_settings">Loading Settings…</string>
|
<string name="load_settings">Loading Settings…</string>
|
||||||
|
|
||||||
<string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
|
|
||||||
|
|
||||||
<!-- Software keyboard -->
|
<!-- Software keyboard -->
|
||||||
<string name="software_keyboard">Software Keyboard</string>
|
<string name="software_keyboard">Software Keyboard</string>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue