mirror of
https://github.com/yuzu-emu/yuzu.git
synced 2024-07-04 23:31:19 +01:00
Merge pull request #12204 from t895/config-migration
android: Multi directory UI
This commit is contained in:
commit
aded28f276
32 changed files with 848 additions and 122 deletions
|
@ -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.games_dir_selected,
|
||||
Toast.LENGTH_LONG
|
||||
R.string.folder_already_added,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return
|
||||
}
|
||||
|
||||
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>
|
||||
|
@ -257,6 +263,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 -->
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue