Compare commits
7 Commits
android-14
...
android-14
Author | SHA1 | Date | |
---|---|---|---|
21e859ff2b | |||
b1b0ce1a59 | |||
2ef3268944 | |||
c7649a0cdb | |||
5a96c525e3 | |||
281eb020ea | |||
f61cf14646 |
@ -260,6 +260,11 @@ if (UNIX)
|
||||
add_definitions(-DYUZU_UNIX=1)
|
||||
endif()
|
||||
|
||||
if (ARCHITECTURE_arm64 AND (ANDROID OR ${CMAKE_SYSTEM_NAME} STREQUAL "Linux"))
|
||||
set(HAS_NCE 1)
|
||||
add_definitions(-DHAS_NCE=1)
|
||||
endif()
|
||||
|
||||
# Configure C++ standard
|
||||
# ===========================
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
| Pull Request | Commit | Title | Author | Merged? |
|
||||
|----|----|----|----|----|
|
||||
| [12074](https://github.com/yuzu-emu/yuzu//pull/12074) | [`15f35b865`](https://github.com/yuzu-emu/yuzu//pull/12074/files) | Implement Native Code Execution (NCE) | [GPUCode](https://github.com/GPUCode/) | Yes |
|
||||
| [12074](https://github.com/yuzu-emu/yuzu//pull/12074) | [`a76a8fb5f`](https://github.com/yuzu-emu/yuzu//pull/12074/files) | Implement Native Code Execution (NCE) | [GPUCode](https://github.com/GPUCode/) | Yes |
|
||||
| [12204](https://github.com/yuzu-emu/yuzu//pull/12204) | [`caa83609f`](https://github.com/yuzu-emu/yuzu//pull/12204/files) | android: Multi directory UI | [t895](https://github.com/t895/) | Yes |
|
||||
|
||||
|
||||
End of merge log. You can find the original README.md below the break.
|
||||
|
@ -0,0 +1,76 @@
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.adapters
|
||||
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.yuzu.yuzu_emu.databinding.CardFolderBinding
|
||||
import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment
|
||||
import org.yuzu.yuzu_emu.model.GameDir
|
||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||
|
||||
class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) :
|
||||
ListAdapter<GameDir, FolderAdapter.FolderViewHolder>(
|
||||
AsyncDifferConfig.Builder(DiffCallback()).build()
|
||||
) {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): FolderAdapter.FolderViewHolder {
|
||||
CardFolderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
.also { return FolderViewHolder(it) }
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: FolderAdapter.FolderViewHolder, position: Int) =
|
||||
holder.bind(currentList[position])
|
||||
|
||||
inner class FolderViewHolder(val binding: CardFolderBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
private lateinit var gameDir: GameDir
|
||||
|
||||
fun bind(gameDir: GameDir) {
|
||||
this.gameDir = gameDir
|
||||
|
||||
binding.apply {
|
||||
path.text = Uri.parse(gameDir.uriString).path
|
||||
path.postDelayed(
|
||||
{
|
||||
path.isSelected = true
|
||||
path.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||
},
|
||||
3000
|
||||
)
|
||||
|
||||
buttonEdit.setOnClickListener {
|
||||
GameFolderPropertiesDialogFragment.newInstance(this@FolderViewHolder.gameDir)
|
||||
.show(
|
||||
activity.supportFragmentManager,
|
||||
GameFolderPropertiesDialogFragment.TAG
|
||||
)
|
||||
}
|
||||
|
||||
buttonDelete.setOnClickListener {
|
||||
gamesViewModel.removeFolder(this@FolderViewHolder.gameDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<GameDir>() {
|
||||
override fun areItemsTheSame(oldItem: GameDir, newItem: GameDir): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: GameDir, newItem: GameDir): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
@ -3,33 +3,9 @@
|
||||
|
||||
package org.yuzu.yuzu_emu.features.settings.model
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.widget.Toast
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
object Settings {
|
||||
private val context get() = YuzuApplication.appContext
|
||||
|
||||
fun saveSettings(gameId: String = "") {
|
||||
if (TextUtils.isEmpty(gameId)) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.ini_saved),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
NativeConfig.saveSettings()
|
||||
} else {
|
||||
// TODO: Save custom game settings
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.gameid_saved, gameId),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
enum class Category {
|
||||
Android,
|
||||
Audio,
|
||||
|
@ -19,12 +19,13 @@ import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.navArgs
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
|
||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
|
||||
import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment
|
||||
import org.yuzu.yuzu_emu.model.SettingsViewModel
|
||||
@ -53,10 +54,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
settingsViewModel.shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE)
|
||||
}
|
||||
|
||||
if (InsetsHelper.getSystemGestureType(applicationContext) !=
|
||||
InsetsHelper.GESTURE_NAVIGATION
|
||||
) {
|
||||
@ -127,12 +124,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
// Critical: If super method is not called, rotations will be busted.
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putBoolean(KEY_SHOULD_SAVE, settingsViewModel.shouldSave)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
// TODO: Load custom settings contextually
|
||||
@ -141,16 +132,10 @@ class SettingsActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If this is called, the user has left the settings screen (potentially through the
|
||||
* home button) and will expect their changes to be persisted. So we kick off an
|
||||
* IntentService which will do so on a background thread.
|
||||
*/
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
if (isFinishing && settingsViewModel.shouldSave) {
|
||||
Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
|
||||
Settings.saveSettings()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
NativeConfig.saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
@ -160,9 +145,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
fun onSettingsReset() {
|
||||
// Prevents saving to a non-existent settings file
|
||||
settingsViewModel.shouldSave = false
|
||||
|
||||
// Delete settings file because the user may have changed values that do not exist in the UI
|
||||
NativeConfig.unloadConfig()
|
||||
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
|
||||
@ -194,8 +176,4 @@ class SettingsActivity : AppCompatActivity() {
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_SHOULD_SAVE = "should_save"
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +105,6 @@ class SettingsAdapter(
|
||||
fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
|
||||
item.checked = checked
|
||||
settingsViewModel.setShouldReloadSettingsList(true)
|
||||
settingsViewModel.shouldSave = true
|
||||
}
|
||||
|
||||
fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
|
||||
@ -161,7 +160,6 @@ class SettingsAdapter(
|
||||
epochTime += timePicker.hour.toLong() * 60 * 60
|
||||
epochTime += timePicker.minute.toLong() * 60
|
||||
if (item.value != epochTime) {
|
||||
settingsViewModel.shouldSave = true
|
||||
notifyItemChanged(position)
|
||||
item.value = epochTime
|
||||
}
|
||||
|
@ -0,0 +1,53 @@
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.databinding.DialogAddFolderBinding
|
||||
import org.yuzu.yuzu_emu.model.GameDir
|
||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||
|
||||
class AddGameFolderDialogFragment : DialogFragment() {
|
||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val binding = DialogAddFolderBinding.inflate(layoutInflater)
|
||||
val folderUriString = requireArguments().getString(FOLDER_URI_STRING)
|
||||
if (folderUriString == null) {
|
||||
dismiss()
|
||||
}
|
||||
binding.path.text = Uri.parse(folderUriString).path
|
||||
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.add_game_folder)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||
val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked)
|
||||
gamesViewModel.addFolder(newGameDir)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setView(binding.root)
|
||||
.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "AddGameFolderDialogFragment"
|
||||
|
||||
private const val FOLDER_URI_STRING = "FolderUriString"
|
||||
|
||||
fun newInstance(folderUriString: String): AddGameFolderDialogFragment {
|
||||
val args = Bundle()
|
||||
args.putString(FOLDER_URI_STRING, folderUriString)
|
||||
val fragment = AddGameFolderDialogFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding
|
||||
import org.yuzu.yuzu_emu.model.GameDir
|
||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||
import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
|
||||
|
||||
class GameFolderPropertiesDialogFragment : DialogFragment() {
|
||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||
|
||||
private var deepScan = false
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val binding = DialogFolderPropertiesBinding.inflate(layoutInflater)
|
||||
val gameDir = requireArguments().parcelable<GameDir>(GAME_DIR)!!
|
||||
|
||||
// Restore checkbox state
|
||||
binding.deepScanSwitch.isChecked =
|
||||
savedInstanceState?.getBoolean(DEEP_SCAN) ?: gameDir.deepScan
|
||||
|
||||
// Ensure that we can get the checkbox state even if the view is destroyed
|
||||
deepScan = binding.deepScanSwitch.isChecked
|
||||
binding.deepScanSwitch.setOnClickListener {
|
||||
deepScan = binding.deepScanSwitch.isChecked
|
||||
}
|
||||
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(binding.root)
|
||||
.setTitle(R.string.game_folder_properties)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||
val folderIndex = gamesViewModel.folders.value.indexOf(gameDir)
|
||||
if (folderIndex != -1) {
|
||||
gamesViewModel.folders.value[folderIndex].deepScan =
|
||||
binding.deepScanSwitch.isChecked
|
||||
gamesViewModel.updateGameDirs()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putBoolean(DEEP_SCAN, deepScan)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "GameFolderPropertiesDialogFragment"
|
||||
|
||||
private const val GAME_DIR = "GameDir"
|
||||
|
||||
private const val DEEP_SCAN = "DeepScan"
|
||||
|
||||
fun newInstance(gameDir: GameDir): GameFolderPropertiesDialogFragment {
|
||||
val args = Bundle()
|
||||
args.putParcelable(GAME_DIR, gameDir)
|
||||
val fragment = GameFolderPropertiesDialogFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import kotlinx.coroutines.launch
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.adapters.FolderAdapter
|
||||
import org.yuzu.yuzu_emu.databinding.FragmentFoldersBinding
|
||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||
|
||||
class GameFoldersFragment : Fragment() {
|
||||
private var _binding: FragmentFoldersBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||
private val gamesViewModel: GamesViewModel by activityViewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||
|
||||
gamesViewModel.onOpenGameFoldersFragment()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentFoldersBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
||||
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||
|
||||
binding.toolbarFolders.setNavigationOnClickListener {
|
||||
binding.root.findNavController().popBackStack()
|
||||
}
|
||||
|
||||
binding.listFolders.apply {
|
||||
layoutManager = GridLayoutManager(
|
||||
requireContext(),
|
||||
resources.getInteger(R.integer.grid_columns)
|
||||
)
|
||||
adapter = FolderAdapter(requireActivity(), gamesViewModel)
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
gamesViewModel.folders.collect {
|
||||
(binding.listFolders.adapter as FolderAdapter).submitList(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val mainActivity = requireActivity() as MainActivity
|
||||
binding.buttonAdd.setOnClickListener {
|
||||
mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
|
||||
}
|
||||
|
||||
setInsets()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
gamesViewModel.onCloseGameFoldersFragment()
|
||||
}
|
||||
|
||||
private fun setInsets() =
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.root
|
||||
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||
|
||||
val leftInsets = barInsets.left + cutoutInsets.left
|
||||
val rightInsets = barInsets.right + cutoutInsets.right
|
||||
|
||||
val mlpToolbar = binding.toolbarFolders.layoutParams as ViewGroup.MarginLayoutParams
|
||||
mlpToolbar.leftMargin = leftInsets
|
||||
mlpToolbar.rightMargin = rightInsets
|
||||
binding.toolbarFolders.layoutParams = mlpToolbar
|
||||
|
||||
val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
|
||||
val mlpFab =
|
||||
binding.buttonAdd.layoutParams as ViewGroup.MarginLayoutParams
|
||||
mlpFab.leftMargin = leftInsets + fabSpacing
|
||||
mlpFab.rightMargin = rightInsets + fabSpacing
|
||||
mlpFab.bottomMargin = barInsets.bottom + fabSpacing
|
||||
binding.buttonAdd.layoutParams = mlpFab
|
||||
|
||||
val mlpListFolders = binding.listFolders.layoutParams as ViewGroup.MarginLayoutParams
|
||||
mlpListFolders.leftMargin = leftInsets
|
||||
mlpListFolders.rightMargin = rightInsets
|
||||
binding.listFolders.layoutParams = mlpListFolders
|
||||
|
||||
binding.listFolders.updatePadding(
|
||||
bottom = barInsets.bottom +
|
||||
resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
|
||||
)
|
||||
|
||||
windowInsets
|
||||
}
|
||||
}
|
@ -127,18 +127,13 @@ class HomeSettingsFragment : Fragment() {
|
||||
)
|
||||
add(
|
||||
HomeSetting(
|
||||
R.string.select_games_folder,
|
||||
R.string.manage_game_folders,
|
||||
R.string.select_games_folder_description,
|
||||
R.drawable.ic_add,
|
||||
{
|
||||
mainActivity.getGamesDirectory.launch(
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data
|
||||
)
|
||||
},
|
||||
{ true },
|
||||
0,
|
||||
0,
|
||||
homeViewModel.gamesDir
|
||||
binding.root.findNavController()
|
||||
.navigate(R.id.action_homeSettingsFragment_to_gameFoldersFragment)
|
||||
}
|
||||
)
|
||||
)
|
||||
add(
|
||||
|
@ -52,7 +52,6 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||
settingsViewModel.clickedItem!!.setting.reset()
|
||||
settingsViewModel.setAdapterItemChanged(position)
|
||||
settingsViewModel.shouldSave = true
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
@ -137,24 +136,17 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
||||
is SingleChoiceSetting -> {
|
||||
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
|
||||
val value = getValueForSingleChoiceSelection(scSetting, which)
|
||||
if (scSetting.selectedValue != value) {
|
||||
settingsViewModel.shouldSave = true
|
||||
}
|
||||
scSetting.selectedValue = value
|
||||
}
|
||||
|
||||
is StringSingleChoiceSetting -> {
|
||||
val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
|
||||
val value = scSetting.getValueAt(which)
|
||||
if (scSetting.selectedValue != value) settingsViewModel.shouldSave = true
|
||||
scSetting.selectedValue = value
|
||||
}
|
||||
|
||||
is SliderSetting -> {
|
||||
val sliderSetting = settingsViewModel.clickedItem as SliderSetting
|
||||
if (sliderSetting.selectedValue != settingsViewModel.sliderProgress.value) {
|
||||
settingsViewModel.shouldSave = true
|
||||
}
|
||||
sliderSetting.selectedValue = settingsViewModel.sliderProgress.value
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ import org.yuzu.yuzu_emu.model.SetupPage
|
||||
import org.yuzu.yuzu_emu.model.StepState
|
||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||
import org.yuzu.yuzu_emu.utils.GameHelper
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils
|
||||
|
||||
class SetupFragment : Fragment() {
|
||||
@ -184,11 +184,7 @@ class SetupFragment : Fragment() {
|
||||
R.string.add_games_warning_description,
|
||||
R.string.add_games_warning_help,
|
||||
{
|
||||
val preferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(
|
||||
YuzuApplication.appContext
|
||||
)
|
||||
if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) {
|
||||
if (NativeConfig.getGameDirs().isNotEmpty()) {
|
||||
StepState.COMPLETE
|
||||
} else {
|
||||
StepState.INCOMPLETE
|
||||
|
@ -0,0 +1,13 @@
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class GameDir(
|
||||
val uriString: String,
|
||||
var deepScan: Boolean
|
||||
) : Parcelable
|
@ -12,6 +12,7 @@ import java.util.Locale
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.decodeFromString
|
||||
@ -20,6 +21,7 @@ import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.utils.GameHelper
|
||||
import org.yuzu.yuzu_emu.utils.GameMetadata
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
class GamesViewModel : ViewModel() {
|
||||
val games: StateFlow<List<Game>> get() = _games
|
||||
@ -40,6 +42,9 @@ class GamesViewModel : ViewModel() {
|
||||
val searchFocused: StateFlow<Boolean> get() = _searchFocused
|
||||
private val _searchFocused = MutableStateFlow(false)
|
||||
|
||||
private val _folders = MutableStateFlow(mutableListOf<GameDir>())
|
||||
val folders = _folders.asStateFlow()
|
||||
|
||||
init {
|
||||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
||||
NativeLibrary.reloadKeys()
|
||||
@ -50,6 +55,7 @@ class GamesViewModel : ViewModel() {
|
||||
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
getGameDirs()
|
||||
if (storedGames!!.isNotEmpty()) {
|
||||
val deserializedGames = mutableSetOf<Game>()
|
||||
storedGames.forEach {
|
||||
@ -104,7 +110,7 @@ class GamesViewModel : ViewModel() {
|
||||
_searchFocused.value = searchFocused
|
||||
}
|
||||
|
||||
fun reloadGames(directoryChanged: Boolean) {
|
||||
fun reloadGames(directoriesChanged: Boolean) {
|
||||
if (isReloading.value) {
|
||||
return
|
||||
}
|
||||
@ -116,10 +122,61 @@ class GamesViewModel : ViewModel() {
|
||||
setGames(GameHelper.getGames())
|
||||
_isReloading.value = false
|
||||
|
||||
if (directoryChanged) {
|
||||
if (directoriesChanged) {
|
||||
setShouldSwapData(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addFolder(gameDir: GameDir) =
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
NativeConfig.addGameDir(gameDir)
|
||||
getGameDirs()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFolder(gameDir: GameDir) =
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val gameDirs = _folders.value.toMutableList()
|
||||
val removedDirIndex = gameDirs.indexOf(gameDir)
|
||||
if (removedDirIndex != -1) {
|
||||
gameDirs.removeAt(removedDirIndex)
|
||||
NativeConfig.setGameDirs(gameDirs.toTypedArray())
|
||||
getGameDirs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateGameDirs() =
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
NativeConfig.setGameDirs(_folders.value.toTypedArray())
|
||||
getGameDirs()
|
||||
}
|
||||
}
|
||||
|
||||
fun onOpenGameFoldersFragment() =
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
getGameDirs()
|
||||
}
|
||||
}
|
||||
|
||||
fun onCloseGameFoldersFragment() =
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
getGameDirs(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getGameDirs(reloadList: Boolean = false) {
|
||||
val gameDirs = NativeConfig.getGameDirs()
|
||||
_folders.value = gameDirs.toMutableList()
|
||||
if (reloadList) {
|
||||
reloadGames(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,15 +3,9 @@
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.utils.GameHelper
|
||||
|
||||
class HomeViewModel : ViewModel() {
|
||||
val navigationVisible: StateFlow<Pair<Boolean, Boolean>> get() = _navigationVisible
|
||||
@ -23,14 +17,6 @@ class HomeViewModel : ViewModel() {
|
||||
val shouldPageForward: StateFlow<Boolean> get() = _shouldPageForward
|
||||
private val _shouldPageForward = MutableStateFlow(false)
|
||||
|
||||
val gamesDir: StateFlow<String> get() = _gamesDir
|
||||
private val _gamesDir = MutableStateFlow(
|
||||
Uri.parse(
|
||||
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||
.getString(GameHelper.KEY_GAME_PATH, "")
|
||||
).path ?: ""
|
||||
)
|
||||
|
||||
var navigatedToSetup = false
|
||||
|
||||
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
|
||||
@ -50,9 +36,4 @@ class HomeViewModel : ViewModel() {
|
||||
fun setShouldPageForward(pageForward: Boolean) {
|
||||
_shouldPageForward.value = pageForward
|
||||
}
|
||||
|
||||
fun setGamesDir(activity: FragmentActivity, dir: String) {
|
||||
ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
|
||||
_gamesDir.value = dir
|
||||
}
|
||||
}
|
||||
|
@ -13,8 +13,6 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
||||
class SettingsViewModel : ViewModel() {
|
||||
var game: Game? = null
|
||||
|
||||
var shouldSave = false
|
||||
|
||||
var clickedItem: SettingsItem? = null
|
||||
|
||||
val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate
|
||||
@ -73,6 +71,5 @@ class SettingsViewModel : ViewModel() {
|
||||
|
||||
fun clear() {
|
||||
game = null
|
||||
shouldSave = false
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ 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.features.settings.model.Settings
|
||||
import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
|
||||
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
|
||||
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
||||
import org.yuzu.yuzu_emu.getPublicFilesDir
|
||||
@ -252,6 +253,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
NativeConfig.saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
EmulationActivity.stopForegroundService(this)
|
||||
super.onDestroy()
|
||||
@ -293,20 +301,19 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
)
|
||||
|
||||
// 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()
|
||||
val uriString = result.toString()
|
||||
val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString }
|
||||
if (folder != null) {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.folder_already_added,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return
|
||||
}
|
||||
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.games_dir_selected,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
gamesViewModel.reloadGames(true)
|
||||
homeViewModel.setGamesDir(this, result.path!!)
|
||||
AddGameFolderDialogFragment.newInstance(uriString)
|
||||
.show(supportFragmentManager, AddGameFolderDialogFragment.TAG)
|
||||
}
|
||||
|
||||
val getProdKey =
|
||||
|
@ -364,6 +364,27 @@ object FileUtil {
|
||||
.lowercase()
|
||||
}
|
||||
|
||||
fun isTreeUriValid(uri: Uri): Boolean {
|
||||
val resolver = context.contentResolver
|
||||
val columns = arrayOf(
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE
|
||||
)
|
||||
return try {
|
||||
val docId: String = if (isRootTreeUri(uri)) {
|
||||
DocumentsContract.getTreeDocumentId(uri)
|
||||
} else {
|
||||
DocumentsContract.getDocumentId(uri)
|
||||
}
|
||||
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId)
|
||||
resolver.query(childrenUri, columns, null, null, null)
|
||||
true
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun getStringFromFile(file: File): String =
|
||||
String(file.readBytes(), StandardCharsets.UTF_8)
|
||||
|
@ -11,10 +11,11 @@ import kotlinx.serialization.json.Json
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
import org.yuzu.yuzu_emu.model.GameDir
|
||||
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
|
||||
|
||||
object GameHelper {
|
||||
const val KEY_GAME_PATH = "game_path"
|
||||
private const val KEY_OLD_GAME_PATH = "game_path"
|
||||
const val KEY_GAMES = "Games"
|
||||
|
||||
private lateinit var preferences: SharedPreferences
|
||||
@ -22,15 +23,43 @@ object GameHelper {
|
||||
fun getGames(): List<Game> {
|
||||
val games = mutableListOf<Game>()
|
||||
val context = YuzuApplication.appContext
|
||||
val gamesDir =
|
||||
PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "")
|
||||
val gamesUri = Uri.parse(gamesDir)
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
val gameDirs = mutableListOf<GameDir>()
|
||||
val oldGamesDir = preferences.getString(KEY_OLD_GAME_PATH, "") ?: ""
|
||||
if (oldGamesDir.isNotEmpty()) {
|
||||
gameDirs.add(GameDir(oldGamesDir, true))
|
||||
preferences.edit().remove(KEY_OLD_GAME_PATH).apply()
|
||||
}
|
||||
gameDirs.addAll(NativeConfig.getGameDirs())
|
||||
|
||||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
||||
NativeLibrary.reloadKeys()
|
||||
|
||||
addGamesRecursive(games, FileUtil.listFiles(gamesUri), 3)
|
||||
val badDirs = mutableListOf<Int>()
|
||||
gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
|
||||
val gameDirUri = Uri.parse(gameDir.uriString)
|
||||
val isValid = FileUtil.isTreeUriValid(gameDirUri)
|
||||
if (isValid) {
|
||||
addGamesRecursive(
|
||||
games,
|
||||
FileUtil.listFiles(gameDirUri),
|
||||
if (gameDir.deepScan) 3 else 1
|
||||
)
|
||||
} else {
|
||||
badDirs.add(index)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all game dirs with insufficient permissions from config
|
||||
if (badDirs.isNotEmpty()) {
|
||||
var offset = 0
|
||||
badDirs.forEach {
|
||||
gameDirs.removeAt(it - offset)
|
||||
offset++
|
||||
}
|
||||
NativeConfig.setGameDirs(gameDirs.toTypedArray())
|
||||
}
|
||||
|
||||
// Cache list of games found on disk
|
||||
val serializedGames = mutableSetOf<String>()
|
||||
|
@ -3,6 +3,8 @@
|
||||
|
||||
package org.yuzu.yuzu_emu.utils
|
||||
|
||||
import org.yuzu.yuzu_emu.model.GameDir
|
||||
|
||||
object NativeConfig {
|
||||
/**
|
||||
* Creates a Config object and opens the emulation config.
|
||||
@ -54,4 +56,22 @@ object NativeConfig {
|
||||
external fun getConfigHeader(category: Int): String
|
||||
|
||||
external fun getPairedSettingKey(key: String): String
|
||||
|
||||
/**
|
||||
* Gets every [GameDir] in AndroidSettings::values.game_dirs
|
||||
*/
|
||||
@Synchronized
|
||||
external fun getGameDirs(): Array<GameDir>
|
||||
|
||||
/**
|
||||
* Clears the AndroidSettings::values.game_dirs array and replaces them with the provided array
|
||||
*/
|
||||
@Synchronized
|
||||
external fun setGameDirs(dirs: Array<GameDir>)
|
||||
|
||||
/**
|
||||
* Adds a single [GameDir] to the AndroidSettings::values.game_dirs array
|
||||
*/
|
||||
@Synchronized
|
||||
external fun addGameDir(dir: GameDir)
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ void AndroidConfig::SaveAllValues() {
|
||||
void AndroidConfig::ReadAndroidValues() {
|
||||
if (global) {
|
||||
ReadAndroidUIValues();
|
||||
ReadUIValues();
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,9 +46,35 @@ void AndroidConfig::ReadAndroidUIValues() {
|
||||
EndGroup();
|
||||
}
|
||||
|
||||
void AndroidConfig::ReadUIValues() {
|
||||
BeginGroup(Settings::TranslateCategory(Settings::Category::Ui));
|
||||
|
||||
ReadPathValues();
|
||||
|
||||
EndGroup();
|
||||
}
|
||||
|
||||
void AndroidConfig::ReadPathValues() {
|
||||
BeginGroup(Settings::TranslateCategory(Settings::Category::Paths));
|
||||
|
||||
const int gamedirs_size = BeginArray(std::string("gamedirs"));
|
||||
for (int i = 0; i < gamedirs_size; ++i) {
|
||||
SetArrayIndex(i);
|
||||
AndroidSettings::GameDir game_dir;
|
||||
game_dir.path = ReadStringSetting(std::string("path"));
|
||||
game_dir.deep_scan =
|
||||
ReadBooleanSetting(std::string("deep_scan"), std::make_optional(false));
|
||||
AndroidSettings::values.game_dirs.push_back(game_dir);
|
||||
}
|
||||
EndArray();
|
||||
|
||||
EndGroup();
|
||||
}
|
||||
|
||||
void AndroidConfig::SaveAndroidValues() {
|
||||
if (global) {
|
||||
SaveAndroidUIValues();
|
||||
SaveUIValues();
|
||||
}
|
||||
|
||||
WriteToIni();
|
||||
@ -61,6 +88,29 @@ void AndroidConfig::SaveAndroidUIValues() {
|
||||
EndGroup();
|
||||
}
|
||||
|
||||
void AndroidConfig::SaveUIValues() {
|
||||
BeginGroup(Settings::TranslateCategory(Settings::Category::Ui));
|
||||
|
||||
SavePathValues();
|
||||
|
||||
EndGroup();
|
||||
}
|
||||
|
||||
void AndroidConfig::SavePathValues() {
|
||||
BeginGroup(Settings::TranslateCategory(Settings::Category::Paths));
|
||||
|
||||
BeginArray(std::string("gamedirs"));
|
||||
for (size_t i = 0; i < AndroidSettings::values.game_dirs.size(); ++i) {
|
||||
SetArrayIndex(i);
|
||||
const auto& game_dir = AndroidSettings::values.game_dirs[i];
|
||||
WriteSetting(std::string("path"), game_dir.path);
|
||||
WriteSetting(std::string("deep_scan"), game_dir.deep_scan, std::make_optional(false));
|
||||
}
|
||||
EndArray();
|
||||
|
||||
EndGroup();
|
||||
}
|
||||
|
||||
std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) {
|
||||
auto& map = Settings::values.linkage.by_category;
|
||||
if (map.contains(category)) {
|
||||
|
@ -19,9 +19,9 @@ protected:
|
||||
void ReadAndroidUIValues();
|
||||
void ReadHidbusValues() override {}
|
||||
void ReadDebugControlValues() override {}
|
||||
void ReadPathValues() override {}
|
||||
void ReadPathValues() override;
|
||||
void ReadShortcutValues() override {}
|
||||
void ReadUIValues() override {}
|
||||
void ReadUIValues() override;
|
||||
void ReadUIGamelistValues() override {}
|
||||
void ReadUILayoutValues() override {}
|
||||
void ReadMultiplayerValues() override {}
|
||||
@ -30,9 +30,9 @@ protected:
|
||||
void SaveAndroidUIValues();
|
||||
void SaveHidbusValues() override {}
|
||||
void SaveDebugControlValues() override {}
|
||||
void SavePathValues() override {}
|
||||
void SavePathValues() override;
|
||||
void SaveShortcutValues() override {}
|
||||
void SaveUIValues() override {}
|
||||
void SaveUIValues() override;
|
||||
void SaveUIGamelistValues() override {}
|
||||
void SaveUILayoutValues() override {}
|
||||
void SaveMultiplayerValues() override {}
|
||||
|
@ -9,9 +9,17 @@
|
||||
|
||||
namespace AndroidSettings {
|
||||
|
||||
struct GameDir {
|
||||
std::string path;
|
||||
bool deep_scan = false;
|
||||
};
|
||||
|
||||
struct Values {
|
||||
Settings::Linkage linkage;
|
||||
|
||||
// Path settings
|
||||
std::vector<GameDir> game_dirs;
|
||||
|
||||
// Android
|
||||
Settings::Setting<bool> picture_in_picture{linkage, false, "picture_in_picture",
|
||||
Settings::Category::Android};
|
||||
|
@ -13,6 +13,8 @@ static JavaVM* s_java_vm;
|
||||
static jclass s_native_library_class;
|
||||
static jclass s_disk_cache_progress_class;
|
||||
static jclass s_load_callback_stage_class;
|
||||
static jclass s_game_dir_class;
|
||||
static jmethodID s_game_dir_constructor;
|
||||
static jmethodID s_exit_emulation_activity;
|
||||
static jmethodID s_disk_cache_load_progress;
|
||||
static jmethodID s_on_emulation_started;
|
||||
@ -53,6 +55,14 @@ jclass GetDiskCacheLoadCallbackStageClass() {
|
||||
return s_load_callback_stage_class;
|
||||
}
|
||||
|
||||
jclass GetGameDirClass() {
|
||||
return s_game_dir_class;
|
||||
}
|
||||
|
||||
jmethodID GetGameDirConstructor() {
|
||||
return s_game_dir_constructor;
|
||||
}
|
||||
|
||||
jmethodID GetExitEmulationActivity() {
|
||||
return s_exit_emulation_activity;
|
||||
}
|
||||
@ -90,6 +100,11 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
s_load_callback_stage_class = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass(
|
||||
"org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage")));
|
||||
|
||||
const jclass game_dir_class = env->FindClass("org/yuzu/yuzu_emu/model/GameDir");
|
||||
s_game_dir_class = reinterpret_cast<jclass>(env->NewGlobalRef(game_dir_class));
|
||||
s_game_dir_constructor = env->GetMethodID(game_dir_class, "<init>", "(Ljava/lang/String;Z)V");
|
||||
env->DeleteLocalRef(game_dir_class);
|
||||
|
||||
// Initialize methods
|
||||
s_exit_emulation_activity =
|
||||
env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
|
||||
@ -120,6 +135,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
|
||||
env->DeleteGlobalRef(s_native_library_class);
|
||||
env->DeleteGlobalRef(s_disk_cache_progress_class);
|
||||
env->DeleteGlobalRef(s_load_callback_stage_class);
|
||||
env->DeleteGlobalRef(s_game_dir_class);
|
||||
|
||||
// UnInitialize applets
|
||||
SoftwareKeyboard::CleanupJNI(env);
|
||||
|
@ -13,6 +13,8 @@ JNIEnv* GetEnvForThread();
|
||||
jclass GetNativeLibraryClass();
|
||||
jclass GetDiskCacheProgressClass();
|
||||
jclass GetDiskCacheLoadCallbackStageClass();
|
||||
jclass GetGameDirClass();
|
||||
jmethodID GetGameDirConstructor();
|
||||
jmethodID GetExitEmulationActivity();
|
||||
jmethodID GetDiskCacheLoadProgress();
|
||||
jmethodID GetOnEmulationStarted();
|
||||
|
@ -11,6 +11,7 @@
|
||||
#include "common/settings.h"
|
||||
#include "frontend_common/config.h"
|
||||
#include "jni/android_common/android_common.h"
|
||||
#include "jni/id_cache.h"
|
||||
|
||||
std::unique_ptr<AndroidConfig> config;
|
||||
|
||||
@ -253,4 +254,55 @@ jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* e
|
||||
return ToJString(env, setting->PairedSetting()->GetLabel());
|
||||
}
|
||||
|
||||
jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getGameDirs(JNIEnv* env, jobject obj) {
|
||||
jclass gameDirClass = IDCache::GetGameDirClass();
|
||||
jmethodID gameDirConstructor = IDCache::GetGameDirConstructor();
|
||||
jobjectArray jgameDirArray =
|
||||
env->NewObjectArray(AndroidSettings::values.game_dirs.size(), gameDirClass, nullptr);
|
||||
for (size_t i = 0; i < AndroidSettings::values.game_dirs.size(); ++i) {
|
||||
jobject jgameDir =
|
||||
env->NewObject(gameDirClass, gameDirConstructor,
|
||||
ToJString(env, AndroidSettings::values.game_dirs[i].path),
|
||||
static_cast<jboolean>(AndroidSettings::values.game_dirs[i].deep_scan));
|
||||
env->SetObjectArrayElement(jgameDirArray, i, jgameDir);
|
||||
}
|
||||
return jgameDirArray;
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setGameDirs(JNIEnv* env, jobject obj,
|
||||
jobjectArray gameDirs) {
|
||||
AndroidSettings::values.game_dirs.clear();
|
||||
int size = env->GetArrayLength(gameDirs);
|
||||
|
||||
if (size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
jobject dir = env->GetObjectArrayElement(gameDirs, 0);
|
||||
jclass gameDirClass = IDCache::GetGameDirClass();
|
||||
jfieldID uriStringField = env->GetFieldID(gameDirClass, "uriString", "Ljava/lang/String;");
|
||||
jfieldID deepScanBooleanField = env->GetFieldID(gameDirClass, "deepScan", "Z");
|
||||
for (int i = 0; i < size; ++i) {
|
||||
dir = env->GetObjectArrayElement(gameDirs, i);
|
||||
jstring juriString = static_cast<jstring>(env->GetObjectField(dir, uriStringField));
|
||||
jboolean jdeepScanBoolean = env->GetBooleanField(dir, deepScanBooleanField);
|
||||
std::string uriString = GetJString(env, juriString);
|
||||
AndroidSettings::values.game_dirs.push_back(
|
||||
AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)});
|
||||
}
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_addGameDir(JNIEnv* env, jobject obj,
|
||||
jobject gameDir) {
|
||||
jclass gameDirClass = IDCache::GetGameDirClass();
|
||||
jfieldID uriStringField = env->GetFieldID(gameDirClass, "uriString", "Ljava/lang/String;");
|
||||
jfieldID deepScanBooleanField = env->GetFieldID(gameDirClass, "deepScan", "Z");
|
||||
|
||||
jstring juriString = static_cast<jstring>(env->GetObjectField(gameDir, uriStringField));
|
||||
jboolean jdeepScanBoolean = env->GetBooleanField(gameDir, deepScanBooleanField);
|
||||
std::string uriString = GetJString(env, juriString);
|
||||
AndroidSettings::values.game_dirs.push_back(
|
||||
AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)});
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
70
src/android/app/src/main/res/layout/card_folder.xml
Normal file
70
src/android/app/src/main/res/layout/card_folder.xml
Normal file
@ -0,0 +1,70 @@
|
||||
<?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/materialCardViewOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginVertical="12dp"
|
||||
android:focusable="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/path"
|
||||
style="@style/TextAppearance.Material3.BodyLarge"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical|start"
|
||||
android:ellipsize="none"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:requiresFadingEdge="horizontal"
|
||||
android:singleLine="true"
|
||||
android:textAlignment="viewStart"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/button_layout"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@string/select_gpu_driver_default" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/button_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_edit"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/delete"
|
||||
android:tooltipText="@string/edit"
|
||||
app:icon="@drawable/ic_edit"
|
||||
app:iconTint="?attr/colorControlNormal" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_delete"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/delete"
|
||||
android:tooltipText="@string/delete"
|
||||
app:icon="@drawable/ic_delete"
|
||||
app:iconTint="?attr/colorControlNormal" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
45
src/android/app/src/main/res/layout/dialog_add_folder.xml
Normal file
45
src/android/app/src/main/res/layout/dialog_add_folder.xml
Normal file
@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="24dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/path"
|
||||
style="@style/TextAppearance.Material3.BodyLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="center_vertical|start"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:requiresFadingEdge="horizontal"
|
||||
android:singleLine="true"
|
||||
android:textAlignment="viewStart"
|
||||
tools:text="folder/folder/folder/folder" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="8dp">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
style="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical|start"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/deep_scan"
|
||||
android:textAlignment="viewStart" />
|
||||
|
||||
<com.google.android.material.checkbox.MaterialCheckBox
|
||||
android:id="@+id/deep_scan_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="24dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/deep_scan_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
style="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical|start"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/deep_scan"
|
||||
android:textAlignment="viewStart" />
|
||||
|
||||
<com.google.android.material.checkbox.MaterialCheckBox
|
||||
android:id="@+id/deep_scan_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
48
src/android/app/src/main/res/layout/fragment_folders.xml
Normal file
48
src/android/app/src/main/res/layout/fragment_folders.xml
Normal file
@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/coordinator_folders"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar_folders"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
app:liftOnScrollTargetViewId="@id/list_folders">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_folders"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:navigationIcon="@drawable/ic_back"
|
||||
app:title="@string/game_folders" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list_folders"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/button_add"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:contentDescription="@string/add_games"
|
||||
app:srcCompat="@drawable/ic_add"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -28,6 +28,9 @@
|
||||
<action
|
||||
android:id="@+id/action_homeSettingsFragment_to_appletLauncherFragment"
|
||||
app:destination="@id/appletLauncherFragment" />
|
||||
<action
|
||||
android:id="@+id/action_homeSettingsFragment_to_gameFoldersFragment"
|
||||
app:destination="@id/gameFoldersFragment" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
@ -117,5 +120,9 @@
|
||||
android:id="@+id/cabinetLauncherDialogFragment"
|
||||
android:name="org.yuzu.yuzu_emu.fragments.CabinetLauncherDialogFragment"
|
||||
android:label="CabinetLauncherDialogFragment" />
|
||||
<fragment
|
||||
android:id="@+id/gameFoldersFragment"
|
||||
android:name="org.yuzu.yuzu_emu.fragments.GameFoldersFragment"
|
||||
android:label="GameFoldersFragment" />
|
||||
|
||||
</navigation>
|
||||
|
@ -13,7 +13,7 @@
|
||||
<dimen name="menu_width">256dp</dimen>
|
||||
<dimen name="card_width">165dp</dimen>
|
||||
<dimen name="icon_inset">24dp</dimen>
|
||||
<dimen name="spacing_bottom_list_fab">72dp</dimen>
|
||||
<dimen name="spacing_bottom_list_fab">76dp</dimen>
|
||||
<dimen name="spacing_fab">24dp</dimen>
|
||||
|
||||
<dimen name="dialog_margin">20dp</dimen>
|
||||
|
@ -38,6 +38,7 @@
|
||||
<string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
|
||||
<string name="search_and_filter_games">Search and filter games</string>
|
||||
<string name="select_games_folder">Select games folder</string>
|
||||
<string name="manage_game_folders">Manage game folders</string>
|
||||
<string name="select_games_folder_description">Allows yuzu to populate the games list</string>
|
||||
<string name="add_games_warning">Skip selecting games folder?</string>
|
||||
<string name="add_games_warning_description">Games won\'t be displayed in the Games list if a folder isn\'t selected.</string>
|
||||
@ -124,6 +125,11 @@
|
||||
<string name="manage_yuzu_data_description">Import/export firmware, keys, user data, and more!</string>
|
||||
<string name="share_save_file">Share save file</string>
|
||||
<string name="export_save_failed">Failed to export save</string>
|
||||
<string name="game_folders">Game folders</string>
|
||||
<string name="deep_scan">Deep scan</string>
|
||||
<string name="add_game_folder">Add game folder</string>
|
||||
<string name="folder_already_added">This folder was already added!</string>
|
||||
<string name="game_folder_properties">Game folder properties</string>
|
||||
|
||||
<!-- Applet launcher strings -->
|
||||
<string name="applets">Applet launcher</string>
|
||||
@ -258,6 +264,7 @@
|
||||
<string name="cancelling">Cancelling</string>
|
||||
<string name="install">Install</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="export_success">Exported successfully</string>
|
||||
|
||||
<!-- GPU driver installation -->
|
||||
|
@ -199,7 +199,7 @@ if(ARCHITECTURE_x86_64)
|
||||
target_link_libraries(common PRIVATE xbyak::xbyak)
|
||||
endif()
|
||||
|
||||
if (ARCHITECTURE_arm64 AND (ANDROID OR LINUX))
|
||||
if (HAS_NCE)
|
||||
target_sources(common
|
||||
PRIVATE
|
||||
arm64/native_clock.cpp
|
||||
|
@ -182,7 +182,7 @@ struct Values {
|
||||
// Cpu
|
||||
SwitchableSetting<CpuBackend, true> cpu_backend{
|
||||
linkage, CpuBackend::Dynarmic, CpuBackend::Dynarmic,
|
||||
#ifdef ARCHITECTURE_arm64
|
||||
#ifdef HAS_NCE
|
||||
CpuBackend::Nce,
|
||||
#else
|
||||
CpuBackend::Dynarmic,
|
||||
|
@ -10,7 +10,7 @@
|
||||
#include "common/x64/rdtsc.h"
|
||||
#endif
|
||||
|
||||
#if defined(ARCHITECTURE_arm64) && defined(__linux__)
|
||||
#ifdef HAS_NCE
|
||||
#include "common/arm64/native_clock.h"
|
||||
#endif
|
||||
|
||||
@ -68,7 +68,7 @@ std::unique_ptr<WallClock> CreateOptimalClock() {
|
||||
// - Is not more precise than 1 GHz (1ns resolution)
|
||||
return std::make_unique<StandardWallClock>();
|
||||
}
|
||||
#elif defined(ARCHITECTURE_arm64) && defined(__linux__)
|
||||
#elif defined(HAS_NCE)
|
||||
return std::make_unique<Arm64::NativeClock>();
|
||||
#else
|
||||
return std::make_unique<StandardWallClock>();
|
||||
|
@ -926,8 +926,7 @@ if (ENABLE_WEB_SERVICE)
|
||||
target_link_libraries(core PRIVATE web_service)
|
||||
endif()
|
||||
|
||||
if (ARCHITECTURE_arm64 AND (ANDROID OR ${CMAKE_SYSTEM_NAME} STREQUAL "Linux"))
|
||||
target_compile_definitions(core PRIVATE -DHAS_NCE)
|
||||
if (HAS_NCE)
|
||||
enable_language(C ASM)
|
||||
set(CMAKE_ASM_FLAGS "${CFLAGS} -x assembler-with-cpp")
|
||||
|
||||
|
@ -167,6 +167,11 @@ protected:
|
||||
*/
|
||||
std::pair<f32, f32> MapToTouchScreen(u32 framebuffer_x, u32 framebuffer_y) const;
|
||||
|
||||
/**
|
||||
* Clip the provided coordinates to be inside the touchscreen area.
|
||||
*/
|
||||
std::pair<u32, u32> ClipToTouchScreen(u32 new_x, u32 new_y) const;
|
||||
|
||||
WindowSystemInfo window_info;
|
||||
|
||||
bool strict_context_required = false;
|
||||
@ -181,11 +186,6 @@ private:
|
||||
// By default, ignore this request and do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Clip the provided coordinates to be inside the touchscreen area.
|
||||
*/
|
||||
std::pair<u32, u32> ClipToTouchScreen(u32 new_x, u32 new_y) const;
|
||||
|
||||
Layout::FramebufferLayout framebuffer_layout; ///< Current framebuffer layout
|
||||
|
||||
u32 client_area_width; ///< Current client width, should be set by window impl.
|
||||
|
@ -131,7 +131,7 @@ void Cabinet::DisplayCompleted(bool apply_changes, std::string_view amiibo_name)
|
||||
nfp_device->DeleteApplicationArea();
|
||||
break;
|
||||
case Service::NFP::CabinetMode::StartRestorer:
|
||||
nfp_device->RestoreAmiibo();
|
||||
nfp_device->Restore();
|
||||
break;
|
||||
case Service::NFP::CabinetMode::StartFormatter:
|
||||
nfp_device->Format();
|
||||
|
@ -98,7 +98,7 @@ void Ver3StoreData::BuildToStoreData(StoreData& out_store_data) const {
|
||||
}
|
||||
|
||||
void Ver3StoreData::BuildFromStoreData(const StoreData& store_data) {
|
||||
version = 1;
|
||||
version = 3;
|
||||
mii_information.gender.Assign(static_cast<u8>(store_data.GetGender()));
|
||||
mii_information.favorite_color.Assign(static_cast<u8>(store_data.GetFavoriteColor()));
|
||||
height = store_data.GetHeight();
|
||||
|
@ -401,6 +401,12 @@ Result NfcDevice::SendCommandByPassThrough(const Time::Clock::TimeSpanType& time
|
||||
}
|
||||
|
||||
Result NfcDevice::Mount(NFP::ModelType model_type, NFP::MountTarget mount_target_) {
|
||||
bool is_corrupted = false;
|
||||
|
||||
if (model_type != NFP::ModelType::Amiibo) {
|
||||
return ResultInvalidArgument;
|
||||
}
|
||||
|
||||
if (device_state != DeviceState::TagFound) {
|
||||
LOG_ERROR(Service_NFP, "Wrong device state {}", device_state);
|
||||
return ResultWrongDeviceState;
|
||||
@ -420,26 +426,32 @@ Result NfcDevice::Mount(NFP::ModelType model_type, NFP::MountTarget mount_target
|
||||
if (is_plain_amiibo) {
|
||||
std::vector<u8> data(sizeof(NFP::NTAG215File));
|
||||
memcpy(data.data(), &tag_data, sizeof(tag_data));
|
||||
WriteBackupData(tag_data.uid, data);
|
||||
|
||||
device_state = DeviceState::TagMounted;
|
||||
mount_target = mount_target_;
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
if (!NFP::AmiiboCrypto::DecodeAmiibo(encrypted_tag_data, tag_data)) {
|
||||
bool has_backup = HasBackup(encrypted_tag_data.uuid).IsSuccess();
|
||||
LOG_ERROR(Service_NFP, "Can't decode amiibo, has_backup= {}", has_backup);
|
||||
return has_backup ? ResultCorruptedDataWithBackup : ResultCorruptedData;
|
||||
if (!is_plain_amiibo && !NFP::AmiiboCrypto::DecodeAmiibo(encrypted_tag_data, tag_data)) {
|
||||
LOG_ERROR(Service_NFP, "Can't decode amiibo");
|
||||
is_corrupted = true;
|
||||
}
|
||||
|
||||
std::vector<u8> data(sizeof(NFP::EncryptedNTAG215File));
|
||||
memcpy(data.data(), &encrypted_tag_data, sizeof(encrypted_tag_data));
|
||||
WriteBackupData(encrypted_tag_data.uuid, data);
|
||||
if (tag_data.settings.settings.amiibo_initialized && !tag_data.owner_mii.IsValid()) {
|
||||
LOG_ERROR(Service_NFP, "Invalid mii data");
|
||||
is_corrupted = true;
|
||||
}
|
||||
|
||||
if (!is_corrupted) {
|
||||
std::vector<u8> data(sizeof(NFP::EncryptedNTAG215File));
|
||||
memcpy(data.data(), &encrypted_tag_data, sizeof(encrypted_tag_data));
|
||||
WriteBackupData(encrypted_tag_data.uuid, data);
|
||||
}
|
||||
|
||||
device_state = DeviceState::TagMounted;
|
||||
mount_target = mount_target_;
|
||||
|
||||
if (is_corrupted) {
|
||||
bool has_backup = HasBackup(encrypted_tag_data.uuid).IsSuccess();
|
||||
return has_backup ? ResultCorruptedDataWithBackup : ResultCorruptedData;
|
||||
}
|
||||
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
@ -606,6 +618,17 @@ Result NfcDevice::Restore() {
|
||||
}
|
||||
}
|
||||
|
||||
// Restore mii data in case is corrupted by previous instances of yuzu
|
||||
if (tag_data.settings.settings.amiibo_initialized && !tag_data.owner_mii.IsValid()) {
|
||||
LOG_ERROR(Service_NFP, "Regenerating mii data");
|
||||
Mii::StoreData new_mii{};
|
||||
new_mii.BuildRandom(Mii::Age::All, Mii::Gender::All, Mii::Race::All);
|
||||
new_mii.SetNickname({u'y', u'u', u'z', u'u', u'\0'});
|
||||
|
||||
tag_data.owner_mii.BuildFromStoreData(new_mii);
|
||||
tag_data.mii_extension.SetFromStoreData(new_mii);
|
||||
}
|
||||
|
||||
// Overwrite tag contents with backup and mount the tag
|
||||
tag_data = temporary_tag_data;
|
||||
encrypted_tag_data = temporary_encrypted_tag_data;
|
||||
@ -851,25 +874,6 @@ Result NfcDevice::SetRegisterInfoPrivate(const NFP::RegisterInfoPrivate& registe
|
||||
return Flush();
|
||||
}
|
||||
|
||||
Result NfcDevice::RestoreAmiibo() {
|
||||
if (device_state != DeviceState::TagMounted) {
|
||||
LOG_ERROR(Service_NFP, "Wrong device state {}", device_state);
|
||||
if (device_state == DeviceState::TagRemoved) {
|
||||
return ResultTagRemoved;
|
||||
}
|
||||
return ResultWrongDeviceState;
|
||||
}
|
||||
|
||||
if (mount_target == NFP::MountTarget::None || mount_target == NFP::MountTarget::Rom) {
|
||||
LOG_ERROR(Service_NFP, "Amiibo is read only", device_state);
|
||||
return ResultWrongDeviceState;
|
||||
}
|
||||
|
||||
// TODO: Load amiibo from backup on system
|
||||
LOG_ERROR(Service_NFP, "Not Implemented");
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
Result NfcDevice::Format() {
|
||||
Result result = ResultSuccess;
|
||||
|
||||
@ -877,7 +881,9 @@ Result NfcDevice::Format() {
|
||||
result = Mount(NFP::ModelType::Amiibo, NFP::MountTarget::All);
|
||||
}
|
||||
|
||||
if (result.IsError()) {
|
||||
// We are formatting all data. Corruption is not an issue.
|
||||
if (result.IsError() &&
|
||||
(result != ResultCorruptedData && result != ResultCorruptedDataWithBackup)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,6 @@ public:
|
||||
|
||||
Result DeleteRegisterInfo();
|
||||
Result SetRegisterInfoPrivate(const NFP::RegisterInfoPrivate& register_info);
|
||||
Result RestoreAmiibo();
|
||||
Result Format();
|
||||
|
||||
Result OpenApplicationArea(u32 access_id);
|
||||
|
@ -924,12 +924,14 @@ std::string Config::AdjustOutputString(const std::string& string) {
|
||||
|
||||
// Windows requires that two forward slashes are used at the start of a path for unmapped
|
||||
// network drives so we have to watch for that here
|
||||
#ifndef ANDROID
|
||||
if (string.substr(0, 2) == "//") {
|
||||
boost::replace_all(adjusted_string, "//", "/");
|
||||
adjusted_string.insert(0, "/");
|
||||
} else {
|
||||
boost::replace_all(adjusted_string, "//", "/");
|
||||
}
|
||||
#endif
|
||||
|
||||
// Needed for backwards compatibility with QSettings deserialization
|
||||
for (const auto& special_character : special_characters) {
|
||||
|
@ -30,7 +30,6 @@
|
||||
#include <QSize>
|
||||
#include <QStringLiteral>
|
||||
#include <QSurfaceFormat>
|
||||
#include <QTimer>
|
||||
#include <QWindow>
|
||||
#include <QtCore/qobjectdefs.h>
|
||||
|
||||
@ -66,6 +65,8 @@ class QObject;
|
||||
class QPaintEngine;
|
||||
class QSurface;
|
||||
|
||||
constexpr int default_mouse_constrain_timeout = 10;
|
||||
|
||||
EmuThread::EmuThread(Core::System& system) : m_system{system} {}
|
||||
|
||||
EmuThread::~EmuThread() = default;
|
||||
@ -304,6 +305,9 @@ GRenderWindow::GRenderWindow(GMainWindow* parent, EmuThread* emu_thread_,
|
||||
Qt::QueuedConnection);
|
||||
connect(this, &GRenderWindow::ExitSignal, parent, &GMainWindow::OnExit, Qt::QueuedConnection);
|
||||
connect(this, &GRenderWindow::TasPlaybackStateChanged, parent, &GMainWindow::OnTasStateChanged);
|
||||
|
||||
mouse_constrain_timer.setInterval(default_mouse_constrain_timeout);
|
||||
connect(&mouse_constrain_timer, &QTimer::timeout, this, &GRenderWindow::ConstrainMouse);
|
||||
}
|
||||
|
||||
void GRenderWindow::ExecuteProgram(std::size_t program_index) {
|
||||
@ -393,6 +397,22 @@ void GRenderWindow::closeEvent(QCloseEvent* event) {
|
||||
QWidget::closeEvent(event);
|
||||
}
|
||||
|
||||
void GRenderWindow::leaveEvent(QEvent* event) {
|
||||
if (Settings::values.mouse_panning) {
|
||||
const QRect& rect = QWidget::geometry();
|
||||
QPoint position = QCursor::pos();
|
||||
|
||||
qint32 x = qBound(rect.left(), position.x(), rect.right());
|
||||
qint32 y = qBound(rect.top(), position.y(), rect.bottom());
|
||||
// Only start the timer if the mouse has left the window bound.
|
||||
// The leave event is also triggered when the window looses focus.
|
||||
if (x != position.x() || y != position.y()) {
|
||||
mouse_constrain_timer.start();
|
||||
}
|
||||
event->accept();
|
||||
}
|
||||
}
|
||||
|
||||
int GRenderWindow::QtKeyToSwitchKey(Qt::Key qt_key) {
|
||||
static constexpr std::array<std::pair<Qt::Key, Settings::NativeKeyboard::Keys>, 106> key_map = {
|
||||
std::pair<Qt::Key, Settings::NativeKeyboard::Keys>{Qt::Key_A, Settings::NativeKeyboard::A},
|
||||
@ -658,10 +678,19 @@ void GRenderWindow::mouseMoveEvent(QMouseEvent* event) {
|
||||
input_subsystem->GetMouse()->TouchMove(touch_x, touch_y);
|
||||
input_subsystem->GetMouse()->Move(pos.x(), pos.y(), center_x, center_y);
|
||||
|
||||
// Center mouse for mouse panning
|
||||
if (Settings::values.mouse_panning && !Settings::values.mouse_enabled) {
|
||||
QCursor::setPos(mapToGlobal(QPoint{center_x, center_y}));
|
||||
}
|
||||
|
||||
// Constrain mouse for mouse emulation with mouse panning
|
||||
if (Settings::values.mouse_panning && Settings::values.mouse_enabled) {
|
||||
const auto [clamped_mouse_x, clamped_mouse_y] = ClipToTouchScreen(x, y);
|
||||
QCursor::setPos(mapToGlobal(
|
||||
QPoint{static_cast<int>(clamped_mouse_x), static_cast<int>(clamped_mouse_y)}));
|
||||
}
|
||||
|
||||
mouse_constrain_timer.stop();
|
||||
emit MouseActivity();
|
||||
}
|
||||
|
||||
@ -675,6 +704,31 @@ void GRenderWindow::mouseReleaseEvent(QMouseEvent* event) {
|
||||
input_subsystem->GetMouse()->ReleaseButton(button);
|
||||
}
|
||||
|
||||
void GRenderWindow::ConstrainMouse() {
|
||||
if (emu_thread == nullptr || !Settings::values.mouse_panning) {
|
||||
mouse_constrain_timer.stop();
|
||||
return;
|
||||
}
|
||||
if (!this->isActiveWindow()) {
|
||||
mouse_constrain_timer.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Settings::values.mouse_enabled) {
|
||||
const auto pos = mapFromGlobal(QCursor::pos());
|
||||
const int new_pos_x = std::clamp(pos.x(), 0, width());
|
||||
const int new_pos_y = std::clamp(pos.y(), 0, height());
|
||||
|
||||
QCursor::setPos(mapToGlobal(QPoint{new_pos_x, new_pos_y}));
|
||||
return;
|
||||
}
|
||||
|
||||
const int center_x = width() / 2;
|
||||
const int center_y = height() / 2;
|
||||
|
||||
QCursor::setPos(mapToGlobal(QPoint{center_x, center_y}));
|
||||
}
|
||||
|
||||
void GRenderWindow::wheelEvent(QWheelEvent* event) {
|
||||
const int x = event->angleDelta().x();
|
||||
const int y = event->angleDelta().y();
|
||||
|
@ -17,6 +17,7 @@
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
#include <qglobal.h>
|
||||
#include <qnamespace.h>
|
||||
@ -38,7 +39,6 @@ class QMouseEvent;
|
||||
class QObject;
|
||||
class QResizeEvent;
|
||||
class QShowEvent;
|
||||
class QTimer;
|
||||
class QTouchEvent;
|
||||
class QWheelEvent;
|
||||
|
||||
@ -166,6 +166,7 @@ public:
|
||||
std::pair<u32, u32> ScaleTouch(const QPointF& pos) const;
|
||||
|
||||
void closeEvent(QCloseEvent* event) override;
|
||||
void leaveEvent(QEvent* event) override;
|
||||
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
|
||||
@ -229,6 +230,7 @@ private:
|
||||
void TouchBeginEvent(const QTouchEvent* event);
|
||||
void TouchUpdateEvent(const QTouchEvent* event);
|
||||
void TouchEndEvent();
|
||||
void ConstrainMouse();
|
||||
|
||||
void RequestCameraCapture();
|
||||
void OnCameraCapture(int requestId, const QImage& img);
|
||||
@ -268,6 +270,8 @@ private:
|
||||
std::unique_ptr<QTimer> camera_timer;
|
||||
#endif
|
||||
|
||||
QTimer mouse_constrain_timer;
|
||||
|
||||
Core::System& system;
|
||||
|
||||
protected:
|
||||
|
@ -27,6 +27,13 @@ ConfigureCpu::ConfigureCpu(const Core::System& system_,
|
||||
|
||||
connect(accuracy_combobox, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||
&ConfigureCpu::UpdateGroup);
|
||||
|
||||
connect(backend_combobox, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
||||
&ConfigureCpu::UpdateGroup);
|
||||
|
||||
#ifdef HAS_NCE
|
||||
ui->backend_group->setVisible(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
ConfigureCpu::~ConfigureCpu() = default;
|
||||
@ -34,6 +41,7 @@ ConfigureCpu::~ConfigureCpu() = default;
|
||||
void ConfigureCpu::SetConfiguration() {}
|
||||
void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) {
|
||||
auto* accuracy_layout = ui->widget_accuracy->layout();
|
||||
auto* backend_layout = ui->widget_backend->layout();
|
||||
auto* unsafe_layout = ui->unsafe_widget->layout();
|
||||
std::map<u32, QWidget*> unsafe_hold{};
|
||||
|
||||
@ -62,6 +70,9 @@ void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) {
|
||||
// Keep track of cpu_accuracy combobox to display/hide the unsafe settings
|
||||
accuracy_layout->addWidget(widget);
|
||||
accuracy_combobox = widget->combobox;
|
||||
} else if (setting->Id() == Settings::values.cpu_backend.Id()) {
|
||||
backend_layout->addWidget(widget);
|
||||
backend_combobox = widget->combobox;
|
||||
} else {
|
||||
// Presently, all other settings here are unsafe checkboxes
|
||||
unsafe_hold.insert({setting->Id(), widget});
|
||||
@ -73,6 +84,7 @@ void ConfigureCpu::Setup(const ConfigurationShared::Builder& builder) {
|
||||
}
|
||||
|
||||
UpdateGroup(accuracy_combobox->currentIndex());
|
||||
UpdateGroup(backend_combobox->currentIndex());
|
||||
}
|
||||
|
||||
void ConfigureCpu::UpdateGroup(int index) {
|
||||
|
@ -49,4 +49,5 @@ private:
|
||||
std::vector<std::function<void(bool)>> apply_funcs{};
|
||||
|
||||
QComboBox* accuracy_combobox;
|
||||
QComboBox* backend_combobox;
|
||||
};
|
||||
|
@ -59,6 +59,36 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="backend_group">
|
||||
<property name="title">
|
||||
<string>CPU Backend</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_backend" native="true">
|
||||
<layout class="QVBoxLayout" name="verticalLayout1">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="unsafe_group">
|
||||
<property name="title">
|
||||
|
@ -44,6 +44,7 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) {
|
||||
|
||||
// Cpu
|
||||
INSERT(Settings, cpu_accuracy, tr("Accuracy:"), QStringLiteral());
|
||||
INSERT(Settings, cpu_backend, tr("Backend:"), QStringLiteral());
|
||||
|
||||
// Cpu Debug
|
||||
|
||||
@ -240,6 +241,11 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QWidget* parent) {
|
||||
PAIR(CpuAccuracy, Unsafe, tr("Unsafe")),
|
||||
PAIR(CpuAccuracy, Paranoid, tr("Paranoid (disables most optimizations)")),
|
||||
}});
|
||||
translations->insert({Settings::EnumMetadata<Settings::CpuBackend>::Index(),
|
||||
{
|
||||
PAIR(CpuBackend, Dynarmic, tr("Dynarmic")),
|
||||
PAIR(CpuBackend, Nce, tr("NCE")),
|
||||
}});
|
||||
translations->insert({Settings::EnumMetadata<Settings::FullscreenMode>::Index(),
|
||||
{
|
||||
PAIR(FullscreenMode, Borderless, tr("Borderless Windowed")),
|
||||
|
@ -187,7 +187,6 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
|
||||
#endif
|
||||
|
||||
constexpr int default_mouse_hide_timeout = 2500;
|
||||
constexpr int default_mouse_center_timeout = 10;
|
||||
constexpr int default_input_update_timeout = 1;
|
||||
|
||||
constexpr size_t CopyBufferSize = 1_MiB;
|
||||
@ -437,9 +436,6 @@ GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulk
|
||||
connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor);
|
||||
connect(ui->menubar, &QMenuBar::hovered, this, &GMainWindow::ShowMouseCursor);
|
||||
|
||||
mouse_center_timer.setInterval(default_mouse_center_timeout);
|
||||
connect(&mouse_center_timer, &QTimer::timeout, this, &GMainWindow::CenterMouseCursor);
|
||||
|
||||
update_input_timer.setInterval(default_input_update_timeout);
|
||||
connect(&update_input_timer, &QTimer::timeout, this, &GMainWindow::UpdateInputDrivers);
|
||||
update_input_timer.start();
|
||||
@ -1376,14 +1372,6 @@ void GMainWindow::InitializeHotkeys() {
|
||||
}
|
||||
});
|
||||
connect_shortcut(QStringLiteral("Toggle Mouse Panning"), [&] {
|
||||
if (Settings::values.mouse_enabled) {
|
||||
Settings::values.mouse_panning = false;
|
||||
QMessageBox::warning(
|
||||
this, tr("Emulated mouse is enabled"),
|
||||
tr("Real mouse input and mouse panning are incompatible. Please disable the "
|
||||
"emulated mouse in input advanced settings to allow mouse panning."));
|
||||
return;
|
||||
}
|
||||
Settings::values.mouse_panning = !Settings::values.mouse_panning;
|
||||
if (Settings::values.mouse_panning) {
|
||||
render_window->installEventFilter(render_window);
|
||||
@ -4711,26 +4699,10 @@ void GMainWindow::ShowMouseCursor() {
|
||||
}
|
||||
}
|
||||
|
||||
void GMainWindow::CenterMouseCursor() {
|
||||
if (emu_thread == nullptr || !Settings::values.mouse_panning) {
|
||||
mouse_center_timer.stop();
|
||||
return;
|
||||
}
|
||||
if (!this->isActiveWindow()) {
|
||||
mouse_center_timer.stop();
|
||||
return;
|
||||
}
|
||||
const int center_x = render_window->width() / 2;
|
||||
const int center_y = render_window->height() / 2;
|
||||
|
||||
QCursor::setPos(mapToGlobal(QPoint{center_x, center_y}));
|
||||
}
|
||||
|
||||
void GMainWindow::OnMouseActivity() {
|
||||
if (!Settings::values.mouse_panning) {
|
||||
ShowMouseCursor();
|
||||
}
|
||||
mouse_center_timer.stop();
|
||||
}
|
||||
|
||||
void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
|
||||
@ -5031,22 +5003,6 @@ void GMainWindow::dragMoveEvent(QDragMoveEvent* event) {
|
||||
AcceptDropEvent(event);
|
||||
}
|
||||
|
||||
void GMainWindow::leaveEvent(QEvent* event) {
|
||||
if (Settings::values.mouse_panning) {
|
||||
const QRect& rect = geometry();
|
||||
QPoint position = QCursor::pos();
|
||||
|
||||
qint32 x = qBound(rect.left(), position.x(), rect.right());
|
||||
qint32 y = qBound(rect.top(), position.y(), rect.bottom());
|
||||
// Only start the timer if the mouse has left the window bound.
|
||||
// The leave event is also triggered when the window looses focus.
|
||||
if (x != position.x() || y != position.y()) {
|
||||
mouse_center_timer.start();
|
||||
}
|
||||
event->accept();
|
||||
}
|
||||
}
|
||||
|
||||
bool GMainWindow::ConfirmChangeGame() {
|
||||
if (emu_thread == nullptr)
|
||||
return true;
|
||||
|
@ -451,7 +451,6 @@ private:
|
||||
void UpdateInputDrivers();
|
||||
void HideMouseCursor();
|
||||
void ShowMouseCursor();
|
||||
void CenterMouseCursor();
|
||||
void OpenURL(const QUrl& url);
|
||||
void LoadTranslation();
|
||||
void OpenPerGameConfiguration(u64 title_id, const std::string& file_name);
|
||||
@ -535,7 +534,6 @@ private:
|
||||
bool auto_paused = false;
|
||||
bool auto_muted = false;
|
||||
QTimer mouse_hide_timer;
|
||||
QTimer mouse_center_timer;
|
||||
QTimer update_input_timer;
|
||||
|
||||
QString startup_icon_theme;
|
||||
@ -592,5 +590,4 @@ protected:
|
||||
void dropEvent(QDropEvent* event) override;
|
||||
void dragEnterEvent(QDragEnterEvent* event) override;
|
||||
void dragMoveEvent(QDragMoveEvent* event) override;
|
||||
void leaveEvent(QEvent* event) override;
|
||||
};
|
||||
|
Reference in New Issue
Block a user