diff options
author | Charles Lombardo <clombardo169@gmail.com> | 2023-04-06 02:26:53 +0200 |
---|---|---|
committer | bunnei <bunneidev@gmail.com> | 2023-06-03 09:05:51 +0200 |
commit | 233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38 (patch) | |
tree | a9e82ac5af26b935ca0d5aeb9e7be9f780667369 /src/android/app | |
parent | android: Enforce Vulkan 1.1 support as minimum (diff) | |
download | yuzu-233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38.tar yuzu-233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38.tar.gz yuzu-233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38.tar.bz2 yuzu-233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38.tar.lz yuzu-233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38.tar.xz yuzu-233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38.tar.zst yuzu-233ae9ab691f2e6fe65d97fdd58b2ac6e015ad38.zip |
Diffstat (limited to 'src/android/app')
32 files changed, 1031 insertions, 626 deletions
diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 552d4a721..d8ef02ac1 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -155,6 +155,9 @@ dependencies { implementation("org.ini4j:ini4j:0.5.4") implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") + implementation("androidx.navigation:navigation-fragment-ktx:2.5.3") + implementation("androidx.navigation:navigation-ui-ktx:2.5.3") + implementation("info.debatty:java-string-similarity:2.0.0") } fun getVersion(): String { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt index f1f92841c..fd174fd2d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt @@ -13,7 +13,6 @@ import android.view.View import android.view.WindowManager import android.view.inputmethod.InputMethodManager import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.slider.Slider.OnChangeListener @@ -202,7 +201,7 @@ open class EmulationActivity : AppCompatActivity() { private const val EMULATION_RUNNING_NOTIFICATION = 0x1000 @JvmStatic - fun launch(activity: FragmentActivity, game: Game) { + fun launch(activity: AppCompatActivity, game: Game) { val launcher = Intent(activity, EmulationActivity::class.java) launcher.putExtra(EXTRA_SELECTED_GAME, game) activity.startActivity(launcher) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt index af83f05c1..1102b60b1 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt @@ -3,6 +3,7 @@ package org.yuzu.yuzu_emu.adapters +import android.annotation.SuppressLint import android.graphics.Bitmap import android.graphics.BitmapFactory import android.view.LayoutInflater @@ -11,29 +12,25 @@ import android.view.ViewGroup import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import coil.load -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.CardGameBinding import org.yuzu.yuzu_emu.activities.EmulationActivity import org.yuzu.yuzu_emu.model.Game -import kotlin.collections.ArrayList - -/** - * This adapter gets its information from a database Cursor. This fact, paired with the usage of - * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly) - * large dataset. - */ -class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<Game>) : - RecyclerView.Adapter<GameAdapter.GameViewHolder>(), +import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder + +class GameAdapter(private val activity: AppCompatActivity) : + ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()), View.OnClickListener { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { // Create a new view. - val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context)) + val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) binding.root.setOnClickListener(this) // Use that view to create a ViewHolder. @@ -41,12 +38,10 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList< } override fun onBindViewHolder(holder: GameViewHolder, position: Int) { - holder.bind(games[position]) + holder.bind(currentList[position]) } - override fun getItemCount(): Int { - return games.size - } + override fun getItemCount(): Int = currentList.size /** * Launches the game that was clicked on. @@ -55,7 +50,7 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList< */ override fun onClick(view: View) { val holder = view.tag as GameViewHolder - EmulationActivity.launch((view.context as AppCompatActivity), holder.game) + EmulationActivity.launch(activity, holder.game) } inner class GameViewHolder(val binding: CardGameBinding) : @@ -74,7 +69,6 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList< val bitmap = decodeGameIcon(game.path) binding.imageGameScreen.load(bitmap) { error(R.drawable.no_icon) - crossfade(true) } } @@ -87,9 +81,15 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList< } } - fun swapData(games: ArrayList<Game>) { - this.games = games - notifyDataSetChanged() + private class DiffCallback : DiffUtil.ItemCallback<Game>() { + override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean { + return oldItem.gameId == newItem.gameId + } + + @SuppressLint("DiffUtilEquals") + override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean { + return oldItem == newItem + } } private fun decodeGameIcon(uri: String): Bitmap? { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeOptionAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeOptionAdapter.kt new file mode 100644 index 000000000..2bec2de87 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeOptionAdapter.kt @@ -0,0 +1,55 @@ +package org.yuzu.yuzu_emu.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.res.ResourcesCompat +import androidx.recyclerview.widget.RecyclerView +import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding +import org.yuzu.yuzu_emu.model.HomeOption + +class HomeOptionAdapter(private val activity: AppCompatActivity, var options: List<HomeOption>) : + RecyclerView.Adapter<HomeOptionAdapter.HomeOptionViewHolder>(), + View.OnClickListener { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder { + val binding = CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) + binding.root.setOnClickListener(this) + return HomeOptionViewHolder(binding) + } + + override fun getItemCount(): Int { + return options.size + } + + override fun onBindViewHolder(holder: HomeOptionViewHolder, position: Int) { + holder.bind(options[position]) + } + + override fun onClick(view: View) { + val holder = view.tag as HomeOptionViewHolder + holder.option.onClick.invoke() + } + + inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) : + RecyclerView.ViewHolder(binding.root) { + lateinit var option: HomeOption + + init { + itemView.tag = this + } + + fun bind(option: HomeOption) { + this.option = option + binding.optionTitle.text = activity.resources.getString(option.titleId) + binding.optionDescription.text = activity.resources.getString(option.descriptionId) + binding.optionIcon.setImageDrawable( + ResourcesCompat.getDrawable( + activity.resources, + option.iconId, + activity.theme + ) + ) + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt index 0f2c23827..e4bdcc991 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt @@ -15,6 +15,7 @@ import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding +import com.google.android.material.color.MaterialColors import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding @@ -50,6 +51,11 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView { setSupportActionBar(binding.toolbarSettings) supportActionBar!!.setDisplayHomeAsUpEnabled(true) + ThemeHelper.setNavigationBarColor( + this, + MaterialColors.getColor(window.decorView, R.attr.colorSurface) + ) + setInsets() } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt new file mode 100644 index 000000000..dac9e67d5 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt @@ -0,0 +1,281 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.content.DialogInterface +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.adapters.HomeOptionAdapter +import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding +import org.yuzu.yuzu_emu.databinding.FragmentOptionsBinding +import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity +import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile +import org.yuzu.yuzu_emu.model.GamesViewModel +import org.yuzu.yuzu_emu.model.HomeOption +import org.yuzu.yuzu_emu.utils.DirectoryInitialization +import org.yuzu.yuzu_emu.utils.FileUtil +import org.yuzu.yuzu_emu.utils.GameHelper +import org.yuzu.yuzu_emu.utils.GpuDriverHelper +import java.io.IOException + +class OptionsFragment : Fragment() { + private var _binding: FragmentOptionsBinding? = null + private val binding get() = _binding!! + + private val gamesViewModel: GamesViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentOptionsBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val optionsList: List<HomeOption> = listOf( + HomeOption( + R.string.add_games, + R.string.add_games_description, + R.drawable.ic_add + ) { getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) }, + HomeOption( + R.string.install_prod_keys, + R.string.install_prod_keys_description, + R.drawable.ic_unlock + ) { getProdKey.launch(arrayOf("*/*")) }, + HomeOption( + R.string.install_amiibo_keys, + R.string.install_amiibo_keys_description, + R.drawable.ic_nfc + ) { getAmiiboKey.launch(arrayOf("*/*")) }, + HomeOption( + R.string.install_gpu_driver, + R.string.install_gpu_driver_description, + R.drawable.ic_input + ) { driverInstaller() }, + HomeOption( + R.string.settings, + R.string.settings_description, + R.drawable.ic_settings + ) { SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") } + ) + + binding.optionsList.apply { + layoutManager = LinearLayoutManager(requireContext()) + adapter = HomeOptionAdapter(requireActivity() as AppCompatActivity, optionsList) + } + + requireActivity().window.statusBarColor = ThemeHelper.getColorWithOpacity( + MaterialColors.getColor( + binding.root, + R.attr.colorSurface + ), ThemeHelper.SYSTEM_BAR_ALPHA + ) + + setInsets() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun driverInstaller() { + // Get the driver name for the dialog message. + var driverName = GpuDriverHelper.customDriverName + if (driverName == null) { + driverName = getString(R.string.system_gpu_driver) + } + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.select_gpu_driver_title)) + .setMessage(driverName) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.select_gpu_driver_default) { _: DialogInterface?, _: Int -> + GpuDriverHelper.installDefaultDriver(requireContext()) + Toast.makeText( + requireContext(), + R.string.select_gpu_driver_use_default, + Toast.LENGTH_SHORT + ).show() + } + .setNeutralButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int -> + getDriver.launch(arrayOf("application/zip")) + } + .show() + } + + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener(binding.scrollViewOptions) { view: View, windowInsets: WindowInsetsCompat -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + view.setPadding( + insets.left, + insets.top, + insets.right, + insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation) + ) + windowInsets + } + + private val getGamesDirectory = + registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> + if (result == null) + return@registerForActivityResult + + val takeFlags = + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION + requireActivity().contentResolver.takePersistableUriPermission( + result, + takeFlags + ) + + // When a new directory is picked, we currently will reset the existing games + // database. This effectively means that only one game directory is supported. + PreferenceManager.getDefaultSharedPreferences(requireContext()).edit() + .putString(GameHelper.KEY_GAME_PATH, result.toString()) + .apply() + + gamesViewModel.reloadGames(true) + } + + private val getProdKey = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result == null) + return@registerForActivityResult + + val takeFlags = + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION + requireActivity().contentResolver.takePersistableUriPermission( + result, + takeFlags + ) + + val dstPath = DirectoryInitialization.userDirectory + "/keys/" + if (FileUtil.copyUriToInternalStorage(requireContext(), result, dstPath, "prod.keys")) { + if (NativeLibrary.reloadKeys()) { + Toast.makeText( + requireContext(), + R.string.install_keys_success, + Toast.LENGTH_SHORT + ).show() + gamesViewModel.reloadGames(true) + } else { + Toast.makeText( + requireContext(), + R.string.install_keys_failure, + Toast.LENGTH_LONG + ).show() + } + } + } + + private val getAmiiboKey = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result == null) + return@registerForActivityResult + + val takeFlags = + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION + requireActivity().contentResolver.takePersistableUriPermission( + result, + takeFlags + ) + + val dstPath = DirectoryInitialization.userDirectory + "/keys/" + if (FileUtil.copyUriToInternalStorage( + requireContext(), + result, + dstPath, + "key_retail.bin" + ) + ) { + if (NativeLibrary.reloadKeys()) { + Toast.makeText( + requireContext(), + R.string.install_keys_success, + Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText( + requireContext(), + R.string.install_amiibo_keys_failure, + Toast.LENGTH_LONG + ).show() + } + } + } + + private val getDriver = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result == null) + return@registerForActivityResult + + val takeFlags = + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION + requireActivity().contentResolver.takePersistableUriPermission( + result, + takeFlags + ) + + val progressBinding = DialogProgressBarBinding.inflate(layoutInflater) + progressBinding.progressBar.isIndeterminate = true + val installationDialog = MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.installing_driver) + .setView(progressBinding.root) + .show() + + lifecycleScope.launch { + withContext(Dispatchers.IO) { + // Ignore file exceptions when a user selects an invalid zip + try { + GpuDriverHelper.installCustomDriver(requireContext(), result) + } catch (_: IOException) { + } + + withContext(Dispatchers.Main) { + installationDialog.dismiss() + + val driverName = GpuDriverHelper.customDriverName + if (driverName != null) { + Toast.makeText( + requireContext(), + getString( + R.string.select_gpu_driver_install_success, + driverName + ), + Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText( + requireContext(), + R.string.select_gpu_driver_error, + Toast.LENGTH_LONG + ).show() + } + } + } + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt index fde99f1a2..709a5b976 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt @@ -1,18 +1,58 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + package org.yuzu.yuzu_emu.model import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.utils.GameHelper class GamesViewModel : ViewModel() { - private val _games = MutableLiveData<ArrayList<Game>>() - val games: LiveData<ArrayList<Game>> get() = _games + private val _games = MutableLiveData<List<Game>>(emptyList()) + val games: LiveData<List<Game>> get() = _games + + private val _searchedGames = MutableLiveData<List<Game>>(emptyList()) + val searchedGames: LiveData<List<Game>> get() = _searchedGames + + private val _isReloading = MutableLiveData(false) + val isReloading: LiveData<Boolean> get() = _isReloading + + private val _shouldSwapData = MutableLiveData(false) + val shouldSwapData: LiveData<Boolean> get() = _shouldSwapData init { - _games.value = ArrayList() + reloadGames(false) + } + + fun setSearchedGames(games: List<Game>) { + _searchedGames.postValue(games) + } + + fun setShouldSwapData(shouldSwap: Boolean) { + _shouldSwapData.postValue(shouldSwap) } - fun setGames(games: ArrayList<Game>) { - _games.value = games + fun reloadGames(directoryChanged: Boolean) { + if (isReloading.value == true) + return + _isReloading.postValue(true) + + viewModelScope.launch { + withContext(Dispatchers.IO) { + NativeLibrary.resetRomMetadata() + _games.postValue(GameHelper.getGames()) + _isReloading.postValue(false) + + if (directoryChanged) { + setShouldSwapData(true) + } + } + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeOption.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeOption.kt new file mode 100644 index 000000000..c995ff12c --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeOption.kt @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.model + +data class HomeOption( + val titleId: Int, + val descriptionId: Int, + val iconId: Int, + val onClick: () -> Unit +) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt new file mode 100644 index 000000000..74f12429c --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt @@ -0,0 +1,17 @@ +package org.yuzu.yuzu_emu.model + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel + +class HomeViewModel : ViewModel() { + private val _navigationVisible = MutableLiveData(true) + val navigationVisible: LiveData<Boolean> get() = _navigationVisible + + fun setNavigationVisible(visible: Boolean) { + if (_navigationVisible.value == visible) { + return + } + _navigationVisible.value = visible + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt new file mode 100644 index 000000000..0c609798b --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt @@ -0,0 +1,220 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +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 com.google.android.material.color.MaterialColors +import com.google.android.material.search.SearchView +import com.google.android.material.search.SearchView.TransitionState +import info.debatty.java.stringsimilarity.Jaccard +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.adapters.GameAdapter +import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding +import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager +import org.yuzu.yuzu_emu.model.Game +import org.yuzu.yuzu_emu.model.GamesViewModel +import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.utils.ThemeHelper +import java.util.Locale + +class GamesFragment : Fragment() { + private var _binding: FragmentGamesBinding? = null + private val binding get() = _binding!! + + private val gamesViewModel: GamesViewModel by activityViewModels() + private val homeViewModel: HomeViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentGamesBinding.inflate(inflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + // Use custom back navigation so the user doesn't back out of the app when trying to back + // out of the search view + requireActivity().onBackPressedDispatcher.addCallback( + viewLifecycleOwner, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (binding.searchView.currentTransitionState == TransitionState.SHOWN) { + binding.searchView.hide() + } else { + requireActivity().finish() + } + } + }) + + binding.gridGames.apply { + layoutManager = AutofitGridLayoutManager( + requireContext(), + requireContext().resources.getDimensionPixelSize(R.dimen.card_width) + ) + adapter = GameAdapter(requireActivity() as AppCompatActivity) + } + setUpSearch() + + // Add swipe down to refresh gesture + binding.swipeRefresh.setOnRefreshListener { + gamesViewModel.reloadGames(false) + } + + // Set theme color to the refresh animation's background + binding.swipeRefresh.setProgressBackgroundColorSchemeColor( + MaterialColors.getColor(binding.swipeRefresh, R.attr.colorPrimary) + ) + binding.swipeRefresh.setColorSchemeColors( + MaterialColors.getColor(binding.swipeRefresh, R.attr.colorOnPrimary) + ) + + // Watch for when we get updates to any of our games lists + gamesViewModel.isReloading.observe(viewLifecycleOwner) { isReloading -> + binding.swipeRefresh.isRefreshing = isReloading + + if (!isReloading) { + if (gamesViewModel.games.value!!.isEmpty()) { + binding.noticeText.visibility = View.VISIBLE + } else { + binding.noticeText.visibility = View.GONE + } + } + } + gamesViewModel.games.observe(viewLifecycleOwner) { + (binding.gridGames.adapter as GameAdapter).submitList(it) + } + gamesViewModel.searchedGames.observe(viewLifecycleOwner) { + (binding.gridSearch.adapter as GameAdapter).submitList(it) + } + gamesViewModel.shouldSwapData.observe(viewLifecycleOwner) { shouldSwapData -> + if (shouldSwapData) { + (binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value) + gamesViewModel.setShouldSwapData(false) + } + } + + // 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() + } + } + } + + // 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() + } + + setInsets() + + // Make sure the loading indicator appears even if the layout is told to refresh before being fully drawn + binding.swipeRefresh.post { + binding.swipeRefresh.isRefreshing = gamesViewModel.isReloading.value!! + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun searchShown() { + homeViewModel.setNavigationVisible(false) + requireActivity().window.statusBarColor = + ContextCompat.getColor(requireContext(), android.R.color.transparent) + } + + private fun searchHidden() { + homeViewModel.setNavigationVisible(true) + requireActivity().window.statusBarColor = ThemeHelper.getColorWithOpacity( + MaterialColors.getColor( + binding.root, + R.attr.colorSurface + ), ThemeHelper.SYSTEM_BAR_ALPHA + ) + } + + 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) + } + } + + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener(binding.gridGames) { view: View, windowInsets: WindowInsetsCompat -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med) + + view.setPadding( + insets.left, + insets.top + resources.getDimensionPixelSize(R.dimen.spacing_search), + insets.right, + insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation) + extraListSpacing + ) + binding.gridSearch.updatePadding( + left = insets.left, + top = extraListSpacing, + right = insets.right, + bottom = insets.bottom + extraListSpacing + ) + + binding.swipeRefresh.setSlingshotDistance( + resources.getDimensionPixelSize(R.dimen.spacing_refresh_slingshot) + ) + binding.swipeRefresh.setProgressViewOffset( + false, + insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_start), + insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end) + ) + + windowInsets + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 69a371947..a16ca8529 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -3,42 +3,31 @@ package org.yuzu.yuzu_emu.ui.main -import android.content.DialogInterface -import android.content.Intent import android.os.Bundle -import android.view.Menu -import android.view.MenuItem import android.view.View -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts +import android.view.ViewGroup.MarginLayoutParams +import android.view.animation.PathInterpolator +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updatePadding -import androidx.lifecycle.lifecycleScope -import androidx.preference.PreferenceManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.yuzu.yuzu_emu.NativeLibrary +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.setupWithNavController +import com.google.android.material.color.MaterialColors +import com.google.android.material.elevation.ElevationOverlayProvider import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.activities.EmulationActivity import org.yuzu.yuzu_emu.databinding.ActivityMainBinding -import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding -import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity -import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment +import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.utils.* -import java.io.IOException - -class MainActivity : AppCompatActivity(), MainView { - private var platformGamesFragment: PlatformGamesFragment? = null - private val presenter = MainPresenter(this) +class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding + private val homeViewModel: HomeViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { val splashScreen = installSplashScreen() splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } @@ -52,19 +41,36 @@ class MainActivity : AppCompatActivity(), MainView { WindowCompat.setDecorFitsSystemWindows(window, false) - setSupportActionBar(binding.toolbarMain) - presenter.onCreate() - if (savedInstanceState == null) { - StartupHandler.handleInit(this) - platformGamesFragment = PlatformGamesFragment() - supportFragmentManager.beginTransaction() - .add(R.id.games_platform_frame, platformGamesFragment!!) - .commit() - } else { - platformGamesFragment = supportFragmentManager.getFragment( - savedInstanceState, - PlatformGamesFragment.TAG - ) as PlatformGamesFragment? + ThemeHelper.setNavigationBarColor( + this, + ElevationOverlayProvider(binding.navigationBar.context).compositeOverlay( + MaterialColors.getColor(binding.navigationBar, R.attr.colorSurface), + binding.navigationBar.elevation + ) + ) + + // Set up a central host fragment that is controlled via bottom navigation with xml navigation + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment + binding.navigationBar.setupWithNavController(navHostFragment.navController) + + binding.statusBarShade.setBackgroundColor( + ThemeHelper.getColorWithOpacity( + MaterialColors.getColor( + binding.root, + R.attr.colorSurface + ), ThemeHelper.SYSTEM_BAR_ALPHA + ) + ) + + // Prevents navigation from being drawn for a short time on recreation if set to hidden + if (homeViewModel.navigationVisible.value == false) { + binding.navigationBar.visibility = View.INVISIBLE + binding.statusBarShade.visibility = View.INVISIBLE + } + + homeViewModel.navigationVisible.observe(this) { visible -> + showNavigation(visible) } // Dismiss previous notifications (should not happen unless a crash occurred) @@ -73,78 +79,24 @@ class MainActivity : AppCompatActivity(), MainView { setInsets() } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - supportFragmentManager.putFragment( - outState, - PlatformGamesFragment.TAG, - platformGamesFragment!! - ) - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_game_grid, menu) - return true - } - - /** - * MainView - */ - override fun setVersionString(version: String) { - binding.toolbarMain.subtitle = version - } - - override fun launchSettingsActivity(menuTag: String) { - SettingsActivity.launch(this, menuTag, "") - } - - override fun launchFileListActivity(request: Int) { - when (request) { - MainPresenter.REQUEST_ADD_DIRECTORY -> getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) - MainPresenter.REQUEST_INSTALL_KEYS -> getProdKey.launch(arrayOf("*/*")) - MainPresenter.REQUEST_INSTALL_AMIIBO_KEYS -> getAmiiboKey.launch(arrayOf("*/*")) - MainPresenter.REQUEST_SELECT_GPU_DRIVER -> { - // Get the driver name for the dialog message. - var driverName = GpuDriverHelper.customDriverName - if (driverName == null) { - driverName = getString(R.string.system_gpu_driver) - } - - MaterialAlertDialogBuilder(this) - .setTitle(getString(R.string.select_gpu_driver_title)) - .setMessage(driverName) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.select_gpu_driver_default) { _: DialogInterface?, _: Int -> - GpuDriverHelper.installDefaultDriver(this) - Toast.makeText( - this, - R.string.select_gpu_driver_use_default, - Toast.LENGTH_SHORT - ).show() - } - .setNeutralButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int -> - getDriver.launch(arrayOf("application/zip")) - } - .show() + private fun showNavigation(visible: Boolean) { + binding.navigationBar.animate().apply { + if (visible) { + binding.navigationBar.visibility = View.VISIBLE + binding.navigationBar.translationY = binding.navigationBar.height.toFloat() * 2 + duration = 300 + translationY(0f) + interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f) + } else { + duration = 300 + translationY(binding.navigationBar.height.toFloat() * 2) + interpolator = PathInterpolator(0.3f, 0f, 0.8f, 0.15f) } - } - } - - /** - * Called by the framework whenever any actionbar/toolbar icon is clicked. - * - * @param item The icon that was clicked on. - * @return True if the event was handled, false to bubble it up to the OS. - */ - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return presenter.handleOptionSelection(item.itemId) - } - - private fun refreshFragment() { - if (platformGamesFragment != null) { - NativeLibrary.resetRomMetadata() - platformGamesFragment!!.refresh() - } + }.withEndAction { + if (!visible) { + binding.navigationBar.visibility = View.INVISIBLE + } + }.start() } override fun onDestroy() { @@ -152,145 +104,12 @@ class MainActivity : AppCompatActivity(), MainView { super.onDestroy() } - private fun setInsets() { - ViewCompat.setOnApplyWindowInsetsListener(binding.gamesPlatformFrame) { view: View, windowInsets: WindowInsetsCompat -> + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener(binding.statusBarShade) { view: View, windowInsets: WindowInsetsCompat -> val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) - view.updatePadding(left = insets.left, right = insets.right) - InsetsHelper.insetAppBar(insets, binding.appbarMain) + val mlpShade = view.layoutParams as MarginLayoutParams + mlpShade.height = insets.top + binding.statusBarShade.layoutParams = mlpShade windowInsets } - } - - private val getGamesDirectory = - registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> - if (result == null) - return@registerForActivityResult - - val takeFlags = - Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION - contentResolver.takePersistableUriPermission( - result, - takeFlags - ) - - // When a new directory is picked, we currently will reset the existing games - // database. This effectively means that only one game directory is supported. - PreferenceManager.getDefaultSharedPreferences(applicationContext).edit() - .putString(GameHelper.KEY_GAME_PATH, result.toString()) - .apply() - } - - private val getProdKey = - registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> - if (result == null) - return@registerForActivityResult - - val takeFlags = - Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION - contentResolver.takePersistableUriPermission( - result, - takeFlags - ) - - val dstPath = DirectoryInitialization.userDirectory + "/keys/" - if (FileUtil.copyUriToInternalStorage(this, result, dstPath, "prod.keys")) { - if (NativeLibrary.reloadKeys()) { - Toast.makeText( - this, - R.string.install_keys_success, - Toast.LENGTH_SHORT - ).show() - refreshFragment() - } else { - Toast.makeText( - this, - R.string.install_keys_failure, - Toast.LENGTH_LONG - ).show() - } - } - } - - private val getAmiiboKey = - registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> - if (result == null) - return@registerForActivityResult - - val takeFlags = - Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION - contentResolver.takePersistableUriPermission( - result, - takeFlags - ) - - val dstPath = DirectoryInitialization.userDirectory + "/keys/" - if (FileUtil.copyUriToInternalStorage(this, result, dstPath, "key_retail.bin")) { - if (NativeLibrary.reloadKeys()) { - Toast.makeText( - this, - R.string.install_keys_success, - Toast.LENGTH_SHORT - ).show() - refreshFragment() - } else { - Toast.makeText( - this, - R.string.install_amiibo_keys_failure, - Toast.LENGTH_LONG - ).show() - } - } - } - - private val getDriver = - registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> - if (result == null) - return@registerForActivityResult - - val takeFlags = - Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION - contentResolver.takePersistableUriPermission( - result, - takeFlags - ) - - val progressBinding = DialogProgressBarBinding.inflate(layoutInflater) - progressBinding.progressBar.isIndeterminate = true - val installationDialog = MaterialAlertDialogBuilder(this) - .setTitle(R.string.installing_driver) - .setView(progressBinding.root) - .show() - - lifecycleScope.launch { - withContext(Dispatchers.IO) { - // Ignore file exceptions when a user selects an invalid zip - try { - GpuDriverHelper.installCustomDriver(applicationContext, result) - } catch (_: IOException) { - } - - withContext(Dispatchers.Main) { - installationDialog.dismiss() - - val driverName = GpuDriverHelper.customDriverName - if (driverName != null) { - Toast.makeText( - applicationContext, - getString( - R.string.select_gpu_driver_install_success, - driverName - ), - Toast.LENGTH_SHORT - ).show() - } else { - Toast.makeText( - applicationContext, - R.string.select_gpu_driver_error, - Toast.LENGTH_LONG - ).show() - } - } - } - } - } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt deleted file mode 100644 index a7ddc333f..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.kt +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.yuzu.yuzu_emu.ui.main - -import org.yuzu.yuzu_emu.BuildConfig -import org.yuzu.yuzu_emu.R -import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile - -class MainPresenter(private val view: MainView) { - fun onCreate() { - val versionName = BuildConfig.VERSION_NAME - view.setVersionString(versionName) - } - - private fun launchFileListActivity(request: Int) { - view.launchFileListActivity(request) - } - - fun handleOptionSelection(itemId: Int): Boolean { - when (itemId) { - R.id.menu_settings_core -> { - view.launchSettingsActivity(SettingsFile.FILE_NAME_CONFIG) - return true - } - R.id.button_add_directory -> { - launchFileListActivity(REQUEST_ADD_DIRECTORY) - return true - } - R.id.button_install_keys -> { - launchFileListActivity(REQUEST_INSTALL_KEYS) - return true - } - R.id.button_install_amiibo_keys -> { - launchFileListActivity(REQUEST_INSTALL_AMIIBO_KEYS) - return true - } - R.id.button_select_gpu_driver -> { - launchFileListActivity(REQUEST_SELECT_GPU_DRIVER) - return true - } - } - return false - } - - companion object { - const val REQUEST_ADD_DIRECTORY = 1 - const val REQUEST_INSTALL_KEYS = 2 - const val REQUEST_INSTALL_AMIIBO_KEYS = 3 - const val REQUEST_SELECT_GPU_DRIVER = 4 - } -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt deleted file mode 100644 index 4dc9f0706..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainView.kt +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.yuzu.yuzu_emu.ui.main - -/** - * Abstraction for the screen that shows on application launch. - * Implementations will differ primarily to target touch-screen - * or non-touch screen devices. - */ -interface MainView { - /** - * Pass the view the native library's version string. Displaying - * it is optional. - * - * @param version A string pulled from native code. - */ - fun setVersionString(version: String) - - fun launchSettingsActivity(menuTag: String) - - fun launchFileListActivity(request: Int) -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt deleted file mode 100644 index 443a37cd2..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/platform/PlatformGamesFragment.kt +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.yuzu.yuzu_emu.ui.platform - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updatePadding -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import com.google.android.material.color.MaterialColors -import org.yuzu.yuzu_emu.R -import org.yuzu.yuzu_emu.adapters.GameAdapter -import org.yuzu.yuzu_emu.databinding.FragmentGridBinding -import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager -import org.yuzu.yuzu_emu.model.GamesViewModel -import org.yuzu.yuzu_emu.utils.GameHelper - -class PlatformGamesFragment : Fragment() { - private var _binding: FragmentGridBinding? = null - private val binding get() = _binding!! - - private lateinit var gamesViewModel: GamesViewModel - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentGridBinding.inflate(inflater) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - gamesViewModel = ViewModelProvider(requireActivity())[GamesViewModel::class.java] - - binding.gridGames.apply { - layoutManager = AutofitGridLayoutManager( - requireContext(), - requireContext().resources.getDimensionPixelSize(R.dimen.card_width) - ) - adapter = - GameAdapter(requireActivity() as AppCompatActivity, gamesViewModel.games.value!!) - } - - // Add swipe down to refresh gesture - binding.swipeRefresh.setOnRefreshListener { - refresh() - binding.swipeRefresh.isRefreshing = false - } - - // Set theme color to the refresh animation's background - binding.swipeRefresh.setProgressBackgroundColorSchemeColor( - MaterialColors.getColor(binding.swipeRefresh, R.attr.colorPrimary) - ) - binding.swipeRefresh.setColorSchemeColors( - MaterialColors.getColor(binding.swipeRefresh, R.attr.colorOnPrimary) - ) - - gamesViewModel.games.observe(viewLifecycleOwner) { - (binding.gridGames.adapter as GameAdapter).swapData(it) - updateTextView() - } - - setInsets() - - refresh() - } - - override fun onResume() { - super.onResume() - refresh() - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - fun refresh() { - gamesViewModel.setGames(GameHelper.getGames()) - updateTextView() - } - - private fun updateTextView() { - if (_binding == null) - return - - binding.gamelistEmptyText.visibility = - if ((binding.gridGames.adapter as GameAdapter).itemCount == 0) View.VISIBLE else View.GONE - } - - private fun setInsets() { - ViewCompat.setOnApplyWindowInsetsListener(binding.gridGames) { view: View, windowInsets: WindowInsetsCompat -> - val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) - view.updatePadding(bottom = insets.bottom) - windowInsets - } - } - - companion object { - const val TAG = "PlatformGamesFragment" - } -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.kt deleted file mode 100644 index e2e56eb06..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.kt +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.yuzu.yuzu_emu.utils - -import androidx.preference.PreferenceManager -import android.text.Html -import android.text.method.LinkMovementMethod -import android.view.View -import android.widget.TextView -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import org.yuzu.yuzu_emu.R -import org.yuzu.yuzu_emu.YuzuApplication -import org.yuzu.yuzu_emu.features.settings.model.Settings -import org.yuzu.yuzu_emu.ui.main.MainActivity -import org.yuzu.yuzu_emu.ui.main.MainPresenter - -object StartupHandler { - private val preferences = - PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) - - private fun handleStartupPromptDismiss(parent: MainActivity) { - parent.launchFileListActivity(MainPresenter.REQUEST_INSTALL_KEYS) - } - - private fun markFirstBoot() { - preferences.edit() - .putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false) - .apply() - } - - fun handleInit(parent: MainActivity) { - if (preferences.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)) { - markFirstBoot() - val alert = MaterialAlertDialogBuilder(parent) - .setMessage(Html.fromHtml(parent.resources.getString(R.string.app_disclaimer))) - .setTitle(R.string.app_name) - .setIcon(R.drawable.ic_launcher) - .setPositiveButton(android.R.string.ok, null) - .setOnDismissListener { - handleStartupPromptDismiss(parent) - } - .show() - (alert.findViewById<View>(android.R.id.message) as TextView?)!!.movementMethod = - LinkMovementMethod.getInstance() - } - } -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt index ce6396e91..481498f7b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ThemeHelper.kt @@ -15,7 +15,7 @@ import org.yuzu.yuzu_emu.R import kotlin.math.roundToInt object ThemeHelper { - private const val NAV_BAR_ALPHA = 0.9f + const val SYSTEM_BAR_ALPHA = 0.9f @JvmStatic fun setTheme(activity: AppCompatActivity) { @@ -29,10 +29,6 @@ object ThemeHelper { windowController.isAppearanceLightNavigationBars = isLightMode activity.window.statusBarColor = ContextCompat.getColor(activity, android.R.color.transparent) - - val navigationBarColor = - MaterialColors.getColor(activity.window.decorView, R.attr.colorSurface) - setNavigationBarColor(activity, navigationBarColor) } @JvmStatic @@ -48,7 +44,7 @@ object ThemeHelper { } else if (gestureType == InsetsHelper.THREE_BUTTON_NAVIGATION || gestureType == InsetsHelper.TWO_BUTTON_NAVIGATION ) { - activity.window.navigationBarColor = getColorWithOpacity(color, NAV_BAR_ALPHA) + activity.window.navigationBarColor = getColorWithOpacity(color, SYSTEM_BAR_ALPHA) } else { activity.window.navigationBarColor = ContextCompat.getColor( activity.applicationContext, @@ -58,7 +54,7 @@ object ThemeHelper { } @ColorInt - private fun getColorWithOpacity(@ColorInt color: Int, alphaFactor: Float): Int { + fun getColorWithOpacity(@ColorInt color: Int, alphaFactor: Float): Int { return Color.argb( (alphaFactor * Color.alpha(color)).roundToInt(), Color.red(color), Color.green(color), Color.blue(color) diff --git a/src/android/app/src/main/res/drawable/ic_add.xml b/src/android/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 000000000..f7deb2532 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_add.xml @@ -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,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_input.xml b/src/android/app/src/main/res/drawable/ic_input.xml new file mode 100644 index 000000000..c170865ef --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_input.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:autoMirrored="true" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="?attr/colorControlNormal" + android:pathData="M21,3.01H3c-1.1,0 -2,0.9 -2,2V9h2V4.99h18v14.03H3V15H1v4.01c0,1.1 0.9,1.98 2,1.98h18c1.1,0 2,-0.88 2,-1.98v-14c0,-1.11 -0.9,-2 -2,-2zM11,16l4,-4 -4,-4v3H1v2h10v3z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_nfc.xml b/src/android/app/src/main/res/drawable/ic_nfc.xml new file mode 100644 index 000000000..3dacf798b --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_nfc.xml @@ -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="M20,2L4,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,20L4,20L4,4h16v16zM18,6h-5c-1.1,0 -2,0.9 -2,2v2.28c-0.6,0.35 -1,0.98 -1,1.72 0,1.1 0.9,2 2,2s2,-0.9 2,-2c0,-0.74 -0.4,-1.38 -1,-1.72L13,8h3v8L8,16L8,8h2L10,6L6,6v12h12L18,6z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_options.xml b/src/android/app/src/main/res/drawable/ic_options.xml new file mode 100644 index 000000000..91d52f1b8 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_options.xml @@ -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="M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9 -2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9 4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1 0.1,-1.4z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_unlock.xml b/src/android/app/src/main/res/drawable/ic_unlock.xml new file mode 100644 index 000000000..40952cbc5 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_unlock.xml @@ -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="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z" /> +</vector> diff --git a/src/android/app/src/main/res/drawable/ic_yuzu_themed.xml b/src/android/app/src/main/res/drawable/ic_yuzu_themed.xml new file mode 100644 index 000000000..4400e9eaf --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_yuzu_themed.xml @@ -0,0 +1,18 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="614.697dp" + android:height="683dp" + android:viewportWidth="614.4" + android:viewportHeight="682.67"> + <group> + <clip-path android:pathData="M-43,-46.67h699.6v777.33h-699.6z" /> + <path + android:fillColor="?attr/colorPrimary" + android:pathData="M340.81,138V682.08c150.26,0 272.06,-121.81 272.06,-272.06S491.07,138 340.81,138M394,197.55a219.06,219.06 0,0 1,0 424.94V197.55" /> + </group> + <group> + <clip-path android:pathData="M-43,-46.67h699.6v777.33h-699.6z" /> + <path + android:fillColor="?attr/colorPrimary" + android:pathData="M272.79,1.92C122.53,1.92 0.73,123.73 0.73,274s121.8,272.07 272.06,272.07ZM219.65,61.51v425A219,219 0,0 1,118 119.18,217.51 217.51,0 0,1 219.65,61.51" /> + </group> +</vector> diff --git a/src/android/app/src/main/res/layout/activity_main.xml b/src/android/app/src/main/res/layout/activity_main.xml index 059aaa9b4..9002b0642 100644 --- a/src/android/app/src/main/res/layout/activity_main.xml +++ b/src/android/app/src/main/res/layout/activity_main.xml @@ -1,28 +1,32 @@ <?xml version="1.0" encoding="utf-8"?> -<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" +<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/coordinator_main" android:layout_width="match_parent" android:layout_height="match_parent"> - <com.google.android.material.appbar.AppBarLayout - android:id="@+id/appbar_main" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:fitsSystemWindows="true" - app:liftOnScrollTargetViewId="@id/grid_games"> - - <androidx.appcompat.widget.Toolbar - android:id="@+id/toolbar_main" - android:layout_width="match_parent" - android:layout_height="?attr/actionBarSize" /> + <androidx.fragment.app.FragmentContainerView + android:id="@+id/fragment_container" + android:name="androidx.navigation.fragment.NavHostFragment" + android:layout_width="0dp" + android:layout_height="0dp" + app:defaultNavHost="true" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:navGraph="@navigation/home_navigation" + tools:layout="@layout/fragment_games" /> - </com.google.android.material.appbar.AppBarLayout> - - <FrameLayout - android:id="@+id/games_platform_frame" + <com.google.android.material.bottomnavigation.BottomNavigationView + android:id="@+id/navigation_bar" android:layout_width="match_parent" - android:layout_height="match_parent" - app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:menu="@menu/menu_navigation" /> -</androidx.coordinatorlayout.widget.CoordinatorLayout> +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/src/android/app/src/main/res/layout/card_home_option.xml b/src/android/app/src/main/res/layout/card_home_option.xml new file mode 100644 index 000000000..aea354783 --- /dev/null +++ b/src/android/app/src/main/res/layout/card_home_option.xml @@ -0,0 +1,53 @@ +<?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" + style="?attr/materialCardViewFilledStyle" + android:id="@+id/option_card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginVertical="8dp" + android:layout_marginHorizontal="16dp" + android:background="?attr/selectableItemBackground" + android:clickable="true" + android:focusable="true"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <ImageView + android:id="@+id/option_icon" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_marginStart="28dp" + android:layout_gravity="center_vertical" + app:tint="?attr/colorPrimary" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="16dp" + android:orientation="vertical"> + + <com.google.android.material.textview.MaterialTextView + style="@style/TextAppearance.Material3.BodyMedium" + android:id="@+id/option_title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAlignment="viewStart" + tools:text="@string/install_prod_keys" /> + + <com.google.android.material.textview.MaterialTextView + style="@style/TextAppearance.Material3.BodySmall" + android:id="@+id/option_description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAlignment="viewStart" + tools:text="@string/install_prod_keys_description" /> + + </LinearLayout> + + </LinearLayout> + +</com.google.android.material.card.MaterialCardView> diff --git a/src/android/app/src/main/res/layout/fragment_games.xml b/src/android/app/src/main/res/layout/fragment_games.xml new file mode 100644 index 000000000..5cfe76de3 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_games.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout + 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/coordinator_main" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?attr/colorSurface"> + + <androidx.swiperefreshlayout.widget.SwipeRefreshLayout + android:id="@+id/swipe_refresh" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + app:layout_behavior="@string/searchbar_scrolling_view_behavior"> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/notice_text" + style="@style/TextAppearance.Material3.BodyLarge" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center" + android:padding="@dimen/spacing_large" + android:text="@string/empty_gamelist" + 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" + app:liftOnScrollTargetViewId="@id/grid_games"> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:fitsSystemWindows="true"> + + <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" /> + + </FrameLayout> + + </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 + android:id="@+id/grid_search" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + tools:listitem="@layout/card_game" /> + + </com.google.android.material.search.SearchView> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/src/android/app/src/main/res/layout/fragment_grid.xml b/src/android/app/src/main/res/layout/fragment_grid.xml deleted file mode 100644 index bfb670b6d..000000000 --- a/src/android/app/src/main/res/layout/fragment_grid.xml +++ /dev/null @@ -1,37 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<FrameLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <androidx.swiperefreshlayout.widget.SwipeRefreshLayout - android:id="@+id/swipe_refresh" - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <RelativeLayout - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <TextView - android:id="@+id/gamelist_empty_text" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:gravity="center" - android:text="@string/empty_gamelist" - android:textSize="18sp" - android:visibility="gone" /> - - <androidx.recyclerview.widget.RecyclerView - android:id="@+id/grid_games" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:clipToPadding="false" - tools:listitem="@layout/card_game" /> - - </RelativeLayout> - - </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> - -</FrameLayout> diff --git a/src/android/app/src/main/res/layout/fragment_options.xml b/src/android/app/src/main/res/layout/fragment_options.xml new file mode 100644 index 000000000..ec6e7c205 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_options.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.core.widget.NestedScrollView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/scroll_view_options" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?attr/colorSurface" + android:clipToPadding="false"> + + <androidx.appcompat.widget.LinearLayoutCompat + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:background="?attr/colorSurface"> + + <ImageView + android:layout_width="128dp" + android:layout_height="128dp" + android:layout_margin="64dp" + android:layout_gravity="center_horizontal" + android:src="@drawable/ic_yuzu_themed" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/options_list" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + </androidx.appcompat.widget.LinearLayoutCompat> + +</androidx.core.widget.NestedScrollView> diff --git a/src/android/app/src/main/res/menu/menu_game_grid.xml b/src/android/app/src/main/res/menu/menu_game_grid.xml deleted file mode 100644 index 73046de0e..000000000 --- a/src/android/app/src/main/res/menu/menu_game_grid.xml +++ /dev/null @@ -1,47 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<menu xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto"> - - <item - android:id="@+id/button_file_menu" - android:icon="@drawable/ic_folder" - android:title="@string/select_game_folder" - app:showAsAction="ifRoom"> - - <menu> - - <item - android:id="@+id/button_add_directory" - android:icon="@drawable/ic_folder" - android:title="@string/select_game_folder" - app:showAsAction="ifRoom" /> - - <item - android:id="@+id/button_install_keys" - android:icon="@drawable/ic_install" - android:title="@string/install_keys" - app:showAsAction="ifRoom" /> - - <item - android:id="@+id/button_install_amiibo_keys" - android:icon="@drawable/ic_install" - android:title="@string/install_amiibo_keys" - app:showAsAction="ifRoom" /> - - <item - android:id="@+id/button_select_gpu_driver" - android:icon="@drawable/ic_settings" - android:title="@string/select_gpu_driver" - app:showAsAction="ifRoom" /> - - </menu> - - </item> - - <item - android:id="@+id/menu_settings_core" - android:icon="@drawable/ic_settings" - android:title="@string/grid_menu_core_settings" - app:showAsAction="ifRoom" /> - -</menu> diff --git a/src/android/app/src/main/res/menu/menu_navigation.xml b/src/android/app/src/main/res/menu/menu_navigation.xml new file mode 100644 index 000000000..ca5a656a6 --- /dev/null +++ b/src/android/app/src/main/res/menu/menu_navigation.xml @@ -0,0 +1,14 @@ +<?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/ic_controller" + android:title="@string/home_games" /> + + <item + android:id="@+id/optionsFragment" + android:icon="@drawable/ic_options" + android:title="@string/home_options" /> + +</menu> diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml new file mode 100644 index 000000000..e85e24a85 --- /dev/null +++ b/src/android/app/src/main/res/navigation/home_navigation.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<navigation xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/home_navigation" + app:startDestination="@id/gamesFragment"> + + <fragment + android:id="@+id/gamesFragment" + android:name="org.yuzu.yuzu_emu.ui.GamesFragment" + android:label="PlatformGamesFragment" /> + + <fragment + android:id="@+id/optionsFragment" + android:name="org.yuzu.yuzu_emu.fragments.OptionsFragment" + android:label="OptionsFragment" /> + +</navigation> diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml index db0a8f7e5..23977c9f1 100644 --- a/src/android/app/src/main/res/values/dimens.xml +++ b/src/android/app/src/main/res/values/dimens.xml @@ -1,10 +1,15 @@ <resources> <dimen name="spacing_small">4dp</dimen> + <dimen name="spacing_med">8dp</dimen> <dimen name="spacing_medlarge">12dp</dimen> <dimen name="spacing_large">16dp</dimen> <dimen name="spacing_xtralarge">32dp</dimen> <dimen name="spacing_list">64dp</dimen> - <dimen name="spacing_fab">72dp</dimen> + <dimen name="spacing_navigation">80dp</dimen> + <dimen name="spacing_search">88dp</dimen> + <dimen name="spacing_refresh_slingshot">80dp</dimen> + <dimen name="spacing_refresh_start">32dp</dimen> + <dimen name="spacing_refresh_end">96dp</dimen> <dimen name="menu_width">256dp</dimen> <dimen name="card_width">160dp</dimen> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 75d1f2293..564bad081 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -9,6 +9,24 @@ <string name="app_notification_channel_description">yuzu Switch emulator notifications</string> <string name="app_notification_running">yuzu is running</string> + <!-- Home strings --> + <string name="home_games">Games</string> + <string name="home_options">Options</string> + <string name="add_games">Add Games</string> + <string name="add_games_description">Select your games folder</string> + <string name="home_search_games">Search Games</string> + <string name="install_prod_keys">Install Prod.keys</string> + <string name="install_prod_keys_description">Required to decrypt retail games</string> + <string name="install_amiibo_keys">Install Amiibo Keys</string> + <string name="install_amiibo_keys_description">Required to use Amiibo in game</string> + <string name="install_keys_success">Keys successfully installed</string> + <string name="install_keys_failure">Keys file (prod.keys) is invalid</string> + <string name="install_amiibo_keys_failure">Keys file (key_retail.bin) is invalid</string> + <string name="install_gpu_driver">Install GPU Driver</string> + <string name="install_gpu_driver_description">Use a different driver for potentially better performance or accuracy</string> + <string name="settings">Settings</string> + <string name="settings_description">Configure emulator settings</string> + <!-- General settings strings --> <string name="frame_limit_enable">Enable limit speed</string> <string name="frame_limit_enable_description">When enabled, emulation speed will be limited to a specified percentage of normal speed.</string> @@ -51,17 +69,6 @@ <string name="error_saving">Error saving %1$s.ini: %2$s</string> <string name="loading">Loading...</string> - <!-- Game Grid Screen--> - <string name="grid_menu_core_settings">Settings</string> - - <!-- Add Directory Screen--> - <string name="select_game_folder">Select game folder</string> - <string name="install_keys">Install keys</string> - <string name="install_amiibo_keys">Install amiibo keys</string> - <string name="install_keys_success">Keys successfully installed</string> - <string name="install_keys_failure">Keys file (prod.keys) is invalid</string> - <string name="install_amiibo_keys_failure">Keys file (key_retail.bin) is invalid</string> - <!-- GPU driver installation --> <string name="select_gpu_driver">Select GPU driver</string> <string name="select_gpu_driver_title">Would you like to replace your current GPU driver?</string> |