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 +import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder -/** - * 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) : - RecyclerView.Adapter(), +class GameAdapter(private val activity: AppCompatActivity) : + ListAdapter(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) { - this.games = games - notifyDataSetChanged() + private class DiffCallback : DiffUtil.ItemCallback() { + 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) : + RecyclerView.Adapter(), + 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 = 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>() - val games: LiveData> get() = _games + private val _games = MutableLiveData>(emptyList()) + val games: LiveData> get() = _games + + private val _searchedGames = MutableLiveData>(emptyList()) + val searchedGames: LiveData> get() = _searchedGames + + private val _isReloading = MutableLiveData(false) + val isReloading: LiveData get() = _isReloading + + private val _shouldSwapData = MutableLiveData(false) + val shouldSwapData: LiveData get() = _shouldSwapData init { - _games.value = ArrayList() + reloadGames(false) } - fun setGames(games: ArrayList) { - _games.value = games + fun setSearchedGames(games: List) { + _searchedGames.postValue(games) + } + + fun setShouldSwapData(shouldSwap: Boolean) { + _shouldSwapData.postValue(shouldSwap) + } + + 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 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 = 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(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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + + + + + + + + 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 @@ - - + + + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:menu="@menu/menu_navigation" /> - - - - - - - + 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 @@ + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ - - - - - - - - - - - - - - - - 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 @@ + + + + + + + + + + + + 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 @@ - - - - - - - - - - - - - - - - - - - - - - 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 @@ + + + + + + + + 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 @@ + + + + + + + + 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 @@ 4dp + 8dp 12dp 16dp 32dp 64dp - 72dp + 80dp + 88dp + 80dp + 32dp + 96dp 256dp 160dp 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 @@ yuzu Switch emulator notifications yuzu is running + + Games + Options + Add Games + Select your games folder + Search Games + Install Prod.keys + Required to decrypt retail games + Install Amiibo Keys + Required to use Amiibo in game + Keys successfully installed + Keys file (prod.keys) is invalid + Keys file (key_retail.bin) is invalid + Install GPU Driver + Use a different driver for potentially better performance or accuracy + Settings + Configure emulator settings + Enable limit speed When enabled, emulation speed will be limited to a specified percentage of normal speed. @@ -51,17 +69,6 @@ Error saving %1$s.ini: %2$s Loading... - - Settings - - - Select game folder - Install keys - Install amiibo keys - Keys successfully installed - Keys file (prod.keys) is invalid - Keys file (key_retail.bin) is invalid - Select GPU driver Would you like to replace your current GPU driver?