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
|
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.R
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
|
||||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
|
||||||
|
|
||||||
object Settings {
|
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 {
|
enum class Category {
|
||||||
Android,
|
Android,
|
||||||
Audio,
|
Audio,
|
||||||
|
|
|
@ -19,12 +19,13 @@ import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import androidx.navigation.navArgs
|
import androidx.navigation.navArgs
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
|
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.features.settings.utils.SettingsFile
|
||||||
import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment
|
import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment
|
||||||
import org.yuzu.yuzu_emu.model.SettingsViewModel
|
import org.yuzu.yuzu_emu.model.SettingsViewModel
|
||||||
|
@ -53,10 +54,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
settingsViewModel.shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (InsetsHelper.getSystemGestureType(applicationContext) !=
|
if (InsetsHelper.getSystemGestureType(applicationContext) !=
|
||||||
InsetsHelper.GESTURE_NAVIGATION
|
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() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
// TODO: Load custom settings contextually
|
// 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() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
if (isFinishing && settingsViewModel.shouldSave) {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
|
NativeConfig.saveSettings()
|
||||||
Settings.saveSettings()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,9 +145,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSettingsReset() {
|
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
|
// Delete settings file because the user may have changed values that do not exist in the UI
|
||||||
NativeConfig.unloadConfig()
|
NativeConfig.unloadConfig()
|
||||||
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
|
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
|
||||||
|
@ -194,8 +176,4 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
windowInsets
|
windowInsets
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val KEY_SHOULD_SAVE = "should_save"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,7 +105,6 @@ class SettingsAdapter(
|
||||||
fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
|
fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
|
||||||
item.checked = checked
|
item.checked = checked
|
||||||
settingsViewModel.setShouldReloadSettingsList(true)
|
settingsViewModel.setShouldReloadSettingsList(true)
|
||||||
settingsViewModel.shouldSave = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
|
fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
|
||||||
|
@ -161,7 +160,6 @@ class SettingsAdapter(
|
||||||
epochTime += timePicker.hour.toLong() * 60 * 60
|
epochTime += timePicker.hour.toLong() * 60 * 60
|
||||||
epochTime += timePicker.minute.toLong() * 60
|
epochTime += timePicker.minute.toLong() * 60
|
||||||
if (item.value != epochTime) {
|
if (item.value != epochTime) {
|
||||||
settingsViewModel.shouldSave = true
|
|
||||||
notifyItemChanged(position)
|
notifyItemChanged(position)
|
||||||
item.value = epochTime
|
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(
|
add(
|
||||||
HomeSetting(
|
HomeSetting(
|
||||||
R.string.select_games_folder,
|
R.string.manage_game_folders,
|
||||||
R.string.select_games_folder_description,
|
R.string.select_games_folder_description,
|
||||||
R.drawable.ic_add,
|
R.drawable.ic_add,
|
||||||
{
|
{
|
||||||
mainActivity.getGamesDirectory.launch(
|
binding.root.findNavController()
|
||||||
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data
|
.navigate(R.id.action_homeSettingsFragment_to_gameFoldersFragment)
|
||||||
)
|
}
|
||||||
},
|
|
||||||
{ true },
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
homeViewModel.gamesDir
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
add(
|
add(
|
||||||
|
|
|
@ -52,7 +52,6 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
||||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||||
settingsViewModel.clickedItem!!.setting.reset()
|
settingsViewModel.clickedItem!!.setting.reset()
|
||||||
settingsViewModel.setAdapterItemChanged(position)
|
settingsViewModel.setAdapterItemChanged(position)
|
||||||
settingsViewModel.shouldSave = true
|
|
||||||
}
|
}
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.create()
|
.create()
|
||||||
|
@ -137,24 +136,17 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
||||||
is SingleChoiceSetting -> {
|
is SingleChoiceSetting -> {
|
||||||
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
|
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
|
||||||
val value = getValueForSingleChoiceSelection(scSetting, which)
|
val value = getValueForSingleChoiceSelection(scSetting, which)
|
||||||
if (scSetting.selectedValue != value) {
|
|
||||||
settingsViewModel.shouldSave = true
|
|
||||||
}
|
|
||||||
scSetting.selectedValue = value
|
scSetting.selectedValue = value
|
||||||
}
|
}
|
||||||
|
|
||||||
is StringSingleChoiceSetting -> {
|
is StringSingleChoiceSetting -> {
|
||||||
val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
|
val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
|
||||||
val value = scSetting.getValueAt(which)
|
val value = scSetting.getValueAt(which)
|
||||||
if (scSetting.selectedValue != value) settingsViewModel.shouldSave = true
|
|
||||||
scSetting.selectedValue = value
|
scSetting.selectedValue = value
|
||||||
}
|
}
|
||||||
|
|
||||||
is SliderSetting -> {
|
is SliderSetting -> {
|
||||||
val sliderSetting = settingsViewModel.clickedItem as SliderSetting
|
val sliderSetting = settingsViewModel.clickedItem as SliderSetting
|
||||||
if (sliderSetting.selectedValue != settingsViewModel.sliderProgress.value) {
|
|
||||||
settingsViewModel.shouldSave = true
|
|
||||||
}
|
|
||||||
sliderSetting.selectedValue = settingsViewModel.sliderProgress.value
|
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.model.StepState
|
||||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
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
|
import org.yuzu.yuzu_emu.utils.ViewUtils
|
||||||
|
|
||||||
class SetupFragment : Fragment() {
|
class SetupFragment : Fragment() {
|
||||||
|
@ -184,11 +184,7 @@ class SetupFragment : Fragment() {
|
||||||
R.string.add_games_warning_description,
|
R.string.add_games_warning_description,
|
||||||
R.string.add_games_warning_help,
|
R.string.add_games_warning_help,
|
||||||
{
|
{
|
||||||
val preferences =
|
if (NativeConfig.getGameDirs().isNotEmpty()) {
|
||||||
PreferenceManager.getDefaultSharedPreferences(
|
|
||||||
YuzuApplication.appContext
|
|
||||||
)
|
|
||||||
if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) {
|
|
||||||
StepState.COMPLETE
|
StepState.COMPLETE
|
||||||
} else {
|
} else {
|
||||||
StepState.INCOMPLETE
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.decodeFromString
|
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.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.utils.GameHelper
|
import org.yuzu.yuzu_emu.utils.GameHelper
|
||||||
import org.yuzu.yuzu_emu.utils.GameMetadata
|
import org.yuzu.yuzu_emu.utils.GameMetadata
|
||||||
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
class GamesViewModel : ViewModel() {
|
class GamesViewModel : ViewModel() {
|
||||||
val games: StateFlow<List<Game>> get() = _games
|
val games: StateFlow<List<Game>> get() = _games
|
||||||
|
@ -40,6 +42,9 @@ class GamesViewModel : ViewModel() {
|
||||||
val searchFocused: StateFlow<Boolean> get() = _searchFocused
|
val searchFocused: StateFlow<Boolean> get() = _searchFocused
|
||||||
private val _searchFocused = MutableStateFlow(false)
|
private val _searchFocused = MutableStateFlow(false)
|
||||||
|
|
||||||
|
private val _folders = MutableStateFlow(mutableListOf<GameDir>())
|
||||||
|
val folders = _folders.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
||||||
NativeLibrary.reloadKeys()
|
NativeLibrary.reloadKeys()
|
||||||
|
@ -50,6 +55,7 @@ class GamesViewModel : ViewModel() {
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
getGameDirs()
|
||||||
if (storedGames!!.isNotEmpty()) {
|
if (storedGames!!.isNotEmpty()) {
|
||||||
val deserializedGames = mutableSetOf<Game>()
|
val deserializedGames = mutableSetOf<Game>()
|
||||||
storedGames.forEach {
|
storedGames.forEach {
|
||||||
|
@ -104,7 +110,7 @@ class GamesViewModel : ViewModel() {
|
||||||
_searchFocused.value = searchFocused
|
_searchFocused.value = searchFocused
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadGames(directoryChanged: Boolean) {
|
fun reloadGames(directoriesChanged: Boolean) {
|
||||||
if (isReloading.value) {
|
if (isReloading.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -116,10 +122,61 @@ class GamesViewModel : ViewModel() {
|
||||||
setGames(GameHelper.getGames())
|
setGames(GameHelper.getGames())
|
||||||
_isReloading.value = false
|
_isReloading.value = false
|
||||||
|
|
||||||
if (directoryChanged) {
|
if (directoriesChanged) {
|
||||||
setShouldSwapData(true)
|
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
|
package org.yuzu.yuzu_emu.model
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
|
||||||
import org.yuzu.yuzu_emu.utils.GameHelper
|
|
||||||
|
|
||||||
class HomeViewModel : ViewModel() {
|
class HomeViewModel : ViewModel() {
|
||||||
val navigationVisible: StateFlow<Pair<Boolean, Boolean>> get() = _navigationVisible
|
val navigationVisible: StateFlow<Pair<Boolean, Boolean>> get() = _navigationVisible
|
||||||
|
@ -23,14 +17,6 @@ class HomeViewModel : ViewModel() {
|
||||||
val shouldPageForward: StateFlow<Boolean> get() = _shouldPageForward
|
val shouldPageForward: StateFlow<Boolean> get() = _shouldPageForward
|
||||||
private val _shouldPageForward = MutableStateFlow(false)
|
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
|
var navigatedToSetup = false
|
||||||
|
|
||||||
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
|
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
|
||||||
|
@ -50,9 +36,4 @@ class HomeViewModel : ViewModel() {
|
||||||
fun setShouldPageForward(pageForward: Boolean) {
|
fun setShouldPageForward(pageForward: Boolean) {
|
||||||
_shouldPageForward.value = pageForward
|
_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() {
|
class SettingsViewModel : ViewModel() {
|
||||||
var game: Game? = null
|
var game: Game? = null
|
||||||
|
|
||||||
var shouldSave = false
|
|
||||||
|
|
||||||
var clickedItem: SettingsItem? = null
|
var clickedItem: SettingsItem? = null
|
||||||
|
|
||||||
val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate
|
val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate
|
||||||
|
@ -73,6 +71,5 @@ class SettingsViewModel : ViewModel() {
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
game = null
|
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.activities.EmulationActivity
|
||||||
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
|
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
|
||||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
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.IndeterminateProgressDialogFragment
|
||||||
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
|
||||||
import org.yuzu.yuzu_emu.getPublicFilesDir
|
import org.yuzu.yuzu_emu.getPublicFilesDir
|
||||||
|
@ -252,6 +253,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
NativeConfig.saveSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
EmulationActivity.stopForegroundService(this)
|
EmulationActivity.stopForegroundService(this)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
@ -293,20 +301,19 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
)
|
)
|
||||||
|
|
||||||
// When a new directory is picked, we currently will reset the existing games
|
val uriString = result.toString()
|
||||||
// database. This effectively means that only one game directory is supported.
|
val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString }
|
||||||
PreferenceManager.getDefaultSharedPreferences(applicationContext).edit()
|
if (folder != null) {
|
||||||
.putString(GameHelper.KEY_GAME_PATH, result.toString())
|
Toast.makeText(
|
||||||
.apply()
|
applicationContext,
|
||||||
|
R.string.folder_already_added,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Toast.makeText(
|
AddGameFolderDialogFragment.newInstance(uriString)
|
||||||
applicationContext,
|
.show(supportFragmentManager, AddGameFolderDialogFragment.TAG)
|
||||||
R.string.games_dir_selected,
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
|
|
||||||
gamesViewModel.reloadGames(true)
|
|
||||||
homeViewModel.setGamesDir(this, result.path!!)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val getProdKey =
|
val getProdKey =
|
||||||
|
|
|
@ -364,6 +364,27 @@ object FileUtil {
|
||||||
.lowercase()
|
.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)
|
@Throws(IOException::class)
|
||||||
fun getStringFromFile(file: File): String =
|
fun getStringFromFile(file: File): String =
|
||||||
String(file.readBytes(), StandardCharsets.UTF_8)
|
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.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.model.Game
|
import org.yuzu.yuzu_emu.model.Game
|
||||||
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
|
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
|
||||||
|
|
||||||
object GameHelper {
|
object GameHelper {
|
||||||
const val KEY_GAME_PATH = "game_path"
|
private const val KEY_OLD_GAME_PATH = "game_path"
|
||||||
const val KEY_GAMES = "Games"
|
const val KEY_GAMES = "Games"
|
||||||
|
|
||||||
private lateinit var preferences: SharedPreferences
|
private lateinit var preferences: SharedPreferences
|
||||||
|
@ -22,15 +23,43 @@ object GameHelper {
|
||||||
fun getGames(): List<Game> {
|
fun getGames(): List<Game> {
|
||||||
val games = mutableListOf<Game>()
|
val games = mutableListOf<Game>()
|
||||||
val context = YuzuApplication.appContext
|
val context = YuzuApplication.appContext
|
||||||
val gamesDir =
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "")
|
|
||||||
val gamesUri = Uri.parse(gamesDir)
|
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
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.
|
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
||||||
NativeLibrary.reloadKeys()
|
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
|
// Cache list of games found on disk
|
||||||
val serializedGames = mutableSetOf<String>()
|
val serializedGames = mutableSetOf<String>()
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.utils
|
package org.yuzu.yuzu_emu.utils
|
||||||
|
|
||||||
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
|
|
||||||
object NativeConfig {
|
object NativeConfig {
|
||||||
/**
|
/**
|
||||||
* Creates a Config object and opens the emulation config.
|
* Creates a Config object and opens the emulation config.
|
||||||
|
@ -54,4 +56,22 @@ object NativeConfig {
|
||||||
external fun getConfigHeader(category: Int): String
|
external fun getConfigHeader(category: Int): String
|
||||||
|
|
||||||
external fun getPairedSettingKey(key: String): 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() {
|
void AndroidConfig::ReadAndroidValues() {
|
||||||
if (global) {
|
if (global) {
|
||||||
ReadAndroidUIValues();
|
ReadAndroidUIValues();
|
||||||
|
ReadUIValues();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,9 +46,35 @@ void AndroidConfig::ReadAndroidUIValues() {
|
||||||
EndGroup();
|
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() {
|
void AndroidConfig::SaveAndroidValues() {
|
||||||
if (global) {
|
if (global) {
|
||||||
SaveAndroidUIValues();
|
SaveAndroidUIValues();
|
||||||
|
SaveUIValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteToIni();
|
WriteToIni();
|
||||||
|
@ -61,6 +88,29 @@ void AndroidConfig::SaveAndroidUIValues() {
|
||||||
EndGroup();
|
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) {
|
std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) {
|
||||||
auto& map = Settings::values.linkage.by_category;
|
auto& map = Settings::values.linkage.by_category;
|
||||||
if (map.contains(category)) {
|
if (map.contains(category)) {
|
||||||
|
|
|
@ -19,9 +19,9 @@ protected:
|
||||||
void ReadAndroidUIValues();
|
void ReadAndroidUIValues();
|
||||||
void ReadHidbusValues() override {}
|
void ReadHidbusValues() override {}
|
||||||
void ReadDebugControlValues() override {}
|
void ReadDebugControlValues() override {}
|
||||||
void ReadPathValues() override {}
|
void ReadPathValues() override;
|
||||||
void ReadShortcutValues() override {}
|
void ReadShortcutValues() override {}
|
||||||
void ReadUIValues() override {}
|
void ReadUIValues() override;
|
||||||
void ReadUIGamelistValues() override {}
|
void ReadUIGamelistValues() override {}
|
||||||
void ReadUILayoutValues() override {}
|
void ReadUILayoutValues() override {}
|
||||||
void ReadMultiplayerValues() override {}
|
void ReadMultiplayerValues() override {}
|
||||||
|
@ -30,9 +30,9 @@ protected:
|
||||||
void SaveAndroidUIValues();
|
void SaveAndroidUIValues();
|
||||||
void SaveHidbusValues() override {}
|
void SaveHidbusValues() override {}
|
||||||
void SaveDebugControlValues() override {}
|
void SaveDebugControlValues() override {}
|
||||||
void SavePathValues() override {}
|
void SavePathValues() override;
|
||||||
void SaveShortcutValues() override {}
|
void SaveShortcutValues() override {}
|
||||||
void SaveUIValues() override {}
|
void SaveUIValues() override;
|
||||||
void SaveUIGamelistValues() override {}
|
void SaveUIGamelistValues() override {}
|
||||||
void SaveUILayoutValues() override {}
|
void SaveUILayoutValues() override {}
|
||||||
void SaveMultiplayerValues() override {}
|
void SaveMultiplayerValues() override {}
|
||||||
|
|
|
@ -9,9 +9,17 @@
|
||||||
|
|
||||||
namespace AndroidSettings {
|
namespace AndroidSettings {
|
||||||
|
|
||||||
|
struct GameDir {
|
||||||
|
std::string path;
|
||||||
|
bool deep_scan = false;
|
||||||
|
};
|
||||||
|
|
||||||
struct Values {
|
struct Values {
|
||||||
Settings::Linkage linkage;
|
Settings::Linkage linkage;
|
||||||
|
|
||||||
|
// Path settings
|
||||||
|
std::vector<GameDir> game_dirs;
|
||||||
|
|
||||||
// Android
|
// Android
|
||||||
Settings::Setting<bool> picture_in_picture{linkage, false, "picture_in_picture",
|
Settings::Setting<bool> picture_in_picture{linkage, false, "picture_in_picture",
|
||||||
Settings::Category::Android};
|
Settings::Category::Android};
|
||||||
|
|
|
@ -13,6 +13,8 @@ static JavaVM* s_java_vm;
|
||||||
static jclass s_native_library_class;
|
static jclass s_native_library_class;
|
||||||
static jclass s_disk_cache_progress_class;
|
static jclass s_disk_cache_progress_class;
|
||||||
static jclass s_load_callback_stage_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_exit_emulation_activity;
|
||||||
static jmethodID s_disk_cache_load_progress;
|
static jmethodID s_disk_cache_load_progress;
|
||||||
static jmethodID s_on_emulation_started;
|
static jmethodID s_on_emulation_started;
|
||||||
|
@ -53,6 +55,14 @@ jclass GetDiskCacheLoadCallbackStageClass() {
|
||||||
return s_load_callback_stage_class;
|
return s_load_callback_stage_class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jclass GetGameDirClass() {
|
||||||
|
return s_game_dir_class;
|
||||||
|
}
|
||||||
|
|
||||||
|
jmethodID GetGameDirConstructor() {
|
||||||
|
return s_game_dir_constructor;
|
||||||
|
}
|
||||||
|
|
||||||
jmethodID GetExitEmulationActivity() {
|
jmethodID GetExitEmulationActivity() {
|
||||||
return s_exit_emulation_activity;
|
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(
|
s_load_callback_stage_class = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass(
|
||||||
"org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage")));
|
"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
|
// Initialize methods
|
||||||
s_exit_emulation_activity =
|
s_exit_emulation_activity =
|
||||||
env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
|
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_native_library_class);
|
||||||
env->DeleteGlobalRef(s_disk_cache_progress_class);
|
env->DeleteGlobalRef(s_disk_cache_progress_class);
|
||||||
env->DeleteGlobalRef(s_load_callback_stage_class);
|
env->DeleteGlobalRef(s_load_callback_stage_class);
|
||||||
|
env->DeleteGlobalRef(s_game_dir_class);
|
||||||
|
|
||||||
// UnInitialize applets
|
// UnInitialize applets
|
||||||
SoftwareKeyboard::CleanupJNI(env);
|
SoftwareKeyboard::CleanupJNI(env);
|
||||||
|
|
|
@ -13,6 +13,8 @@ JNIEnv* GetEnvForThread();
|
||||||
jclass GetNativeLibraryClass();
|
jclass GetNativeLibraryClass();
|
||||||
jclass GetDiskCacheProgressClass();
|
jclass GetDiskCacheProgressClass();
|
||||||
jclass GetDiskCacheLoadCallbackStageClass();
|
jclass GetDiskCacheLoadCallbackStageClass();
|
||||||
|
jclass GetGameDirClass();
|
||||||
|
jmethodID GetGameDirConstructor();
|
||||||
jmethodID GetExitEmulationActivity();
|
jmethodID GetExitEmulationActivity();
|
||||||
jmethodID GetDiskCacheLoadProgress();
|
jmethodID GetDiskCacheLoadProgress();
|
||||||
jmethodID GetOnEmulationStarted();
|
jmethodID GetOnEmulationStarted();
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
#include "frontend_common/config.h"
|
#include "frontend_common/config.h"
|
||||||
#include "jni/android_common/android_common.h"
|
#include "jni/android_common/android_common.h"
|
||||||
|
#include "jni/id_cache.h"
|
||||||
|
|
||||||
std::unique_ptr<AndroidConfig> config;
|
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());
|
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"
|
} // 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
|
<action
|
||||||
android:id="@+id/action_homeSettingsFragment_to_appletLauncherFragment"
|
android:id="@+id/action_homeSettingsFragment_to_appletLauncherFragment"
|
||||||
app:destination="@id/appletLauncherFragment" />
|
app:destination="@id/appletLauncherFragment" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_homeSettingsFragment_to_gameFoldersFragment"
|
||||||
|
app:destination="@id/gameFoldersFragment" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
|
@ -117,5 +120,9 @@
|
||||||
android:id="@+id/cabinetLauncherDialogFragment"
|
android:id="@+id/cabinetLauncherDialogFragment"
|
||||||
android:name="org.yuzu.yuzu_emu.fragments.CabinetLauncherDialogFragment"
|
android:name="org.yuzu.yuzu_emu.fragments.CabinetLauncherDialogFragment"
|
||||||
android:label="CabinetLauncherDialogFragment" />
|
android:label="CabinetLauncherDialogFragment" />
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/gameFoldersFragment"
|
||||||
|
android:name="org.yuzu.yuzu_emu.fragments.GameFoldersFragment"
|
||||||
|
android:label="GameFoldersFragment" />
|
||||||
|
|
||||||
</navigation>
|
</navigation>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<dimen name="menu_width">256dp</dimen>
|
<dimen name="menu_width">256dp</dimen>
|
||||||
<dimen name="card_width">165dp</dimen>
|
<dimen name="card_width">165dp</dimen>
|
||||||
<dimen name="icon_inset">24dp</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="spacing_fab">24dp</dimen>
|
||||||
|
|
||||||
<dimen name="dialog_margin">20dp</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="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="search_and_filter_games">Search and filter games</string>
|
||||||
<string name="select_games_folder">Select games folder</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="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">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>
|
<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="manage_yuzu_data_description">Import/export firmware, keys, user data, and more!</string>
|
||||||
<string name="share_save_file">Share save file</string>
|
<string name="share_save_file">Share save file</string>
|
||||||
<string name="export_save_failed">Failed to export save</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 -->
|
<!-- Applet launcher strings -->
|
||||||
<string name="applets">Applet launcher</string>
|
<string name="applets">Applet launcher</string>
|
||||||
|
@ -257,6 +263,7 @@
|
||||||
<string name="cancelling">Cancelling</string>
|
<string name="cancelling">Cancelling</string>
|
||||||
<string name="install">Install</string>
|
<string name="install">Install</string>
|
||||||
<string name="delete">Delete</string>
|
<string name="delete">Delete</string>
|
||||||
|
<string name="edit">Edit</string>
|
||||||
<string name="export_success">Exported successfully</string>
|
<string name="export_success">Exported successfully</string>
|
||||||
|
|
||||||
<!-- GPU driver installation -->
|
<!-- 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
|
// 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
|
// network drives so we have to watch for that here
|
||||||
|
#ifndef ANDROID
|
||||||
if (string.substr(0, 2) == "//") {
|
if (string.substr(0, 2) == "//") {
|
||||||
boost::replace_all(adjusted_string, "//", "/");
|
boost::replace_all(adjusted_string, "//", "/");
|
||||||
adjusted_string.insert(0, "/");
|
adjusted_string.insert(0, "/");
|
||||||
} else {
|
} else {
|
||||||
boost::replace_all(adjusted_string, "//", "/");
|
boost::replace_all(adjusted_string, "//", "/");
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Needed for backwards compatibility with QSettings deserialization
|
// Needed for backwards compatibility with QSettings deserialization
|
||||||
for (const auto& special_character : special_characters) {
|
for (const auto& special_character : special_characters) {
|
||||||
|
|
Loading…
Reference in a new issue