diff options
Diffstat (limited to 'src/android')
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 { | |||
| 155 | implementation("org.ini4j:ini4j:0.5.4") | 155 | implementation("org.ini4j:ini4j:0.5.4") |
| 156 | implementation("androidx.constraintlayout:constraintlayout:2.1.4") | 156 | implementation("androidx.constraintlayout:constraintlayout:2.1.4") |
| 157 | implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") | 157 | implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") |
| 158 | implementation("androidx.navigation:navigation-fragment-ktx:2.5.3") | ||
| 159 | implementation("androidx.navigation:navigation-ui-ktx:2.5.3") | ||
| 160 | implementation("info.debatty:java-string-similarity:2.0.0") | ||
| 158 | } | 161 | } |
| 159 | 162 | ||
| 160 | fun getVersion(): String { | 163 | 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 | |||
| 13 | import android.view.WindowManager | 13 | import android.view.WindowManager |
| 14 | import android.view.inputmethod.InputMethodManager | 14 | import android.view.inputmethod.InputMethodManager |
| 15 | import androidx.appcompat.app.AppCompatActivity | 15 | import androidx.appcompat.app.AppCompatActivity |
| 16 | import androidx.fragment.app.FragmentActivity | ||
| 17 | import androidx.preference.PreferenceManager | 16 | import androidx.preference.PreferenceManager |
| 18 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | 17 | import com.google.android.material.dialog.MaterialAlertDialogBuilder |
| 19 | import com.google.android.material.slider.Slider.OnChangeListener | 18 | import com.google.android.material.slider.Slider.OnChangeListener |
| @@ -202,7 +201,7 @@ open class EmulationActivity : AppCompatActivity() { | |||
| 202 | private const val EMULATION_RUNNING_NOTIFICATION = 0x1000 | 201 | private const val EMULATION_RUNNING_NOTIFICATION = 0x1000 |
| 203 | 202 | ||
| 204 | @JvmStatic | 203 | @JvmStatic |
| 205 | fun launch(activity: FragmentActivity, game: Game) { | 204 | fun launch(activity: AppCompatActivity, game: Game) { |
| 206 | val launcher = Intent(activity, EmulationActivity::class.java) | 205 | val launcher = Intent(activity, EmulationActivity::class.java) |
| 207 | launcher.putExtra(EXTRA_SELECTED_GAME, game) | 206 | launcher.putExtra(EXTRA_SELECTED_GAME, game) |
| 208 | activity.startActivity(launcher) | 207 | 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 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.adapters | 4 | package org.yuzu.yuzu_emu.adapters |
| 5 | 5 | ||
| 6 | import android.annotation.SuppressLint | ||
| 6 | import android.graphics.Bitmap | 7 | import android.graphics.Bitmap |
| 7 | import android.graphics.BitmapFactory | 8 | import android.graphics.BitmapFactory |
| 8 | import android.view.LayoutInflater | 9 | import android.view.LayoutInflater |
| @@ -11,29 +12,25 @@ import android.view.ViewGroup | |||
| 11 | import android.widget.ImageView | 12 | import android.widget.ImageView |
| 12 | import androidx.appcompat.app.AppCompatActivity | 13 | import androidx.appcompat.app.AppCompatActivity |
| 13 | import androidx.lifecycle.lifecycleScope | 14 | import androidx.lifecycle.lifecycleScope |
| 15 | import androidx.recyclerview.widget.AsyncDifferConfig | ||
| 16 | import androidx.recyclerview.widget.DiffUtil | ||
| 17 | import androidx.recyclerview.widget.ListAdapter | ||
| 14 | import androidx.recyclerview.widget.RecyclerView | 18 | import androidx.recyclerview.widget.RecyclerView |
| 15 | import coil.load | 19 | import coil.load |
| 16 | import kotlinx.coroutines.Dispatchers | ||
| 17 | import kotlinx.coroutines.launch | 20 | import kotlinx.coroutines.launch |
| 18 | import kotlinx.coroutines.withContext | ||
| 19 | import org.yuzu.yuzu_emu.NativeLibrary | 21 | import org.yuzu.yuzu_emu.NativeLibrary |
| 20 | import org.yuzu.yuzu_emu.R | 22 | import org.yuzu.yuzu_emu.R |
| 21 | import org.yuzu.yuzu_emu.databinding.CardGameBinding | 23 | import org.yuzu.yuzu_emu.databinding.CardGameBinding |
| 22 | import org.yuzu.yuzu_emu.activities.EmulationActivity | 24 | import org.yuzu.yuzu_emu.activities.EmulationActivity |
| 23 | import org.yuzu.yuzu_emu.model.Game | 25 | import org.yuzu.yuzu_emu.model.Game |
| 24 | import kotlin.collections.ArrayList | 26 | import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder |
| 25 | 27 | ||
| 26 | /** | 28 | class GameAdapter(private val activity: AppCompatActivity) : |
| 27 | * This adapter gets its information from a database Cursor. This fact, paired with the usage of | 29 | ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()), |
| 28 | * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly) | ||
| 29 | * large dataset. | ||
| 30 | */ | ||
| 31 | class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList<Game>) : | ||
| 32 | RecyclerView.Adapter<GameAdapter.GameViewHolder>(), | ||
| 33 | View.OnClickListener { | 30 | View.OnClickListener { |
| 34 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { | 31 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { |
| 35 | // Create a new view. | 32 | // Create a new view. |
| 36 | val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context)) | 33 | val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) |
| 37 | binding.root.setOnClickListener(this) | 34 | binding.root.setOnClickListener(this) |
| 38 | 35 | ||
| 39 | // Use that view to create a ViewHolder. | 36 | // Use that view to create a ViewHolder. |
| @@ -41,12 +38,10 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList< | |||
| 41 | } | 38 | } |
| 42 | 39 | ||
| 43 | override fun onBindViewHolder(holder: GameViewHolder, position: Int) { | 40 | override fun onBindViewHolder(holder: GameViewHolder, position: Int) { |
| 44 | holder.bind(games[position]) | 41 | holder.bind(currentList[position]) |
| 45 | } | 42 | } |
| 46 | 43 | ||
| 47 | override fun getItemCount(): Int { | 44 | override fun getItemCount(): Int = currentList.size |
| 48 | return games.size | ||
| 49 | } | ||
| 50 | 45 | ||
| 51 | /** | 46 | /** |
| 52 | * Launches the game that was clicked on. | 47 | * Launches the game that was clicked on. |
| @@ -55,7 +50,7 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList< | |||
| 55 | */ | 50 | */ |
| 56 | override fun onClick(view: View) { | 51 | override fun onClick(view: View) { |
| 57 | val holder = view.tag as GameViewHolder | 52 | val holder = view.tag as GameViewHolder |
| 58 | EmulationActivity.launch((view.context as AppCompatActivity), holder.game) | 53 | EmulationActivity.launch(activity, holder.game) |
| 59 | } | 54 | } |
| 60 | 55 | ||
| 61 | inner class GameViewHolder(val binding: CardGameBinding) : | 56 | inner class GameViewHolder(val binding: CardGameBinding) : |
| @@ -74,7 +69,6 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList< | |||
| 74 | val bitmap = decodeGameIcon(game.path) | 69 | val bitmap = decodeGameIcon(game.path) |
| 75 | binding.imageGameScreen.load(bitmap) { | 70 | binding.imageGameScreen.load(bitmap) { |
| 76 | error(R.drawable.no_icon) | 71 | error(R.drawable.no_icon) |
| 77 | crossfade(true) | ||
| 78 | } | 72 | } |
| 79 | } | 73 | } |
| 80 | 74 | ||
| @@ -87,9 +81,15 @@ class GameAdapter(private val activity: AppCompatActivity, var games: ArrayList< | |||
| 87 | } | 81 | } |
| 88 | } | 82 | } |
| 89 | 83 | ||
| 90 | fun swapData(games: ArrayList<Game>) { | 84 | private class DiffCallback : DiffUtil.ItemCallback<Game>() { |
| 91 | this.games = games | 85 | override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean { |
| 92 | notifyDataSetChanged() | 86 | return oldItem.gameId == newItem.gameId |
| 87 | } | ||
| 88 | |||
| 89 | @SuppressLint("DiffUtilEquals") | ||
| 90 | override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean { | ||
| 91 | return oldItem == newItem | ||
| 92 | } | ||
| 93 | } | 93 | } |
| 94 | 94 | ||
| 95 | private fun decodeGameIcon(uri: String): Bitmap? { | 95 | 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 @@ | |||
| 1 | package org.yuzu.yuzu_emu.adapters | ||
| 2 | |||
| 3 | import android.view.LayoutInflater | ||
| 4 | import android.view.View | ||
| 5 | import android.view.ViewGroup | ||
| 6 | import androidx.appcompat.app.AppCompatActivity | ||
| 7 | import androidx.core.content.res.ResourcesCompat | ||
| 8 | import androidx.recyclerview.widget.RecyclerView | ||
| 9 | import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding | ||
| 10 | import org.yuzu.yuzu_emu.model.HomeOption | ||
| 11 | |||
| 12 | class HomeOptionAdapter(private val activity: AppCompatActivity, var options: List<HomeOption>) : | ||
| 13 | RecyclerView.Adapter<HomeOptionAdapter.HomeOptionViewHolder>(), | ||
| 14 | View.OnClickListener { | ||
| 15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder { | ||
| 16 | val binding = CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) | ||
| 17 | binding.root.setOnClickListener(this) | ||
| 18 | return HomeOptionViewHolder(binding) | ||
| 19 | } | ||
| 20 | |||
| 21 | override fun getItemCount(): Int { | ||
| 22 | return options.size | ||
| 23 | } | ||
| 24 | |||
| 25 | override fun onBindViewHolder(holder: HomeOptionViewHolder, position: Int) { | ||
| 26 | holder.bind(options[position]) | ||
| 27 | } | ||
| 28 | |||
| 29 | override fun onClick(view: View) { | ||
| 30 | val holder = view.tag as HomeOptionViewHolder | ||
| 31 | holder.option.onClick.invoke() | ||
| 32 | } | ||
| 33 | |||
| 34 | inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) : | ||
| 35 | RecyclerView.ViewHolder(binding.root) { | ||
| 36 | lateinit var option: HomeOption | ||
| 37 | |||
| 38 | init { | ||
| 39 | itemView.tag = this | ||
| 40 | } | ||
| 41 | |||
| 42 | fun bind(option: HomeOption) { | ||
| 43 | this.option = option | ||
| 44 | binding.optionTitle.text = activity.resources.getString(option.titleId) | ||
| 45 | binding.optionDescription.text = activity.resources.getString(option.descriptionId) | ||
| 46 | binding.optionIcon.setImageDrawable( | ||
| 47 | ResourcesCompat.getDrawable( | ||
| 48 | activity.resources, | ||
| 49 | option.iconId, | ||
| 50 | activity.theme | ||
| 51 | ) | ||
| 52 | ) | ||
| 53 | } | ||
| 54 | } | ||
| 55 | } | ||
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 | |||
| 15 | import androidx.core.view.WindowCompat | 15 | import androidx.core.view.WindowCompat |
| 16 | import androidx.core.view.WindowInsetsCompat | 16 | import androidx.core.view.WindowInsetsCompat |
| 17 | import androidx.core.view.updatePadding | 17 | import androidx.core.view.updatePadding |
| 18 | import com.google.android.material.color.MaterialColors | ||
| 18 | import org.yuzu.yuzu_emu.NativeLibrary | 19 | import org.yuzu.yuzu_emu.NativeLibrary |
| 19 | import org.yuzu.yuzu_emu.R | 20 | import org.yuzu.yuzu_emu.R |
| 20 | import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding | 21 | import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding |
| @@ -50,6 +51,11 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView { | |||
| 50 | setSupportActionBar(binding.toolbarSettings) | 51 | setSupportActionBar(binding.toolbarSettings) |
| 51 | supportActionBar!!.setDisplayHomeAsUpEnabled(true) | 52 | supportActionBar!!.setDisplayHomeAsUpEnabled(true) |
| 52 | 53 | ||
| 54 | ThemeHelper.setNavigationBarColor( | ||
| 55 | this, | ||
| 56 | MaterialColors.getColor(window.decorView, R.attr.colorSurface) | ||
| 57 | ) | ||
| 58 | |||
| 53 | setInsets() | 59 | setInsets() |
| 54 | } | 60 | } |
| 55 | 61 | ||
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 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.fragments | ||
| 5 | |||
| 6 | import android.content.DialogInterface | ||
| 7 | import android.content.Intent | ||
| 8 | import android.os.Bundle | ||
| 9 | import android.view.LayoutInflater | ||
| 10 | import android.view.View | ||
| 11 | import android.view.ViewGroup | ||
| 12 | import android.widget.Toast | ||
| 13 | import androidx.activity.result.contract.ActivityResultContracts | ||
| 14 | import androidx.appcompat.app.AppCompatActivity | ||
| 15 | import androidx.core.view.ViewCompat | ||
| 16 | import androidx.core.view.WindowInsetsCompat | ||
| 17 | import androidx.fragment.app.Fragment | ||
| 18 | import androidx.fragment.app.activityViewModels | ||
| 19 | import androidx.lifecycle.lifecycleScope | ||
| 20 | import androidx.preference.PreferenceManager | ||
| 21 | import androidx.recyclerview.widget.LinearLayoutManager | ||
| 22 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||
| 23 | import kotlinx.coroutines.Dispatchers | ||
| 24 | import kotlinx.coroutines.launch | ||
| 25 | import kotlinx.coroutines.withContext | ||
| 26 | import org.yuzu.yuzu_emu.NativeLibrary | ||
| 27 | import org.yuzu.yuzu_emu.R | ||
| 28 | import org.yuzu.yuzu_emu.adapters.HomeOptionAdapter | ||
| 29 | import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding | ||
| 30 | import org.yuzu.yuzu_emu.databinding.FragmentOptionsBinding | ||
| 31 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity | ||
| 32 | import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile | ||
| 33 | import org.yuzu.yuzu_emu.model.GamesViewModel | ||
| 34 | import org.yuzu.yuzu_emu.model.HomeOption | ||
| 35 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization | ||
| 36 | import org.yuzu.yuzu_emu.utils.FileUtil | ||
| 37 | import org.yuzu.yuzu_emu.utils.GameHelper | ||
| 38 | import org.yuzu.yuzu_emu.utils.GpuDriverHelper | ||
| 39 | import java.io.IOException | ||
| 40 | |||
| 41 | class OptionsFragment : Fragment() { | ||
| 42 | private var _binding: FragmentOptionsBinding? = null | ||
| 43 | private val binding get() = _binding!! | ||
| 44 | |||
| 45 | private val gamesViewModel: GamesViewModel by activityViewModels() | ||
| 46 | |||
| 47 | override fun onCreateView( | ||
| 48 | inflater: LayoutInflater, | ||
| 49 | container: ViewGroup?, | ||
| 50 | savedInstanceState: Bundle? | ||
| 51 | ): View { | ||
| 52 | _binding = FragmentOptionsBinding.inflate(layoutInflater) | ||
| 53 | return binding.root | ||
| 54 | } | ||
| 55 | |||
| 56 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| 57 | val optionsList: List<HomeOption> = listOf( | ||
| 58 | HomeOption( | ||
| 59 | R.string.add_games, | ||
| 60 | R.string.add_games_description, | ||
| 61 | R.drawable.ic_add | ||
| 62 | ) { getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) }, | ||
| 63 | HomeOption( | ||
| 64 | R.string.install_prod_keys, | ||
| 65 | R.string.install_prod_keys_description, | ||
| 66 | R.drawable.ic_unlock | ||
| 67 | ) { getProdKey.launch(arrayOf("*/*")) }, | ||
| 68 | HomeOption( | ||
| 69 | R.string.install_amiibo_keys, | ||
| 70 | R.string.install_amiibo_keys_description, | ||
| 71 | R.drawable.ic_nfc | ||
| 72 | ) { getAmiiboKey.launch(arrayOf("*/*")) }, | ||
| 73 | HomeOption( | ||
| 74 | R.string.install_gpu_driver, | ||
| 75 | R.string.install_gpu_driver_description, | ||
| 76 | R.drawable.ic_input | ||
| 77 | ) { driverInstaller() }, | ||
| 78 | HomeOption( | ||
| 79 | R.string.settings, | ||
| 80 | R.string.settings_description, | ||
| 81 | R.drawable.ic_settings | ||
| 82 | ) { SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") } | ||
| 83 | ) | ||
| 84 | |||
| 85 | binding.optionsList.apply { | ||
| 86 | layoutManager = LinearLayoutManager(requireContext()) | ||
| 87 | adapter = HomeOptionAdapter(requireActivity() as AppCompatActivity, optionsList) | ||
| 88 | } | ||
| 89 | |||
| 90 | requireActivity().window.statusBarColor = ThemeHelper.getColorWithOpacity( | ||
| 91 | MaterialColors.getColor( | ||
| 92 | binding.root, | ||
| 93 | R.attr.colorSurface | ||
| 94 | ), ThemeHelper.SYSTEM_BAR_ALPHA | ||
| 95 | ) | ||
| 96 | |||
| 97 | setInsets() | ||
| 98 | } | ||
| 99 | |||
| 100 | override fun onDestroyView() { | ||
| 101 | super.onDestroyView() | ||
| 102 | _binding = null | ||
| 103 | } | ||
| 104 | |||
| 105 | private fun driverInstaller() { | ||
| 106 | // Get the driver name for the dialog message. | ||
| 107 | var driverName = GpuDriverHelper.customDriverName | ||
| 108 | if (driverName == null) { | ||
| 109 | driverName = getString(R.string.system_gpu_driver) | ||
| 110 | } | ||
| 111 | |||
| 112 | MaterialAlertDialogBuilder(requireContext()) | ||
| 113 | .setTitle(getString(R.string.select_gpu_driver_title)) | ||
| 114 | .setMessage(driverName) | ||
| 115 | .setNegativeButton(android.R.string.cancel, null) | ||
| 116 | .setPositiveButton(R.string.select_gpu_driver_default) { _: DialogInterface?, _: Int -> | ||
| 117 | GpuDriverHelper.installDefaultDriver(requireContext()) | ||
| 118 | Toast.makeText( | ||
| 119 | requireContext(), | ||
| 120 | R.string.select_gpu_driver_use_default, | ||
| 121 | Toast.LENGTH_SHORT | ||
| 122 | ).show() | ||
| 123 | } | ||
| 124 | .setNeutralButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int -> | ||
| 125 | getDriver.launch(arrayOf("application/zip")) | ||
| 126 | } | ||
| 127 | .show() | ||
| 128 | } | ||
| 129 | |||
| 130 | private fun setInsets() = | ||
| 131 | ViewCompat.setOnApplyWindowInsetsListener(binding.scrollViewOptions) { view: View, windowInsets: WindowInsetsCompat -> | ||
| 132 | val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) | ||
| 133 | view.setPadding( | ||
| 134 | insets.left, | ||
| 135 | insets.top, | ||
| 136 | insets.right, | ||
| 137 | insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation) | ||
| 138 | ) | ||
| 139 | windowInsets | ||
| 140 | } | ||
| 141 | |||
| 142 | private val getGamesDirectory = | ||
| 143 | registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> | ||
| 144 | if (result == null) | ||
| 145 | return@registerForActivityResult | ||
| 146 | |||
| 147 | val takeFlags = | ||
| 148 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||
| 149 | requireActivity().contentResolver.takePersistableUriPermission( | ||
| 150 | result, | ||
| 151 | takeFlags | ||
| 152 | ) | ||
| 153 | |||
| 154 | // When a new directory is picked, we currently will reset the existing games | ||
| 155 | // database. This effectively means that only one game directory is supported. | ||
| 156 | PreferenceManager.getDefaultSharedPreferences(requireContext()).edit() | ||
| 157 | .putString(GameHelper.KEY_GAME_PATH, result.toString()) | ||
| 158 | .apply() | ||
| 159 | |||
| 160 | gamesViewModel.reloadGames(true) | ||
| 161 | } | ||
| 162 | |||
| 163 | private val getProdKey = | ||
| 164 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> | ||
| 165 | if (result == null) | ||
| 166 | return@registerForActivityResult | ||
| 167 | |||
| 168 | val takeFlags = | ||
| 169 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||
| 170 | requireActivity().contentResolver.takePersistableUriPermission( | ||
| 171 | result, | ||
| 172 | takeFlags | ||
| 173 | ) | ||
| 174 | |||
| 175 | val dstPath = DirectoryInitialization.userDirectory + "/keys/" | ||
| 176 | if (FileUtil.copyUriToInternalStorage(requireContext(), result, dstPath, "prod.keys")) { | ||
| 177 | if (NativeLibrary.reloadKeys()) { | ||
| 178 | Toast.makeText( | ||
| 179 | requireContext(), | ||
| 180 | R.string.install_keys_success, | ||
| 181 | Toast.LENGTH_SHORT | ||
| 182 | ).show() | ||
| 183 | gamesViewModel.reloadGames(true) | ||
| 184 | } else { | ||
| 185 | Toast.makeText( | ||
| 186 | requireContext(), | ||
| 187 | R.string.install_keys_failure, | ||
| 188 | Toast.LENGTH_LONG | ||
| 189 | ).show() | ||
| 190 | } | ||
| 191 | } | ||
| 192 | } | ||
| 193 | |||
| 194 | private val getAmiiboKey = | ||
| 195 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> | ||
| 196 | if (result == null) | ||
| 197 | return@registerForActivityResult | ||
| 198 | |||
| 199 | val takeFlags = | ||
| 200 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||
| 201 | requireActivity().contentResolver.takePersistableUriPermission( | ||
| 202 | result, | ||
| 203 | takeFlags | ||
| 204 | ) | ||
| 205 | |||
| 206 | val dstPath = DirectoryInitialization.userDirectory + "/keys/" | ||
| 207 | if (FileUtil.copyUriToInternalStorage( | ||
| 208 | requireContext(), | ||
| 209 | result, | ||
| 210 | dstPath, | ||
| 211 | "key_retail.bin" | ||
| 212 | ) | ||
| 213 | ) { | ||
| 214 | if (NativeLibrary.reloadKeys()) { | ||
| 215 | Toast.makeText( | ||
| 216 | requireContext(), | ||
| 217 | R.string.install_keys_success, | ||
| 218 | Toast.LENGTH_SHORT | ||
| 219 | ).show() | ||
| 220 | } else { | ||
| 221 | Toast.makeText( | ||
| 222 | requireContext(), | ||
| 223 | R.string.install_amiibo_keys_failure, | ||
| 224 | Toast.LENGTH_LONG | ||
| 225 | ).show() | ||
| 226 | } | ||
| 227 | } | ||
| 228 | } | ||
| 229 | |||
| 230 | private val getDriver = | ||
| 231 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> | ||
| 232 | if (result == null) | ||
| 233 | return@registerForActivityResult | ||
| 234 | |||
| 235 | val takeFlags = | ||
| 236 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||
| 237 | requireActivity().contentResolver.takePersistableUriPermission( | ||
| 238 | result, | ||
| 239 | takeFlags | ||
| 240 | ) | ||
| 241 | |||
| 242 | val progressBinding = DialogProgressBarBinding.inflate(layoutInflater) | ||
| 243 | progressBinding.progressBar.isIndeterminate = true | ||
| 244 | val installationDialog = MaterialAlertDialogBuilder(requireContext()) | ||
| 245 | .setTitle(R.string.installing_driver) | ||
| 246 | .setView(progressBinding.root) | ||
| 247 | .show() | ||
| 248 | |||
| 249 | lifecycleScope.launch { | ||
| 250 | withContext(Dispatchers.IO) { | ||
| 251 | // Ignore file exceptions when a user selects an invalid zip | ||
| 252 | try { | ||
| 253 | GpuDriverHelper.installCustomDriver(requireContext(), result) | ||
| 254 | } catch (_: IOException) { | ||
| 255 | } | ||
| 256 | |||
| 257 | withContext(Dispatchers.Main) { | ||
| 258 | installationDialog.dismiss() | ||
| 259 | |||
| 260 | val driverName = GpuDriverHelper.customDriverName | ||
| 261 | if (driverName != null) { | ||
| 262 | Toast.makeText( | ||
| 263 | requireContext(), | ||
| 264 | getString( | ||
| 265 | R.string.select_gpu_driver_install_success, | ||
| 266 | driverName | ||
| 267 | ), | ||
| 268 | Toast.LENGTH_SHORT | ||
| 269 | ).show() | ||
| 270 | } else { | ||
| 271 | Toast.makeText( | ||
| 272 | requireContext(), | ||
| 273 | R.string.select_gpu_driver_error, | ||
| 274 | Toast.LENGTH_LONG | ||
| 275 | ).show() | ||
| 276 | } | ||
| 277 | } | ||
| 278 | } | ||
| 279 | } | ||
| 280 | } | ||
| 281 | } | ||
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 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 1 | package org.yuzu.yuzu_emu.model | 4 | package org.yuzu.yuzu_emu.model |
| 2 | 5 | ||
| 3 | import androidx.lifecycle.LiveData | 6 | import androidx.lifecycle.LiveData |
| 4 | import androidx.lifecycle.MutableLiveData | 7 | import androidx.lifecycle.MutableLiveData |
| 5 | import androidx.lifecycle.ViewModel | 8 | import androidx.lifecycle.ViewModel |
| 9 | import androidx.lifecycle.viewModelScope | ||
| 10 | import kotlinx.coroutines.Dispatchers | ||
| 11 | import kotlinx.coroutines.launch | ||
| 12 | import kotlinx.coroutines.withContext | ||
| 13 | import org.yuzu.yuzu_emu.NativeLibrary | ||
| 14 | import org.yuzu.yuzu_emu.utils.GameHelper | ||
| 6 | 15 | ||
| 7 | class GamesViewModel : ViewModel() { | 16 | class GamesViewModel : ViewModel() { |
| 8 | private val _games = MutableLiveData<ArrayList<Game>>() | 17 | private val _games = MutableLiveData<List<Game>>(emptyList()) |
| 9 | val games: LiveData<ArrayList<Game>> get() = _games | 18 | val games: LiveData<List<Game>> get() = _games |
| 19 | |||
| 20 | private val _searchedGames = MutableLiveData<List<Game>>(emptyList()) | ||
| 21 | val searchedGames: LiveData<List<Game>> get() = _searchedGames | ||
| 22 | |||
| 23 | private val _isReloading = MutableLiveData(false) | ||
| 24 | val isReloading: LiveData<Boolean> get() = _isReloading | ||
| 25 | |||
| 26 | private val _shouldSwapData = MutableLiveData(false) | ||
| 27 | val shouldSwapData: LiveData<Boolean> get() = _shouldSwapData | ||
| 10 | 28 | ||
| 11 | init { | 29 | init { |
| 12 | _games.value = ArrayList() | 30 | reloadGames(false) |
| 31 | } | ||
| 32 | |||
| 33 | fun setSearchedGames(games: List<Game>) { | ||
| 34 | _searchedGames.postValue(games) | ||
| 35 | } | ||
| 36 | |||
| 37 | fun setShouldSwapData(shouldSwap: Boolean) { | ||
| 38 | _shouldSwapData.postValue(shouldSwap) | ||
| 13 | } | 39 | } |
| 14 | 40 | ||
| 15 | fun setGames(games: ArrayList<Game>) { | 41 | fun reloadGames(directoryChanged: Boolean) { |
| 16 | _games.value = games | 42 | if (isReloading.value == true) |
| 43 | return | ||
| 44 | _isReloading.postValue(true) | ||
| 45 | |||
| 46 | viewModelScope.launch { | ||
| 47 | withContext(Dispatchers.IO) { | ||
| 48 | NativeLibrary.resetRomMetadata() | ||
| 49 | _games.postValue(GameHelper.getGames()) | ||
| 50 | _isReloading.postValue(false) | ||
| 51 | |||
| 52 | if (directoryChanged) { | ||
| 53 | setShouldSwapData(true) | ||
| 54 | } | ||
| 55 | } | ||
| 56 | } | ||
| 17 | } | 57 | } |
| 18 | } | 58 | } |
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 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.model | ||
| 5 | |||
| 6 | data class HomeOption( | ||
| 7 | val titleId: Int, | ||
| 8 | val descriptionId: Int, | ||
| 9 | val iconId: Int, | ||
| 10 | val onClick: () -> Unit | ||
| 11 | ) | ||
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 @@ | |||
| 1 | package org.yuzu.yuzu_emu.model | ||
| 2 | |||
| 3 | import androidx.lifecycle.LiveData | ||
| 4 | import androidx.lifecycle.MutableLiveData | ||
| 5 | import androidx.lifecycle.ViewModel | ||
| 6 | |||
| 7 | class HomeViewModel : ViewModel() { | ||
| 8 | private val _navigationVisible = MutableLiveData(true) | ||
| 9 | val navigationVisible: LiveData<Boolean> get() = _navigationVisible | ||
| 10 | |||
| 11 | fun setNavigationVisible(visible: Boolean) { | ||
| 12 | if (_navigationVisible.value == visible) { | ||
| 13 | return | ||
| 14 | } | ||
| 15 | _navigationVisible.value = visible | ||
| 16 | } | ||
| 17 | } | ||
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 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.ui | ||
| 5 | |||
| 6 | import android.os.Bundle | ||
| 7 | import android.view.LayoutInflater | ||
| 8 | import android.view.View | ||
| 9 | import android.view.ViewGroup | ||
| 10 | import androidx.activity.OnBackPressedCallback | ||
| 11 | import androidx.appcompat.app.AppCompatActivity | ||
| 12 | import androidx.core.content.ContextCompat | ||
| 13 | import androidx.core.view.ViewCompat | ||
| 14 | import androidx.core.view.WindowInsetsCompat | ||
| 15 | import androidx.core.view.updatePadding | ||
| 16 | import androidx.core.widget.doOnTextChanged | ||
| 17 | import androidx.fragment.app.Fragment | ||
| 18 | import androidx.fragment.app.activityViewModels | ||
| 19 | import com.google.android.material.color.MaterialColors | ||
| 20 | import com.google.android.material.search.SearchView | ||
| 21 | import com.google.android.material.search.SearchView.TransitionState | ||
| 22 | import info.debatty.java.stringsimilarity.Jaccard | ||
| 23 | import org.yuzu.yuzu_emu.R | ||
| 24 | import org.yuzu.yuzu_emu.adapters.GameAdapter | ||
| 25 | import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding | ||
| 26 | import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager | ||
| 27 | import org.yuzu.yuzu_emu.model.Game | ||
| 28 | import org.yuzu.yuzu_emu.model.GamesViewModel | ||
| 29 | import org.yuzu.yuzu_emu.model.HomeViewModel | ||
| 30 | import org.yuzu.yuzu_emu.utils.ThemeHelper | ||
| 31 | import java.util.Locale | ||
| 32 | |||
| 33 | class GamesFragment : Fragment() { | ||
| 34 | private var _binding: FragmentGamesBinding? = null | ||
| 35 | private val binding get() = _binding!! | ||
| 36 | |||
| 37 | private val gamesViewModel: GamesViewModel by activityViewModels() | ||
| 38 | private val homeViewModel: HomeViewModel by activityViewModels() | ||
| 39 | |||
| 40 | override fun onCreateView( | ||
| 41 | inflater: LayoutInflater, | ||
| 42 | container: ViewGroup?, | ||
| 43 | savedInstanceState: Bundle? | ||
| 44 | ): View { | ||
| 45 | _binding = FragmentGamesBinding.inflate(inflater) | ||
| 46 | return binding.root | ||
| 47 | } | ||
| 48 | |||
| 49 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| 50 | // Use custom back navigation so the user doesn't back out of the app when trying to back | ||
| 51 | // out of the search view | ||
| 52 | requireActivity().onBackPressedDispatcher.addCallback( | ||
| 53 | viewLifecycleOwner, | ||
| 54 | object : OnBackPressedCallback(true) { | ||
| 55 | override fun handleOnBackPressed() { | ||
| 56 | if (binding.searchView.currentTransitionState == TransitionState.SHOWN) { | ||
| 57 | binding.searchView.hide() | ||
| 58 | } else { | ||
| 59 | requireActivity().finish() | ||
| 60 | } | ||
| 61 | } | ||
| 62 | }) | ||
| 63 | |||
| 64 | binding.gridGames.apply { | ||
| 65 | layoutManager = AutofitGridLayoutManager( | ||
| 66 | requireContext(), | ||
| 67 | requireContext().resources.getDimensionPixelSize(R.dimen.card_width) | ||
| 68 | ) | ||
| 69 | adapter = GameAdapter(requireActivity() as AppCompatActivity) | ||
| 70 | } | ||
| 71 | setUpSearch() | ||
| 72 | |||
| 73 | // Add swipe down to refresh gesture | ||
| 74 | binding.swipeRefresh.setOnRefreshListener { | ||
| 75 | gamesViewModel.reloadGames(false) | ||
| 76 | } | ||
| 77 | |||
| 78 | // Set theme color to the refresh animation's background | ||
| 79 | binding.swipeRefresh.setProgressBackgroundColorSchemeColor( | ||
| 80 | MaterialColors.getColor(binding.swipeRefresh, R.attr.colorPrimary) | ||
| 81 | ) | ||
| 82 | binding.swipeRefresh.setColorSchemeColors( | ||
| 83 | MaterialColors.getColor(binding.swipeRefresh, R.attr.colorOnPrimary) | ||
| 84 | ) | ||
| 85 | |||
| 86 | // Watch for when we get updates to any of our games lists | ||
| 87 | gamesViewModel.isReloading.observe(viewLifecycleOwner) { isReloading -> | ||
| 88 | binding.swipeRefresh.isRefreshing = isReloading | ||
| 89 | |||
| 90 | if (!isReloading) { | ||
| 91 | if (gamesViewModel.games.value!!.isEmpty()) { | ||
| 92 | binding.noticeText.visibility = View.VISIBLE | ||
| 93 | } else { | ||
| 94 | binding.noticeText.visibility = View.GONE | ||
| 95 | } | ||
| 96 | } | ||
| 97 | } | ||
| 98 | gamesViewModel.games.observe(viewLifecycleOwner) { | ||
| 99 | (binding.gridGames.adapter as GameAdapter).submitList(it) | ||
| 100 | } | ||
| 101 | gamesViewModel.searchedGames.observe(viewLifecycleOwner) { | ||
| 102 | (binding.gridSearch.adapter as GameAdapter).submitList(it) | ||
| 103 | } | ||
| 104 | gamesViewModel.shouldSwapData.observe(viewLifecycleOwner) { shouldSwapData -> | ||
| 105 | if (shouldSwapData) { | ||
| 106 | (binding.gridGames.adapter as GameAdapter).submitList(gamesViewModel.games.value) | ||
| 107 | gamesViewModel.setShouldSwapData(false) | ||
| 108 | } | ||
| 109 | } | ||
| 110 | |||
| 111 | // Hide bottom navigation and FAB when using the search view | ||
| 112 | binding.searchView.addTransitionListener { _: SearchView, _: TransitionState, newState: TransitionState -> | ||
| 113 | when (newState) { | ||
| 114 | TransitionState.SHOWING, | ||
| 115 | TransitionState.SHOWN -> { | ||
| 116 | (binding.gridSearch.adapter as GameAdapter).submitList(emptyList()) | ||
| 117 | searchShown() | ||
| 118 | } | ||
| 119 | TransitionState.HIDDEN, | ||
| 120 | TransitionState.HIDING -> { | ||
| 121 | gamesViewModel.setSearchedGames(emptyList()) | ||
| 122 | searchHidden() | ||
| 123 | } | ||
| 124 | } | ||
| 125 | } | ||
| 126 | |||
| 127 | // Ensure that bottom navigation or FAB don't appear upon recreation | ||
| 128 | val searchState = binding.searchView.currentTransitionState | ||
| 129 | if (searchState == TransitionState.SHOWN) { | ||
| 130 | searchShown() | ||
| 131 | } else if (searchState == TransitionState.HIDDEN) { | ||
| 132 | searchHidden() | ||
| 133 | } | ||
| 134 | |||
| 135 | setInsets() | ||
| 136 | |||
| 137 | // Make sure the loading indicator appears even if the layout is told to refresh before being fully drawn | ||
| 138 | binding.swipeRefresh.post { | ||
| 139 | binding.swipeRefresh.isRefreshing = gamesViewModel.isReloading.value!! | ||
| 140 | } | ||
| 141 | } | ||
| 142 | |||
| 143 | override fun onDestroyView() { | ||
| 144 | super.onDestroyView() | ||
| 145 | _binding = null | ||
| 146 | } | ||
| 147 | |||
| 148 | private fun searchShown() { | ||
| 149 | homeViewModel.setNavigationVisible(false) | ||
| 150 | requireActivity().window.statusBarColor = | ||
| 151 | ContextCompat.getColor(requireContext(), android.R.color.transparent) | ||
| 152 | } | ||
| 153 | |||
| 154 | private fun searchHidden() { | ||
| 155 | homeViewModel.setNavigationVisible(true) | ||
| 156 | requireActivity().window.statusBarColor = ThemeHelper.getColorWithOpacity( | ||
| 157 | MaterialColors.getColor( | ||
| 158 | binding.root, | ||
| 159 | R.attr.colorSurface | ||
| 160 | ), ThemeHelper.SYSTEM_BAR_ALPHA | ||
| 161 | ) | ||
| 162 | } | ||
| 163 | |||
| 164 | private inner class ScoredGame(val score: Double, val item: Game) | ||
| 165 | |||
| 166 | private fun setUpSearch() { | ||
| 167 | binding.gridSearch.apply { | ||
| 168 | layoutManager = AutofitGridLayoutManager( | ||
| 169 | requireContext(), | ||
| 170 | requireContext().resources.getDimensionPixelSize(R.dimen.card_width) | ||
| 171 | ) | ||
| 172 | adapter = GameAdapter(requireActivity() as AppCompatActivity) | ||
| 173 | } | ||
| 174 | |||
| 175 | binding.searchView.editText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int -> | ||
| 176 | val searchTerm = text.toString().lowercase(Locale.getDefault()) | ||
| 177 | val searchAlgorithm = Jaccard(2) | ||
| 178 | val sortedList: List<Game> = gamesViewModel.games.value!!.mapNotNull { game -> | ||
| 179 | val title = game.title.lowercase(Locale.getDefault()) | ||
| 180 | val score = searchAlgorithm.similarity(searchTerm, title) | ||
| 181 | if (score > 0.03) { | ||
| 182 | ScoredGame(score, game) | ||
| 183 | } else { | ||
| 184 | null | ||
| 185 | } | ||
| 186 | }.sortedByDescending { it.score }.map { it.item } | ||
| 187 | gamesViewModel.setSearchedGames(sortedList) | ||
| 188 | } | ||
| 189 | } | ||
| 190 | |||
| 191 | private fun setInsets() = | ||
| 192 | ViewCompat.setOnApplyWindowInsetsListener(binding.gridGames) { view: View, windowInsets: WindowInsetsCompat -> | ||
| 193 | val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) | ||
| 194 | val extraListSpacing = resources.getDimensionPixelSize(R.dimen.spacing_med) | ||
| 195 | |||
| 196 | view.setPadding( | ||
| 197 | insets.left, | ||
| 198 | insets.top + resources.getDimensionPixelSize(R.dimen.spacing_search), | ||
| 199 | insets.right, | ||
| 200 | insets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_navigation) + extraListSpacing | ||
| 201 | ) | ||
| 202 | binding.gridSearch.updatePadding( | ||
| 203 | left = insets.left, | ||
| 204 | top = extraListSpacing, | ||
| 205 | right = insets.right, | ||
| 206 | bottom = insets.bottom + extraListSpacing | ||
| 207 | ) | ||
| 208 | |||
| 209 | binding.swipeRefresh.setSlingshotDistance( | ||
| 210 | resources.getDimensionPixelSize(R.dimen.spacing_refresh_slingshot) | ||
| 211 | ) | ||
| 212 | binding.swipeRefresh.setProgressViewOffset( | ||
| 213 | false, | ||
| 214 | insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_start), | ||
| 215 | insets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end) | ||
| 216 | ) | ||
| 217 | |||
| 218 | windowInsets | ||
| 219 | } | ||
| 220 | } | ||
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 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.ui.main | 4 | package org.yuzu.yuzu_emu.ui.main |
| 5 | 5 | ||
| 6 | import android.content.DialogInterface | ||
| 7 | import android.content.Intent | ||
| 8 | import android.os.Bundle | 6 | import android.os.Bundle |
| 9 | import android.view.Menu | ||
| 10 | import android.view.MenuItem | ||
| 11 | import android.view.View | 7 | import android.view.View |
| 12 | import android.widget.Toast | 8 | import android.view.ViewGroup.MarginLayoutParams |
| 13 | import androidx.activity.result.contract.ActivityResultContracts | 9 | import android.view.animation.PathInterpolator |
| 10 | import androidx.activity.viewModels | ||
| 14 | import androidx.appcompat.app.AppCompatActivity | 11 | import androidx.appcompat.app.AppCompatActivity |
| 15 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen | 12 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen |
| 16 | import androidx.core.view.ViewCompat | 13 | import androidx.core.view.ViewCompat |
| 17 | import androidx.core.view.WindowCompat | 14 | import androidx.core.view.WindowCompat |
| 18 | import androidx.core.view.WindowInsetsCompat | 15 | import androidx.core.view.WindowInsetsCompat |
| 19 | import androidx.core.view.updatePadding | 16 | import androidx.navigation.fragment.NavHostFragment |
| 20 | import androidx.lifecycle.lifecycleScope | 17 | import androidx.navigation.ui.setupWithNavController |
| 21 | import androidx.preference.PreferenceManager | 18 | import com.google.android.material.color.MaterialColors |
| 22 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | 19 | import com.google.android.material.elevation.ElevationOverlayProvider |
| 23 | import kotlinx.coroutines.Dispatchers | ||
| 24 | import kotlinx.coroutines.launch | ||
| 25 | import kotlinx.coroutines.withContext | ||
| 26 | import org.yuzu.yuzu_emu.NativeLibrary | ||
| 27 | import org.yuzu.yuzu_emu.R | 20 | import org.yuzu.yuzu_emu.R |
| 28 | import org.yuzu.yuzu_emu.activities.EmulationActivity | 21 | import org.yuzu.yuzu_emu.activities.EmulationActivity |
| 29 | import org.yuzu.yuzu_emu.databinding.ActivityMainBinding | 22 | import org.yuzu.yuzu_emu.databinding.ActivityMainBinding |
| 30 | import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding | 23 | import org.yuzu.yuzu_emu.model.HomeViewModel |
| 31 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity | ||
| 32 | import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment | ||
| 33 | import org.yuzu.yuzu_emu.utils.* | 24 | import org.yuzu.yuzu_emu.utils.* |
| 34 | import java.io.IOException | ||
| 35 | |||
| 36 | class MainActivity : AppCompatActivity(), MainView { | ||
| 37 | private var platformGamesFragment: PlatformGamesFragment? = null | ||
| 38 | private val presenter = MainPresenter(this) | ||
| 39 | 25 | ||
| 26 | class MainActivity : AppCompatActivity() { | ||
| 40 | private lateinit var binding: ActivityMainBinding | 27 | private lateinit var binding: ActivityMainBinding |
| 41 | 28 | ||
| 29 | private val homeViewModel: HomeViewModel by viewModels() | ||
| 30 | |||
| 42 | override fun onCreate(savedInstanceState: Bundle?) { | 31 | override fun onCreate(savedInstanceState: Bundle?) { |
| 43 | val splashScreen = installSplashScreen() | 32 | val splashScreen = installSplashScreen() |
| 44 | splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } | 33 | splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } |
| @@ -52,19 +41,36 @@ class MainActivity : AppCompatActivity(), MainView { | |||
| 52 | 41 | ||
| 53 | WindowCompat.setDecorFitsSystemWindows(window, false) | 42 | WindowCompat.setDecorFitsSystemWindows(window, false) |
| 54 | 43 | ||
| 55 | setSupportActionBar(binding.toolbarMain) | 44 | ThemeHelper.setNavigationBarColor( |
| 56 | presenter.onCreate() | 45 | this, |
| 57 | if (savedInstanceState == null) { | 46 | ElevationOverlayProvider(binding.navigationBar.context).compositeOverlay( |
| 58 | StartupHandler.handleInit(this) | 47 | MaterialColors.getColor(binding.navigationBar, R.attr.colorSurface), |
| 59 | platformGamesFragment = PlatformGamesFragment() | 48 | binding.navigationBar.elevation |
| 60 | supportFragmentManager.beginTransaction() | 49 | ) |
| 61 | .add(R.id.games_platform_frame, platformGamesFragment!!) | 50 | ) |
| 62 | .commit() | 51 | |
| 63 | } else { | 52 | // Set up a central host fragment that is controlled via bottom navigation with xml navigation |
| 64 | platformGamesFragment = supportFragmentManager.getFragment( | 53 | val navHostFragment = |
| 65 | savedInstanceState, | 54 | supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment |
| 66 | PlatformGamesFragment.TAG | 55 | binding.navigationBar.setupWithNavController(navHostFragment.navController) |
| 67 | ) as PlatformGamesFragment? | 56 | |
| 57 | binding.statusBarShade.setBackgroundColor( | ||
| 58 | ThemeHelper.getColorWithOpacity( | ||
| 59 | MaterialColors.getColor( | ||
| 60 | binding.root, | ||
| 61 | R.attr.colorSurface | ||
| 62 | ), ThemeHelper.SYSTEM_BAR_ALPHA | ||
| 63 | ) | ||
| 64 | ) | ||
| 65 | |||
| 66 | // Prevents navigation from being drawn for a short time on recreation if set to hidden | ||
| 67 | if (homeViewModel.navigationVisible.value == false) { | ||
| 68 | binding.navigationBar.visibility = View.INVISIBLE | ||
| 69 | binding.statusBarShade.visibility = View.INVISIBLE | ||
| 70 | } | ||
| 71 | |||
| 72 | homeViewModel.navigationVisible.observe(this) { visible -> | ||
| 73 | showNavigation(visible) | ||
| 68 | } | 74 | } |
| 69 | 75 | ||
| 70 | // Dismiss previous notifications (should not happen unless a crash occurred) | 76 | // Dismiss previous notifications (should not happen unless a crash occurred) |
| @@ -73,78 +79,24 @@ class MainActivity : AppCompatActivity(), MainView { | |||
| 73 | setInsets() | 79 | setInsets() |
| 74 | } | 80 | } |
| 75 | 81 | ||
| 76 | override fun onSaveInstanceState(outState: Bundle) { | 82 | private fun showNavigation(visible: Boolean) { |
| 77 | super.onSaveInstanceState(outState) | 83 | binding.navigationBar.animate().apply { |
| 78 | supportFragmentManager.putFragment( | 84 | if (visible) { |
| 79 | outState, | 85 | binding.navigationBar.visibility = View.VISIBLE |
| 80 | PlatformGamesFragment.TAG, | 86 | binding.navigationBar.translationY = binding.navigationBar.height.toFloat() * 2 |
| 81 | platformGamesFragment!! | 87 | duration = 300 |
| 82 | ) | 88 | translationY(0f) |
| 83 | } | 89 | interpolator = PathInterpolator(0.05f, 0.7f, 0.1f, 1f) |
| 84 | 90 | } else { | |
| 85 | override fun onCreateOptionsMenu(menu: Menu): Boolean { | 91 | duration = 300 |
| 86 | menuInflater.inflate(R.menu.menu_game_grid, menu) | 92 | translationY(binding.navigationBar.height.toFloat() * 2) |
| 87 | return true | 93 | interpolator = PathInterpolator(0.3f, 0f, 0.8f, 0.15f) |
| 88 | } | ||
| 89 | |||
| 90 | /** | ||
| 91 | * MainView | ||
| 92 | */ | ||
| 93 | override fun setVersionString(version: String) { | ||
| 94 | binding.toolbarMain.subtitle = version | ||
| 95 | } | ||
| 96 | |||
| 97 | override fun launchSettingsActivity(menuTag: String) { | ||
| 98 | SettingsActivity.launch(this, menuTag, "") | ||
| 99 | } | ||
| 100 | |||
| 101 | override fun launchFileListActivity(request: Int) { | ||
| 102 | when (request) { | ||
| 103 | MainPresenter.REQUEST_ADD_DIRECTORY -> getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) | ||
| 104 | MainPresenter.REQUEST_INSTALL_KEYS -> getProdKey.launch(arrayOf("*/*")) | ||
| 105 | MainPresenter.REQUEST_INSTALL_AMIIBO_KEYS -> getAmiiboKey.launch(arrayOf("*/*")) | ||
| 106 | MainPresenter.REQUEST_SELECT_GPU_DRIVER -> { | ||
| 107 | // Get the driver name for the dialog message. | ||
| 108 | var driverName = GpuDriverHelper.customDriverName | ||
| 109 | if (driverName == null) { | ||
| 110 | driverName = getString(R.string.system_gpu_driver) | ||
| 111 | } | ||
| 112 | |||
| 113 | MaterialAlertDialogBuilder(this) | ||
| 114 | .setTitle(getString(R.string.select_gpu_driver_title)) | ||
| 115 | .setMessage(driverName) | ||
| 116 | .setNegativeButton(android.R.string.cancel, null) | ||
| 117 | .setPositiveButton(R.string.select_gpu_driver_default) { _: DialogInterface?, _: Int -> | ||
| 118 | GpuDriverHelper.installDefaultDriver(this) | ||
| 119 | Toast.makeText( | ||
| 120 | this, | ||
| 121 | R.string.select_gpu_driver_use_default, | ||
| 122 | Toast.LENGTH_SHORT | ||
| 123 | ).show() | ||
| 124 | } | ||
| 125 | .setNeutralButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int -> | ||
| 126 | getDriver.launch(arrayOf("application/zip")) | ||
| 127 | } | ||
| 128 | .show() | ||
| 129 | } | 94 | } |
| 130 | } | 95 | }.withEndAction { |
| 131 | } | 96 | if (!visible) { |
| 132 | 97 | binding.navigationBar.visibility = View.INVISIBLE | |
| 133 | /** | 98 | } |
| 134 | * Called by the framework whenever any actionbar/toolbar icon is clicked. | 99 | }.start() |
| 135 | * | ||
| 136 | * @param item The icon that was clicked on. | ||
| 137 | * @return True if the event was handled, false to bubble it up to the OS. | ||
| 138 | */ | ||
| 139 | override fun onOptionsItemSelected(item: MenuItem): Boolean { | ||
| 140 | return presenter.handleOptionSelection(item.itemId) | ||
| 141 | } | ||
| 142 | |||
| 143 | private fun refreshFragment() { | ||
| 144 | if (platformGamesFragment != null) { | ||
| 145 | NativeLibrary.resetRomMetadata() | ||
| 146 | platformGamesFragment!!.refresh() | ||
| 147 | } | ||
| 148 | } | 100 | } |
| 149 | 101 | ||
| 150 | override fun onDestroy() { | 102 | override fun onDestroy() { |
| @@ -152,145 +104,12 @@ class MainActivity : AppCompatActivity(), MainView { | |||
| 152 | super.onDestroy() | 104 | super.onDestroy() |
| 153 | } | 105 | } |
| 154 | 106 | ||
| 155 | private fun setInsets() { | 107 | private fun setInsets() = |
| 156 | ViewCompat.setOnApplyWindowInsetsListener(binding.gamesPlatformFrame) { view: View, windowInsets: WindowInsetsCompat -> | 108 | ViewCompat.setOnApplyWindowInsetsListener(binding.statusBarShade) { view: View, windowInsets: WindowInsetsCompat -> |
| 157 | val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) | 109 | val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) |
| 158 | view.updatePadding(left = insets.left, right = insets.right) | 110 | val mlpShade = view.layoutParams as MarginLayoutParams |
| 159 | InsetsHelper.insetAppBar(insets, binding.appbarMain) | 111 | mlpShade.height = insets.top |
| 112 | binding.statusBarShade.layoutParams = mlpShade | ||
| 160 | windowInsets | 113 | windowInsets |
| 161 | } | 114 | } |
| 162 | } | ||
| 163 | |||
| 164 | private val getGamesDirectory = | ||
| 165 | registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> | ||
| 166 | if (result == null) | ||
| 167 | return@registerForActivityResult | ||
| 168 | |||
| 169 | val takeFlags = | ||
| 170 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||
| 171 | contentResolver.takePersistableUriPermission( | ||
| 172 | result, | ||
| 173 | takeFlags | ||
| 174 | ) | ||
| 175 | |||
| 176 | // When a new directory is picked, we currently will reset the existing games | ||
| 177 | // database. This effectively means that only one game directory is supported. | ||
| 178 | PreferenceManager.getDefaultSharedPreferences(applicationContext).edit() | ||
| 179 | .putString(GameHelper.KEY_GAME_PATH, result.toString()) | ||
| 180 | .apply() | ||
| 181 | } | ||
| 182 | |||
| 183 | private val getProdKey = | ||
| 184 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> | ||
| 185 | if (result == null) | ||
| 186 | return@registerForActivityResult | ||
| 187 | |||
| 188 | val takeFlags = | ||
| 189 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||
| 190 | contentResolver.takePersistableUriPermission( | ||
| 191 | result, | ||
| 192 | takeFlags | ||
| 193 | ) | ||
| 194 | |||
| 195 | val dstPath = DirectoryInitialization.userDirectory + "/keys/" | ||
| 196 | if (FileUtil.copyUriToInternalStorage(this, result, dstPath, "prod.keys")) { | ||
| 197 | if (NativeLibrary.reloadKeys()) { | ||
| 198 | Toast.makeText( | ||
| 199 | this, | ||
| 200 | R.string.install_keys_success, | ||
| 201 | Toast.LENGTH_SHORT | ||
| 202 | ).show() | ||
| 203 | refreshFragment() | ||
| 204 | } else { | ||
| 205 | Toast.makeText( | ||
| 206 | this, | ||
| 207 | R.string.install_keys_failure, | ||
| 208 | Toast.LENGTH_LONG | ||
| 209 | ).show() | ||
| 210 | } | ||
| 211 | } | ||
| 212 | } | ||
| 213 | |||
| 214 | private val getAmiiboKey = | ||
| 215 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> | ||
| 216 | if (result == null) | ||
| 217 | return@registerForActivityResult | ||
| 218 | |||
| 219 | val takeFlags = | ||
| 220 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||
| 221 | contentResolver.takePersistableUriPermission( | ||
| 222 | result, | ||
| 223 | takeFlags | ||
| 224 | ) | ||
| 225 | |||
| 226 | val dstPath = DirectoryInitialization.userDirectory + "/keys/" | ||
| 227 | if (FileUtil.copyUriToInternalStorage(this, result, dstPath, "key_retail.bin")) { | ||
| 228 | if (NativeLibrary.reloadKeys()) { | ||
| 229 | Toast.makeText( | ||
| 230 | this, | ||
| 231 | R.string.install_keys_success, | ||
| 232 | Toast.LENGTH_SHORT | ||
| 233 | ).show() | ||
| 234 | refreshFragment() | ||
| 235 | } else { | ||
| 236 | Toast.makeText( | ||
| 237 | this, | ||
| 238 | R.string.install_amiibo_keys_failure, | ||
| 239 | Toast.LENGTH_LONG | ||
| 240 | ).show() | ||
| 241 | } | ||
| 242 | } | ||
| 243 | } | ||
| 244 | |||
| 245 | private val getDriver = | ||
| 246 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> | ||
| 247 | if (result == null) | ||
| 248 | return@registerForActivityResult | ||
| 249 | |||
| 250 | val takeFlags = | ||
| 251 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||
| 252 | contentResolver.takePersistableUriPermission( | ||
| 253 | result, | ||
| 254 | takeFlags | ||
| 255 | ) | ||
| 256 | |||
| 257 | val progressBinding = DialogProgressBarBinding.inflate(layoutInflater) | ||
| 258 | progressBinding.progressBar.isIndeterminate = true | ||
| 259 | val installationDialog = MaterialAlertDialogBuilder(this) | ||
| 260 | .setTitle(R.string.installing_driver) | ||
| 261 | .setView(progressBinding.root) | ||
| 262 | .show() | ||
| 263 | |||
| 264 | lifecycleScope.launch { | ||
| 265 | withContext(Dispatchers.IO) { | ||
| 266 | // Ignore file exceptions when a user selects an invalid zip | ||
| 267 | try { | ||
| 268 | GpuDriverHelper.installCustomDriver(applicationContext, result) | ||
| 269 | } catch (_: IOException) { | ||
| 270 | } | ||
| 271 | |||
| 272 | withContext(Dispatchers.Main) { | ||
| 273 | installationDialog.dismiss() | ||
| 274 | |||
| 275 | val driverName = GpuDriverHelper.customDriverName | ||
| 276 | if (driverName != null) { | ||
| 277 | Toast.makeText( | ||
| 278 | applicationContext, | ||
| 279 | getString( | ||
| 280 | R.string.select_gpu_driver_install_success, | ||
| 281 | driverName | ||
| 282 | ), | ||
| 283 | Toast.LENGTH_SHORT | ||
| 284 | ).show() | ||
| 285 | } else { | ||
| 286 | Toast.makeText( | ||
| 287 | applicationContext, | ||
| 288 | R.string.select_gpu_driver_error, | ||
| 289 | Toast.LENGTH_LONG | ||
| 290 | ).show() | ||
| 291 | } | ||
| 292 | } | ||
| 293 | } | ||
| 294 | } | ||
| 295 | } | ||
| 296 | } | 115 | } |
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 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.ui.main | ||
| 5 | |||
| 6 | import org.yuzu.yuzu_emu.BuildConfig | ||
| 7 | import org.yuzu.yuzu_emu.R | ||
| 8 | import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile | ||
| 9 | |||
| 10 | class MainPresenter(private val view: MainView) { | ||
| 11 | fun onCreate() { | ||
| 12 | val versionName = BuildConfig.VERSION_NAME | ||
| 13 | view.setVersionString(versionName) | ||
| 14 | } | ||
| 15 | |||
| 16 | private fun launchFileListActivity(request: Int) { | ||
| 17 | view.launchFileListActivity(request) | ||
| 18 | } | ||
| 19 | |||
| 20 | fun handleOptionSelection(itemId: Int): Boolean { | ||
| 21 | when (itemId) { | ||
| 22 | R.id.menu_settings_core -> { | ||
| 23 | view.launchSettingsActivity(SettingsFile.FILE_NAME_CONFIG) | ||
| 24 | return true | ||
| 25 | } | ||
| 26 | R.id.button_add_directory -> { | ||
| 27 | launchFileListActivity(REQUEST_ADD_DIRECTORY) | ||
| 28 | return true | ||
| 29 | } | ||
| 30 | R.id.button_install_keys -> { | ||
| 31 | launchFileListActivity(REQUEST_INSTALL_KEYS) | ||
| 32 | return true | ||
| 33 | } | ||
| 34 | R.id.button_install_amiibo_keys -> { | ||
| 35 | launchFileListActivity(REQUEST_INSTALL_AMIIBO_KEYS) | ||
| 36 | return true | ||
| 37 | } | ||
| 38 | R.id.button_select_gpu_driver -> { | ||
| 39 | launchFileListActivity(REQUEST_SELECT_GPU_DRIVER) | ||
| 40 | return true | ||
| 41 | } | ||
| 42 | } | ||
| 43 | return false | ||
| 44 | } | ||
| 45 | |||
| 46 | companion object { | ||
| 47 | const val REQUEST_ADD_DIRECTORY = 1 | ||
| 48 | const val REQUEST_INSTALL_KEYS = 2 | ||
| 49 | const val REQUEST_INSTALL_AMIIBO_KEYS = 3 | ||
| 50 | const val REQUEST_SELECT_GPU_DRIVER = 4 | ||
| 51 | } | ||
| 52 | } | ||
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 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.ui.main | ||
| 5 | |||
| 6 | /** | ||
| 7 | * Abstraction for the screen that shows on application launch. | ||
| 8 | * Implementations will differ primarily to target touch-screen | ||
| 9 | * or non-touch screen devices. | ||
| 10 | */ | ||
| 11 | interface MainView { | ||
| 12 | /** | ||
| 13 | * Pass the view the native library's version string. Displaying | ||
| 14 | * it is optional. | ||
| 15 | * | ||
| 16 | * @param version A string pulled from native code. | ||
| 17 | */ | ||
| 18 | fun setVersionString(version: String) | ||
| 19 | |||
| 20 | fun launchSettingsActivity(menuTag: String) | ||
| 21 | |||
| 22 | fun launchFileListActivity(request: Int) | ||
| 23 | } | ||
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 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.ui.platform | ||
| 5 | |||
| 6 | import android.os.Bundle | ||
| 7 | import android.view.LayoutInflater | ||
| 8 | import android.view.View | ||
| 9 | import android.view.ViewGroup | ||
| 10 | import androidx.appcompat.app.AppCompatActivity | ||
| 11 | import androidx.core.view.ViewCompat | ||
| 12 | import androidx.core.view.WindowInsetsCompat | ||
| 13 | import androidx.core.view.updatePadding | ||
| 14 | import androidx.fragment.app.Fragment | ||
| 15 | import androidx.lifecycle.ViewModelProvider | ||
| 16 | import com.google.android.material.color.MaterialColors | ||
| 17 | import org.yuzu.yuzu_emu.R | ||
| 18 | import org.yuzu.yuzu_emu.adapters.GameAdapter | ||
| 19 | import org.yuzu.yuzu_emu.databinding.FragmentGridBinding | ||
| 20 | import org.yuzu.yuzu_emu.layout.AutofitGridLayoutManager | ||
| 21 | import org.yuzu.yuzu_emu.model.GamesViewModel | ||
| 22 | import org.yuzu.yuzu_emu.utils.GameHelper | ||
| 23 | |||
| 24 | class PlatformGamesFragment : Fragment() { | ||
| 25 | private var _binding: FragmentGridBinding? = null | ||
| 26 | private val binding get() = _binding!! | ||
| 27 | |||
| 28 | private lateinit var gamesViewModel: GamesViewModel | ||
| 29 | |||
| 30 | override fun onCreateView( | ||
| 31 | inflater: LayoutInflater, | ||
| 32 | container: ViewGroup?, | ||
| 33 | savedInstanceState: Bundle? | ||
| 34 | ): View { | ||
| 35 | _binding = FragmentGridBinding.inflate(inflater) | ||
| 36 | return binding.root | ||
| 37 | } | ||
| 38 | |||
| 39 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| 40 | gamesViewModel = ViewModelProvider(requireActivity())[GamesViewModel::class.java] | ||
| 41 | |||
| 42 | binding.gridGames.apply { | ||
| 43 | layoutManager = AutofitGridLayoutManager( | ||
| 44 | requireContext(), | ||
| 45 | requireContext().resources.getDimensionPixelSize(R.dimen.card_width) | ||
| 46 | ) | ||
| 47 | adapter = | ||
| 48 | GameAdapter(requireActivity() as AppCompatActivity, gamesViewModel.games.value!!) | ||
| 49 | } | ||
| 50 | |||
| 51 | // Add swipe down to refresh gesture | ||
| 52 | binding.swipeRefresh.setOnRefreshListener { | ||
| 53 | refresh() | ||
| 54 | binding.swipeRefresh.isRefreshing = false | ||
| 55 | } | ||
| 56 | |||
| 57 | // Set theme color to the refresh animation's background | ||
| 58 | binding.swipeRefresh.setProgressBackgroundColorSchemeColor( | ||
| 59 | MaterialColors.getColor(binding.swipeRefresh, R.attr.colorPrimary) | ||
| 60 | ) | ||
| 61 | binding.swipeRefresh.setColorSchemeColors( | ||
| 62 | MaterialColors.getColor(binding.swipeRefresh, R.attr.colorOnPrimary) | ||
| 63 | ) | ||
| 64 | |||
| 65 | gamesViewModel.games.observe(viewLifecycleOwner) { | ||
| 66 | (binding.gridGames.adapter as GameAdapter).swapData(it) | ||
| 67 | updateTextView() | ||
| 68 | } | ||
| 69 | |||
| 70 | setInsets() | ||
| 71 | |||
| 72 | refresh() | ||
| 73 | } | ||
| 74 | |||
| 75 | override fun onResume() { | ||
| 76 | super.onResume() | ||
| 77 | refresh() | ||
| 78 | } | ||
| 79 | |||
| 80 | override fun onDestroyView() { | ||
| 81 | super.onDestroyView() | ||
| 82 | _binding = null | ||
| 83 | } | ||
| 84 | |||
| 85 | fun refresh() { | ||
| 86 | gamesViewModel.setGames(GameHelper.getGames()) | ||
| 87 | updateTextView() | ||
| 88 | } | ||
| 89 | |||
| 90 | private fun updateTextView() { | ||
| 91 | if (_binding == null) | ||
| 92 | return | ||
| 93 | |||
| 94 | binding.gamelistEmptyText.visibility = | ||
| 95 | if ((binding.gridGames.adapter as GameAdapter).itemCount == 0) View.VISIBLE else View.GONE | ||
| 96 | } | ||
| 97 | |||
| 98 | private fun setInsets() { | ||
| 99 | ViewCompat.setOnApplyWindowInsetsListener(binding.gridGames) { view: View, windowInsets: WindowInsetsCompat -> | ||
| 100 | val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) | ||
| 101 | view.updatePadding(bottom = insets.bottom) | ||
| 102 | windowInsets | ||
| 103 | } | ||
| 104 | } | ||
| 105 | |||
| 106 | companion object { | ||
| 107 | const val TAG = "PlatformGamesFragment" | ||
| 108 | } | ||
| 109 | } | ||
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 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.utils | ||
| 5 | |||
| 6 | import androidx.preference.PreferenceManager | ||
| 7 | import android.text.Html | ||
| 8 | import android.text.method.LinkMovementMethod | ||
| 9 | import android.view.View | ||
| 10 | import android.widget.TextView | ||
| 11 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||
| 12 | import org.yuzu.yuzu_emu.R | ||
| 13 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 14 | import org.yuzu.yuzu_emu.features.settings.model.Settings | ||
| 15 | import org.yuzu.yuzu_emu.ui.main.MainActivity | ||
| 16 | import org.yuzu.yuzu_emu.ui.main.MainPresenter | ||
| 17 | |||
| 18 | object StartupHandler { | ||
| 19 | private val preferences = | ||
| 20 | PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) | ||
| 21 | |||
| 22 | private fun handleStartupPromptDismiss(parent: MainActivity) { | ||
| 23 | parent.launchFileListActivity(MainPresenter.REQUEST_INSTALL_KEYS) | ||
| 24 | } | ||
| 25 | |||
| 26 | private fun markFirstBoot() { | ||
| 27 | preferences.edit() | ||
| 28 | .putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false) | ||
| 29 | .apply() | ||
| 30 | } | ||
| 31 | |||
| 32 | fun handleInit(parent: MainActivity) { | ||
| 33 | if (preferences.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)) { | ||
| 34 | markFirstBoot() | ||
| 35 | val alert = MaterialAlertDialogBuilder(parent) | ||
| 36 | .setMessage(Html.fromHtml(parent.resources.getString(R.string.app_disclaimer))) | ||
| 37 | .setTitle(R.string.app_name) | ||
| 38 | .setIcon(R.drawable.ic_launcher) | ||
| 39 | .setPositiveButton(android.R.string.ok, null) | ||
| 40 | .setOnDismissListener { | ||
| 41 | handleStartupPromptDismiss(parent) | ||
| 42 | } | ||
| 43 | .show() | ||
| 44 | (alert.findViewById<View>(android.R.id.message) as TextView?)!!.movementMethod = | ||
| 45 | LinkMovementMethod.getInstance() | ||
| 46 | } | ||
| 47 | } | ||
| 48 | } | ||
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 | |||
| 15 | import kotlin.math.roundToInt | 15 | import kotlin.math.roundToInt |
| 16 | 16 | ||
| 17 | object ThemeHelper { | 17 | object ThemeHelper { |
| 18 | private const val NAV_BAR_ALPHA = 0.9f | 18 | const val SYSTEM_BAR_ALPHA = 0.9f |
| 19 | 19 | ||
| 20 | @JvmStatic | 20 | @JvmStatic |
| 21 | fun setTheme(activity: AppCompatActivity) { | 21 | fun setTheme(activity: AppCompatActivity) { |
| @@ -29,10 +29,6 @@ object ThemeHelper { | |||
| 29 | windowController.isAppearanceLightNavigationBars = isLightMode | 29 | windowController.isAppearanceLightNavigationBars = isLightMode |
| 30 | 30 | ||
| 31 | activity.window.statusBarColor = ContextCompat.getColor(activity, android.R.color.transparent) | 31 | activity.window.statusBarColor = ContextCompat.getColor(activity, android.R.color.transparent) |
| 32 | |||
| 33 | val navigationBarColor = | ||
| 34 | MaterialColors.getColor(activity.window.decorView, R.attr.colorSurface) | ||
| 35 | setNavigationBarColor(activity, navigationBarColor) | ||
| 36 | } | 32 | } |
| 37 | 33 | ||
| 38 | @JvmStatic | 34 | @JvmStatic |
| @@ -48,7 +44,7 @@ object ThemeHelper { | |||
| 48 | } else if (gestureType == InsetsHelper.THREE_BUTTON_NAVIGATION || | 44 | } else if (gestureType == InsetsHelper.THREE_BUTTON_NAVIGATION || |
| 49 | gestureType == InsetsHelper.TWO_BUTTON_NAVIGATION | 45 | gestureType == InsetsHelper.TWO_BUTTON_NAVIGATION |
| 50 | ) { | 46 | ) { |
| 51 | activity.window.navigationBarColor = getColorWithOpacity(color, NAV_BAR_ALPHA) | 47 | activity.window.navigationBarColor = getColorWithOpacity(color, SYSTEM_BAR_ALPHA) |
| 52 | } else { | 48 | } else { |
| 53 | activity.window.navigationBarColor = ContextCompat.getColor( | 49 | activity.window.navigationBarColor = ContextCompat.getColor( |
| 54 | activity.applicationContext, | 50 | activity.applicationContext, |
| @@ -58,7 +54,7 @@ object ThemeHelper { | |||
| 58 | } | 54 | } |
| 59 | 55 | ||
| 60 | @ColorInt | 56 | @ColorInt |
| 61 | private fun getColorWithOpacity(@ColorInt color: Int, alphaFactor: Float): Int { | 57 | fun getColorWithOpacity(@ColorInt color: Int, alphaFactor: Float): Int { |
| 62 | return Color.argb( | 58 | return Color.argb( |
| 63 | (alphaFactor * Color.alpha(color)).roundToInt(), Color.red(color), | 59 | (alphaFactor * Color.alpha(color)).roundToInt(), Color.red(color), |
| 64 | Color.green(color), Color.blue(color) | 60 | 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 @@ | |||
| 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | android:width="24dp" | ||
| 3 | android:height="24dp" | ||
| 4 | android:viewportWidth="24" | ||
| 5 | android:viewportHeight="24"> | ||
| 6 | <path | ||
| 7 | android:fillColor="?attr/colorControlNormal" | ||
| 8 | android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" /> | ||
| 9 | </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 @@ | |||
| 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | android:width="24dp" | ||
| 3 | android:height="24dp" | ||
| 4 | android:autoMirrored="true" | ||
| 5 | android:viewportWidth="24" | ||
| 6 | android:viewportHeight="24"> | ||
| 7 | <path | ||
| 8 | android:fillColor="?attr/colorControlNormal" | ||
| 9 | 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" /> | ||
| 10 | </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 @@ | |||
| 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | android:width="24dp" | ||
| 3 | android:height="24dp" | ||
| 4 | android:viewportWidth="24" | ||
| 5 | android:viewportHeight="24"> | ||
| 6 | <path | ||
| 7 | android:fillColor="?attr/colorControlNormal" | ||
| 8 | 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" /> | ||
| 9 | </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 @@ | |||
| 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | android:width="24dp" | ||
| 3 | android:height="24dp" | ||
| 4 | android:viewportWidth="24" | ||
| 5 | android:viewportHeight="24"> | ||
| 6 | <path | ||
| 7 | android:fillColor="?attr/colorControlNormal" | ||
| 8 | 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" /> | ||
| 9 | </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 @@ | |||
| 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | android:width="24dp" | ||
| 3 | android:height="24dp" | ||
| 4 | android:viewportWidth="24" | ||
| 5 | android:viewportHeight="24"> | ||
| 6 | <path | ||
| 7 | android:fillColor="?attr/colorControlNormal" | ||
| 8 | 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" /> | ||
| 9 | </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 @@ | |||
| 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | android:width="614.697dp" | ||
| 3 | android:height="683dp" | ||
| 4 | android:viewportWidth="614.4" | ||
| 5 | android:viewportHeight="682.67"> | ||
| 6 | <group> | ||
| 7 | <clip-path android:pathData="M-43,-46.67h699.6v777.33h-699.6z" /> | ||
| 8 | <path | ||
| 9 | android:fillColor="?attr/colorPrimary" | ||
| 10 | 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" /> | ||
| 11 | </group> | ||
| 12 | <group> | ||
| 13 | <clip-path android:pathData="M-43,-46.67h699.6v777.33h-699.6z" /> | ||
| 14 | <path | ||
| 15 | android:fillColor="?attr/colorPrimary" | ||
| 16 | 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" /> | ||
| 17 | </group> | ||
| 18 | </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 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | 1 | <?xml version="1.0" encoding="utf-8"?> |
| 2 | <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" | 2 | <androidx.constraintlayout.widget.ConstraintLayout |
| 3 | xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | xmlns:app="http://schemas.android.com/apk/res-auto" | 4 | xmlns:app="http://schemas.android.com/apk/res-auto" |
| 5 | xmlns:tools="http://schemas.android.com/tools" | ||
| 4 | android:id="@+id/coordinator_main" | 6 | android:id="@+id/coordinator_main" |
| 5 | android:layout_width="match_parent" | 7 | android:layout_width="match_parent" |
| 6 | android:layout_height="match_parent"> | 8 | android:layout_height="match_parent"> |
| 7 | 9 | ||
| 8 | <com.google.android.material.appbar.AppBarLayout | 10 | <androidx.fragment.app.FragmentContainerView |
| 9 | android:id="@+id/appbar_main" | 11 | android:id="@+id/fragment_container" |
| 10 | android:layout_width="match_parent" | 12 | android:name="androidx.navigation.fragment.NavHostFragment" |
| 11 | android:layout_height="wrap_content" | 13 | android:layout_width="0dp" |
| 12 | android:fitsSystemWindows="true" | 14 | android:layout_height="0dp" |
| 13 | app:liftOnScrollTargetViewId="@id/grid_games"> | 15 | app:defaultNavHost="true" |
| 14 | 16 | app:layout_constraintBottom_toBottomOf="parent" | |
| 15 | <androidx.appcompat.widget.Toolbar | 17 | app:layout_constraintLeft_toLeftOf="parent" |
| 16 | android:id="@+id/toolbar_main" | 18 | app:layout_constraintRight_toRightOf="parent" |
| 17 | android:layout_width="match_parent" | 19 | app:layout_constraintTop_toTopOf="parent" |
| 18 | android:layout_height="?attr/actionBarSize" /> | 20 | app:navGraph="@navigation/home_navigation" |
| 21 | tools:layout="@layout/fragment_games" /> | ||
| 19 | 22 | ||
| 20 | </com.google.android.material.appbar.AppBarLayout> | 23 | <com.google.android.material.bottomnavigation.BottomNavigationView |
| 21 | 24 | android:id="@+id/navigation_bar" | |
| 22 | <FrameLayout | ||
| 23 | android:id="@+id/games_platform_frame" | ||
| 24 | android:layout_width="match_parent" | 25 | android:layout_width="match_parent" |
| 25 | android:layout_height="match_parent" | 26 | android:layout_height="wrap_content" |
| 26 | app:layout_behavior="@string/appbar_scrolling_view_behavior" /> | 27 | app:layout_constraintBottom_toBottomOf="parent" |
| 28 | app:layout_constraintLeft_toLeftOf="parent" | ||
| 29 | app:layout_constraintRight_toRightOf="parent" | ||
| 30 | app:menu="@menu/menu_navigation" /> | ||
| 27 | 31 | ||
| 28 | </androidx.coordinatorlayout.widget.CoordinatorLayout> | 32 | </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 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 4 | xmlns:tools="http://schemas.android.com/tools" | ||
| 5 | style="?attr/materialCardViewFilledStyle" | ||
| 6 | android:id="@+id/option_card" | ||
| 7 | android:layout_width="match_parent" | ||
| 8 | android:layout_height="wrap_content" | ||
| 9 | android:layout_marginVertical="8dp" | ||
| 10 | android:layout_marginHorizontal="16dp" | ||
| 11 | android:background="?attr/selectableItemBackground" | ||
| 12 | android:clickable="true" | ||
| 13 | android:focusable="true"> | ||
| 14 | |||
| 15 | <LinearLayout | ||
| 16 | android:layout_width="match_parent" | ||
| 17 | android:layout_height="wrap_content"> | ||
| 18 | |||
| 19 | <ImageView | ||
| 20 | android:id="@+id/option_icon" | ||
| 21 | android:layout_width="24dp" | ||
| 22 | android:layout_height="24dp" | ||
| 23 | android:layout_marginStart="28dp" | ||
| 24 | android:layout_gravity="center_vertical" | ||
| 25 | app:tint="?attr/colorPrimary" /> | ||
| 26 | |||
| 27 | <LinearLayout | ||
| 28 | android:layout_width="match_parent" | ||
| 29 | android:layout_height="wrap_content" | ||
| 30 | android:layout_margin="16dp" | ||
| 31 | android:orientation="vertical"> | ||
| 32 | |||
| 33 | <com.google.android.material.textview.MaterialTextView | ||
| 34 | style="@style/TextAppearance.Material3.BodyMedium" | ||
| 35 | android:id="@+id/option_title" | ||
| 36 | android:layout_width="match_parent" | ||
| 37 | android:layout_height="wrap_content" | ||
| 38 | android:textAlignment="viewStart" | ||
| 39 | tools:text="@string/install_prod_keys" /> | ||
| 40 | |||
| 41 | <com.google.android.material.textview.MaterialTextView | ||
| 42 | style="@style/TextAppearance.Material3.BodySmall" | ||
| 43 | android:id="@+id/option_description" | ||
| 44 | android:layout_width="match_parent" | ||
| 45 | android:layout_height="wrap_content" | ||
| 46 | android:textAlignment="viewStart" | ||
| 47 | tools:text="@string/install_prod_keys_description" /> | ||
| 48 | |||
| 49 | </LinearLayout> | ||
| 50 | |||
| 51 | </LinearLayout> | ||
| 52 | |||
| 53 | </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 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <androidx.coordinatorlayout.widget.CoordinatorLayout | ||
| 3 | xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 4 | xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 5 | xmlns:tools="http://schemas.android.com/tools" | ||
| 6 | android:id="@+id/coordinator_main" | ||
| 7 | android:layout_width="match_parent" | ||
| 8 | android:layout_height="match_parent" | ||
| 9 | android:background="?attr/colorSurface"> | ||
| 10 | |||
| 11 | <androidx.swiperefreshlayout.widget.SwipeRefreshLayout | ||
| 12 | android:id="@+id/swipe_refresh" | ||
| 13 | android:layout_width="match_parent" | ||
| 14 | android:layout_height="match_parent" | ||
| 15 | android:clipToPadding="false" | ||
| 16 | app:layout_behavior="@string/searchbar_scrolling_view_behavior"> | ||
| 17 | |||
| 18 | <RelativeLayout | ||
| 19 | android:layout_width="match_parent" | ||
| 20 | android:layout_height="match_parent"> | ||
| 21 | |||
| 22 | <com.google.android.material.textview.MaterialTextView | ||
| 23 | android:id="@+id/notice_text" | ||
| 24 | style="@style/TextAppearance.Material3.BodyLarge" | ||
| 25 | android:layout_width="match_parent" | ||
| 26 | android:layout_height="match_parent" | ||
| 27 | android:gravity="center" | ||
| 28 | android:padding="@dimen/spacing_large" | ||
| 29 | android:text="@string/empty_gamelist" | ||
| 30 | tools:visibility="gone" /> | ||
| 31 | |||
| 32 | <androidx.recyclerview.widget.RecyclerView | ||
| 33 | android:id="@+id/grid_games" | ||
| 34 | android:layout_width="match_parent" | ||
| 35 | android:layout_height="match_parent" | ||
| 36 | android:clipToPadding="false" | ||
| 37 | tools:listitem="@layout/card_game" /> | ||
| 38 | |||
| 39 | </RelativeLayout> | ||
| 40 | |||
| 41 | </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> | ||
| 42 | |||
| 43 | <com.google.android.material.appbar.AppBarLayout | ||
| 44 | android:id="@+id/app_bar_search" | ||
| 45 | android:layout_width="match_parent" | ||
| 46 | android:layout_height="wrap_content" | ||
| 47 | app:liftOnScrollTargetViewId="@id/grid_games"> | ||
| 48 | |||
| 49 | <FrameLayout | ||
| 50 | android:layout_width="match_parent" | ||
| 51 | android:layout_height="wrap_content" | ||
| 52 | android:fitsSystemWindows="true"> | ||
| 53 | |||
| 54 | <com.google.android.material.search.SearchBar | ||
| 55 | android:id="@+id/search_bar" | ||
| 56 | android:layout_width="match_parent" | ||
| 57 | android:layout_height="wrap_content" | ||
| 58 | android:hint="@string/home_search_games" /> | ||
| 59 | |||
| 60 | </FrameLayout> | ||
| 61 | |||
| 62 | </com.google.android.material.appbar.AppBarLayout> | ||
| 63 | |||
| 64 | <com.google.android.material.search.SearchView | ||
| 65 | android:id="@+id/search_view" | ||
| 66 | android:layout_width="match_parent" | ||
| 67 | android:layout_height="match_parent" | ||
| 68 | android:hint="@string/home_search_games" | ||
| 69 | app:layout_anchor="@id/search_bar"> | ||
| 70 | |||
| 71 | <androidx.recyclerview.widget.RecyclerView | ||
| 72 | android:id="@+id/grid_search" | ||
| 73 | android:layout_width="match_parent" | ||
| 74 | android:layout_height="match_parent" | ||
| 75 | android:clipToPadding="false" | ||
| 76 | tools:listitem="@layout/card_game" /> | ||
| 77 | |||
| 78 | </com.google.android.material.search.SearchView> | ||
| 79 | |||
| 80 | </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 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <FrameLayout | ||
| 3 | xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 4 | xmlns:tools="http://schemas.android.com/tools" | ||
| 5 | android:layout_width="match_parent" | ||
| 6 | android:layout_height="match_parent"> | ||
| 7 | |||
| 8 | <androidx.swiperefreshlayout.widget.SwipeRefreshLayout | ||
| 9 | android:id="@+id/swipe_refresh" | ||
| 10 | android:layout_width="match_parent" | ||
| 11 | android:layout_height="match_parent"> | ||
| 12 | |||
| 13 | <RelativeLayout | ||
| 14 | android:layout_width="match_parent" | ||
| 15 | android:layout_height="match_parent"> | ||
| 16 | |||
| 17 | <TextView | ||
| 18 | android:id="@+id/gamelist_empty_text" | ||
| 19 | android:layout_width="match_parent" | ||
| 20 | android:layout_height="match_parent" | ||
| 21 | android:gravity="center" | ||
| 22 | android:text="@string/empty_gamelist" | ||
| 23 | android:textSize="18sp" | ||
| 24 | android:visibility="gone" /> | ||
| 25 | |||
| 26 | <androidx.recyclerview.widget.RecyclerView | ||
| 27 | android:id="@+id/grid_games" | ||
| 28 | android:layout_width="match_parent" | ||
| 29 | android:layout_height="match_parent" | ||
| 30 | android:clipToPadding="false" | ||
| 31 | tools:listitem="@layout/card_game" /> | ||
| 32 | |||
| 33 | </RelativeLayout> | ||
| 34 | |||
| 35 | </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> | ||
| 36 | |||
| 37 | </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 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <androidx.core.widget.NestedScrollView | ||
| 3 | xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 4 | android:id="@+id/scroll_view_options" | ||
| 5 | android:layout_width="match_parent" | ||
| 6 | android:layout_height="match_parent" | ||
| 7 | android:background="?attr/colorSurface" | ||
| 8 | android:clipToPadding="false"> | ||
| 9 | |||
| 10 | <androidx.appcompat.widget.LinearLayoutCompat | ||
| 11 | android:layout_width="match_parent" | ||
| 12 | android:layout_height="match_parent" | ||
| 13 | android:orientation="vertical" | ||
| 14 | android:background="?attr/colorSurface"> | ||
| 15 | |||
| 16 | <ImageView | ||
| 17 | android:layout_width="128dp" | ||
| 18 | android:layout_height="128dp" | ||
| 19 | android:layout_margin="64dp" | ||
| 20 | android:layout_gravity="center_horizontal" | ||
| 21 | android:src="@drawable/ic_yuzu_themed" /> | ||
| 22 | |||
| 23 | <androidx.recyclerview.widget.RecyclerView | ||
| 24 | android:id="@+id/options_list" | ||
| 25 | android:layout_width="match_parent" | ||
| 26 | android:layout_height="match_parent" /> | ||
| 27 | |||
| 28 | </androidx.appcompat.widget.LinearLayoutCompat> | ||
| 29 | |||
| 30 | </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 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <menu xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | xmlns:app="http://schemas.android.com/apk/res-auto"> | ||
| 4 | |||
| 5 | <item | ||
| 6 | android:id="@+id/button_file_menu" | ||
| 7 | android:icon="@drawable/ic_folder" | ||
| 8 | android:title="@string/select_game_folder" | ||
| 9 | app:showAsAction="ifRoom"> | ||
| 10 | |||
| 11 | <menu> | ||
| 12 | |||
| 13 | <item | ||
| 14 | android:id="@+id/button_add_directory" | ||
| 15 | android:icon="@drawable/ic_folder" | ||
| 16 | android:title="@string/select_game_folder" | ||
| 17 | app:showAsAction="ifRoom" /> | ||
| 18 | |||
| 19 | <item | ||
| 20 | android:id="@+id/button_install_keys" | ||
| 21 | android:icon="@drawable/ic_install" | ||
| 22 | android:title="@string/install_keys" | ||
| 23 | app:showAsAction="ifRoom" /> | ||
| 24 | |||
| 25 | <item | ||
| 26 | android:id="@+id/button_install_amiibo_keys" | ||
| 27 | android:icon="@drawable/ic_install" | ||
| 28 | android:title="@string/install_amiibo_keys" | ||
| 29 | app:showAsAction="ifRoom" /> | ||
| 30 | |||
| 31 | <item | ||
| 32 | android:id="@+id/button_select_gpu_driver" | ||
| 33 | android:icon="@drawable/ic_settings" | ||
| 34 | android:title="@string/select_gpu_driver" | ||
| 35 | app:showAsAction="ifRoom" /> | ||
| 36 | |||
| 37 | </menu> | ||
| 38 | |||
| 39 | </item> | ||
| 40 | |||
| 41 | <item | ||
| 42 | android:id="@+id/menu_settings_core" | ||
| 43 | android:icon="@drawable/ic_settings" | ||
| 44 | android:title="@string/grid_menu_core_settings" | ||
| 45 | app:showAsAction="ifRoom" /> | ||
| 46 | |||
| 47 | </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 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <menu xmlns:android="http://schemas.android.com/apk/res/android"> | ||
| 3 | |||
| 4 | <item | ||
| 5 | android:id="@+id/gamesFragment" | ||
| 6 | android:icon="@drawable/ic_controller" | ||
| 7 | android:title="@string/home_games" /> | ||
| 8 | |||
| 9 | <item | ||
| 10 | android:id="@+id/optionsFragment" | ||
| 11 | android:icon="@drawable/ic_options" | ||
| 12 | android:title="@string/home_options" /> | ||
| 13 | |||
| 14 | </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 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <navigation xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 4 | android:id="@+id/home_navigation" | ||
| 5 | app:startDestination="@id/gamesFragment"> | ||
| 6 | |||
| 7 | <fragment | ||
| 8 | android:id="@+id/gamesFragment" | ||
| 9 | android:name="org.yuzu.yuzu_emu.ui.GamesFragment" | ||
| 10 | android:label="PlatformGamesFragment" /> | ||
| 11 | |||
| 12 | <fragment | ||
| 13 | android:id="@+id/optionsFragment" | ||
| 14 | android:name="org.yuzu.yuzu_emu.fragments.OptionsFragment" | ||
| 15 | android:label="OptionsFragment" /> | ||
| 16 | |||
| 17 | </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 @@ | |||
| 1 | <resources> | 1 | <resources> |
| 2 | <dimen name="spacing_small">4dp</dimen> | 2 | <dimen name="spacing_small">4dp</dimen> |
| 3 | <dimen name="spacing_med">8dp</dimen> | ||
| 3 | <dimen name="spacing_medlarge">12dp</dimen> | 4 | <dimen name="spacing_medlarge">12dp</dimen> |
| 4 | <dimen name="spacing_large">16dp</dimen> | 5 | <dimen name="spacing_large">16dp</dimen> |
| 5 | <dimen name="spacing_xtralarge">32dp</dimen> | 6 | <dimen name="spacing_xtralarge">32dp</dimen> |
| 6 | <dimen name="spacing_list">64dp</dimen> | 7 | <dimen name="spacing_list">64dp</dimen> |
| 7 | <dimen name="spacing_fab">72dp</dimen> | 8 | <dimen name="spacing_navigation">80dp</dimen> |
| 9 | <dimen name="spacing_search">88dp</dimen> | ||
| 10 | <dimen name="spacing_refresh_slingshot">80dp</dimen> | ||
| 11 | <dimen name="spacing_refresh_start">32dp</dimen> | ||
| 12 | <dimen name="spacing_refresh_end">96dp</dimen> | ||
| 8 | <dimen name="menu_width">256dp</dimen> | 13 | <dimen name="menu_width">256dp</dimen> |
| 9 | <dimen name="card_width">160dp</dimen> | 14 | <dimen name="card_width">160dp</dimen> |
| 10 | 15 | ||
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 @@ | |||
| 9 | <string name="app_notification_channel_description">yuzu Switch emulator notifications</string> | 9 | <string name="app_notification_channel_description">yuzu Switch emulator notifications</string> |
| 10 | <string name="app_notification_running">yuzu is running</string> | 10 | <string name="app_notification_running">yuzu is running</string> |
| 11 | 11 | ||
| 12 | <!-- Home strings --> | ||
| 13 | <string name="home_games">Games</string> | ||
| 14 | <string name="home_options">Options</string> | ||
| 15 | <string name="add_games">Add Games</string> | ||
| 16 | <string name="add_games_description">Select your games folder</string> | ||
| 17 | <string name="home_search_games">Search Games</string> | ||
| 18 | <string name="install_prod_keys">Install Prod.keys</string> | ||
| 19 | <string name="install_prod_keys_description">Required to decrypt retail games</string> | ||
| 20 | <string name="install_amiibo_keys">Install Amiibo Keys</string> | ||
| 21 | <string name="install_amiibo_keys_description">Required to use Amiibo in game</string> | ||
| 22 | <string name="install_keys_success">Keys successfully installed</string> | ||
| 23 | <string name="install_keys_failure">Keys file (prod.keys) is invalid</string> | ||
| 24 | <string name="install_amiibo_keys_failure">Keys file (key_retail.bin) is invalid</string> | ||
| 25 | <string name="install_gpu_driver">Install GPU Driver</string> | ||
| 26 | <string name="install_gpu_driver_description">Use a different driver for potentially better performance or accuracy</string> | ||
| 27 | <string name="settings">Settings</string> | ||
| 28 | <string name="settings_description">Configure emulator settings</string> | ||
| 29 | |||
| 12 | <!-- General settings strings --> | 30 | <!-- General settings strings --> |
| 13 | <string name="frame_limit_enable">Enable limit speed</string> | 31 | <string name="frame_limit_enable">Enable limit speed</string> |
| 14 | <string name="frame_limit_enable_description">When enabled, emulation speed will be limited to a specified percentage of normal speed.</string> | 32 | <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 @@ | |||
| 51 | <string name="error_saving">Error saving %1$s.ini: %2$s</string> | 69 | <string name="error_saving">Error saving %1$s.ini: %2$s</string> |
| 52 | <string name="loading">Loading...</string> | 70 | <string name="loading">Loading...</string> |
| 53 | 71 | ||
| 54 | <!-- Game Grid Screen--> | ||
| 55 | <string name="grid_menu_core_settings">Settings</string> | ||
| 56 | |||
| 57 | <!-- Add Directory Screen--> | ||
| 58 | <string name="select_game_folder">Select game folder</string> | ||
| 59 | <string name="install_keys">Install keys</string> | ||
| 60 | <string name="install_amiibo_keys">Install amiibo keys</string> | ||
| 61 | <string name="install_keys_success">Keys successfully installed</string> | ||
| 62 | <string name="install_keys_failure">Keys file (prod.keys) is invalid</string> | ||
| 63 | <string name="install_amiibo_keys_failure">Keys file (key_retail.bin) is invalid</string> | ||
| 64 | |||
| 65 | <!-- GPU driver installation --> | 72 | <!-- GPU driver installation --> |
| 66 | <string name="select_gpu_driver">Select GPU driver</string> | 73 | <string name="select_gpu_driver">Select GPU driver</string> |
| 67 | <string name="select_gpu_driver_title">Would you like to replace your current GPU driver?</string> | 74 | <string name="select_gpu_driver_title">Would you like to replace your current GPU driver?</string> |