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