diff options
Diffstat (limited to '')
27 files changed, 837 insertions, 59 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt new file mode 100644 index 000000000..ab657a7b9 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt | |||
| @@ -0,0 +1,76 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.adapters | ||
| 5 | |||
| 6 | import android.net.Uri | ||
| 7 | import android.text.TextUtils | ||
| 8 | import android.view.LayoutInflater | ||
| 9 | import android.view.ViewGroup | ||
| 10 | import androidx.fragment.app.FragmentActivity | ||
| 11 | import androidx.recyclerview.widget.AsyncDifferConfig | ||
| 12 | import androidx.recyclerview.widget.DiffUtil | ||
| 13 | import androidx.recyclerview.widget.ListAdapter | ||
| 14 | import androidx.recyclerview.widget.RecyclerView | ||
| 15 | import org.yuzu.yuzu_emu.databinding.CardFolderBinding | ||
| 16 | import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment | ||
| 17 | import org.yuzu.yuzu_emu.model.GameDir | ||
| 18 | import org.yuzu.yuzu_emu.model.GamesViewModel | ||
| 19 | |||
| 20 | class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) : | ||
| 21 | ListAdapter<GameDir, FolderAdapter.FolderViewHolder>( | ||
| 22 | AsyncDifferConfig.Builder(DiffCallback()).build() | ||
| 23 | ) { | ||
| 24 | override fun onCreateViewHolder( | ||
| 25 | parent: ViewGroup, | ||
| 26 | viewType: Int | ||
| 27 | ): FolderAdapter.FolderViewHolder { | ||
| 28 | CardFolderBinding.inflate(LayoutInflater.from(parent.context), parent, false) | ||
| 29 | .also { return FolderViewHolder(it) } | ||
| 30 | } | ||
| 31 | |||
| 32 | override fun onBindViewHolder(holder: FolderAdapter.FolderViewHolder, position: Int) = | ||
| 33 | holder.bind(currentList[position]) | ||
| 34 | |||
| 35 | inner class FolderViewHolder(val binding: CardFolderBinding) : | ||
| 36 | RecyclerView.ViewHolder(binding.root) { | ||
| 37 | private lateinit var gameDir: GameDir | ||
| 38 | |||
| 39 | fun bind(gameDir: GameDir) { | ||
| 40 | this.gameDir = gameDir | ||
| 41 | |||
| 42 | binding.apply { | ||
| 43 | path.text = Uri.parse(gameDir.uriString).path | ||
| 44 | path.postDelayed( | ||
| 45 | { | ||
| 46 | path.isSelected = true | ||
| 47 | path.ellipsize = TextUtils.TruncateAt.MARQUEE | ||
| 48 | }, | ||
| 49 | 3000 | ||
| 50 | ) | ||
| 51 | |||
| 52 | buttonEdit.setOnClickListener { | ||
| 53 | GameFolderPropertiesDialogFragment.newInstance(this@FolderViewHolder.gameDir) | ||
| 54 | .show( | ||
| 55 | activity.supportFragmentManager, | ||
| 56 | GameFolderPropertiesDialogFragment.TAG | ||
| 57 | ) | ||
| 58 | } | ||
| 59 | |||
| 60 | buttonDelete.setOnClickListener { | ||
| 61 | gamesViewModel.removeFolder(this@FolderViewHolder.gameDir) | ||
| 62 | } | ||
| 63 | } | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | private class DiffCallback : DiffUtil.ItemCallback<GameDir>() { | ||
| 68 | override fun areItemsTheSame(oldItem: GameDir, newItem: GameDir): Boolean { | ||
| 69 | return oldItem == newItem | ||
| 70 | } | ||
| 71 | |||
| 72 | override fun areContentsTheSame(oldItem: GameDir, newItem: GameDir): Boolean { | ||
| 73 | return oldItem == newItem | ||
| 74 | } | ||
| 75 | } | ||
| 76 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt new file mode 100644 index 000000000..dec2b7cf1 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt | |||
| @@ -0,0 +1,53 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.fragments | ||
| 5 | |||
| 6 | import android.app.Dialog | ||
| 7 | import android.content.DialogInterface | ||
| 8 | import android.net.Uri | ||
| 9 | import android.os.Bundle | ||
| 10 | import androidx.fragment.app.DialogFragment | ||
| 11 | import androidx.fragment.app.activityViewModels | ||
| 12 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||
| 13 | import org.yuzu.yuzu_emu.R | ||
| 14 | import org.yuzu.yuzu_emu.databinding.DialogAddFolderBinding | ||
| 15 | import org.yuzu.yuzu_emu.model.GameDir | ||
| 16 | import org.yuzu.yuzu_emu.model.GamesViewModel | ||
| 17 | |||
| 18 | class AddGameFolderDialogFragment : DialogFragment() { | ||
| 19 | private val gamesViewModel: GamesViewModel by activityViewModels() | ||
| 20 | |||
| 21 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | ||
| 22 | val binding = DialogAddFolderBinding.inflate(layoutInflater) | ||
| 23 | val folderUriString = requireArguments().getString(FOLDER_URI_STRING) | ||
| 24 | if (folderUriString == null) { | ||
| 25 | dismiss() | ||
| 26 | } | ||
| 27 | binding.path.text = Uri.parse(folderUriString).path | ||
| 28 | |||
| 29 | return MaterialAlertDialogBuilder(requireContext()) | ||
| 30 | .setTitle(R.string.add_game_folder) | ||
| 31 | .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> | ||
| 32 | val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked) | ||
| 33 | gamesViewModel.addFolder(newGameDir) | ||
| 34 | } | ||
| 35 | .setNegativeButton(android.R.string.cancel, null) | ||
| 36 | .setView(binding.root) | ||
| 37 | .show() | ||
| 38 | } | ||
| 39 | |||
| 40 | companion object { | ||
| 41 | const val TAG = "AddGameFolderDialogFragment" | ||
| 42 | |||
| 43 | private const val FOLDER_URI_STRING = "FolderUriString" | ||
| 44 | |||
| 45 | fun newInstance(folderUriString: String): AddGameFolderDialogFragment { | ||
| 46 | val args = Bundle() | ||
| 47 | args.putString(FOLDER_URI_STRING, folderUriString) | ||
| 48 | val fragment = AddGameFolderDialogFragment() | ||
| 49 | fragment.arguments = args | ||
| 50 | return fragment | ||
| 51 | } | ||
| 52 | } | ||
| 53 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt new file mode 100644 index 000000000..b6c2e4635 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt | |||
| @@ -0,0 +1,72 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.fragments | ||
| 5 | |||
| 6 | import android.app.Dialog | ||
| 7 | import android.content.DialogInterface | ||
| 8 | import android.os.Bundle | ||
| 9 | import androidx.fragment.app.DialogFragment | ||
| 10 | import androidx.fragment.app.activityViewModels | ||
| 11 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||
| 12 | import org.yuzu.yuzu_emu.R | ||
| 13 | import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding | ||
| 14 | import org.yuzu.yuzu_emu.model.GameDir | ||
| 15 | import org.yuzu.yuzu_emu.model.GamesViewModel | ||
| 16 | import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable | ||
| 17 | |||
| 18 | class GameFolderPropertiesDialogFragment : DialogFragment() { | ||
| 19 | private val gamesViewModel: GamesViewModel by activityViewModels() | ||
| 20 | |||
| 21 | private var deepScan = false | ||
| 22 | |||
| 23 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | ||
| 24 | val binding = DialogFolderPropertiesBinding.inflate(layoutInflater) | ||
| 25 | val gameDir = requireArguments().parcelable<GameDir>(GAME_DIR)!! | ||
| 26 | |||
| 27 | // Restore checkbox state | ||
| 28 | binding.deepScanSwitch.isChecked = | ||
| 29 | savedInstanceState?.getBoolean(DEEP_SCAN) ?: gameDir.deepScan | ||
| 30 | |||
| 31 | // Ensure that we can get the checkbox state even if the view is destroyed | ||
| 32 | deepScan = binding.deepScanSwitch.isChecked | ||
| 33 | binding.deepScanSwitch.setOnClickListener { | ||
| 34 | deepScan = binding.deepScanSwitch.isChecked | ||
| 35 | } | ||
| 36 | |||
| 37 | return MaterialAlertDialogBuilder(requireContext()) | ||
| 38 | .setView(binding.root) | ||
| 39 | .setTitle(R.string.game_folder_properties) | ||
| 40 | .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> | ||
| 41 | val folderIndex = gamesViewModel.folders.value.indexOf(gameDir) | ||
| 42 | if (folderIndex != -1) { | ||
| 43 | gamesViewModel.folders.value[folderIndex].deepScan = | ||
| 44 | binding.deepScanSwitch.isChecked | ||
| 45 | gamesViewModel.updateGameDirs() | ||
| 46 | } | ||
| 47 | } | ||
| 48 | .setNegativeButton(android.R.string.cancel, null) | ||
| 49 | .show() | ||
| 50 | } | ||
| 51 | |||
| 52 | override fun onSaveInstanceState(outState: Bundle) { | ||
| 53 | super.onSaveInstanceState(outState) | ||
| 54 | outState.putBoolean(DEEP_SCAN, deepScan) | ||
| 55 | } | ||
| 56 | |||
| 57 | companion object { | ||
| 58 | const val TAG = "GameFolderPropertiesDialogFragment" | ||
| 59 | |||
| 60 | private const val GAME_DIR = "GameDir" | ||
| 61 | |||
| 62 | private const val DEEP_SCAN = "DeepScan" | ||
| 63 | |||
| 64 | fun newInstance(gameDir: GameDir): GameFolderPropertiesDialogFragment { | ||
| 65 | val args = Bundle() | ||
| 66 | args.putParcelable(GAME_DIR, gameDir) | ||
| 67 | val fragment = GameFolderPropertiesDialogFragment() | ||
| 68 | fragment.arguments = args | ||
| 69 | return fragment | ||
| 70 | } | ||
| 71 | } | ||
| 72 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt new file mode 100644 index 000000000..341a37fdb --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt | |||
| @@ -0,0 +1,128 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.fragments | ||
| 5 | |||
| 6 | import android.content.Intent | ||
| 7 | import android.os.Bundle | ||
| 8 | import android.view.LayoutInflater | ||
| 9 | import android.view.View | ||
| 10 | import android.view.ViewGroup | ||
| 11 | import androidx.core.view.ViewCompat | ||
| 12 | import androidx.core.view.WindowInsetsCompat | ||
| 13 | import androidx.core.view.updatePadding | ||
| 14 | import androidx.fragment.app.Fragment | ||
| 15 | import androidx.fragment.app.activityViewModels | ||
| 16 | import androidx.lifecycle.Lifecycle | ||
| 17 | import androidx.lifecycle.lifecycleScope | ||
| 18 | import androidx.lifecycle.repeatOnLifecycle | ||
| 19 | import androidx.navigation.findNavController | ||
| 20 | import androidx.recyclerview.widget.GridLayoutManager | ||
| 21 | import com.google.android.material.transition.MaterialSharedAxis | ||
| 22 | import kotlinx.coroutines.launch | ||
| 23 | import org.yuzu.yuzu_emu.R | ||
| 24 | import org.yuzu.yuzu_emu.adapters.FolderAdapter | ||
| 25 | import org.yuzu.yuzu_emu.databinding.FragmentFoldersBinding | ||
| 26 | import org.yuzu.yuzu_emu.model.GamesViewModel | ||
| 27 | import org.yuzu.yuzu_emu.model.HomeViewModel | ||
| 28 | import org.yuzu.yuzu_emu.ui.main.MainActivity | ||
| 29 | |||
| 30 | class GameFoldersFragment : Fragment() { | ||
| 31 | private var _binding: FragmentFoldersBinding? = null | ||
| 32 | private val binding get() = _binding!! | ||
| 33 | |||
| 34 | private val homeViewModel: HomeViewModel by activityViewModels() | ||
| 35 | private val gamesViewModel: GamesViewModel by activityViewModels() | ||
| 36 | |||
| 37 | override fun onCreate(savedInstanceState: Bundle?) { | ||
| 38 | super.onCreate(savedInstanceState) | ||
| 39 | enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) | ||
| 40 | returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) | ||
| 41 | reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) | ||
| 42 | |||
| 43 | gamesViewModel.onOpenGameFoldersFragment() | ||
| 44 | } | ||
| 45 | |||
| 46 | override fun onCreateView( | ||
| 47 | inflater: LayoutInflater, | ||
| 48 | container: ViewGroup?, | ||
| 49 | savedInstanceState: Bundle? | ||
| 50 | ): View { | ||
| 51 | _binding = FragmentFoldersBinding.inflate(inflater) | ||
| 52 | return binding.root | ||
| 53 | } | ||
| 54 | |||
| 55 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| 56 | super.onViewCreated(view, savedInstanceState) | ||
| 57 | homeViewModel.setNavigationVisibility(visible = false, animated = true) | ||
| 58 | homeViewModel.setStatusBarShadeVisibility(visible = false) | ||
| 59 | |||
| 60 | binding.toolbarFolders.setNavigationOnClickListener { | ||
| 61 | binding.root.findNavController().popBackStack() | ||
| 62 | } | ||
| 63 | |||
| 64 | binding.listFolders.apply { | ||
| 65 | layoutManager = GridLayoutManager( | ||
| 66 | requireContext(), | ||
| 67 | resources.getInteger(R.integer.grid_columns) | ||
| 68 | ) | ||
| 69 | adapter = FolderAdapter(requireActivity(), gamesViewModel) | ||
| 70 | } | ||
| 71 | |||
| 72 | viewLifecycleOwner.lifecycleScope.launch { | ||
| 73 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 74 | gamesViewModel.folders.collect { | ||
| 75 | (binding.listFolders.adapter as FolderAdapter).submitList(it) | ||
| 76 | } | ||
| 77 | } | ||
| 78 | } | ||
| 79 | |||
| 80 | val mainActivity = requireActivity() as MainActivity | ||
| 81 | binding.buttonAdd.setOnClickListener { | ||
| 82 | mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) | ||
| 83 | } | ||
| 84 | |||
| 85 | setInsets() | ||
| 86 | } | ||
| 87 | |||
| 88 | override fun onStop() { | ||
| 89 | super.onStop() | ||
| 90 | gamesViewModel.onCloseGameFoldersFragment() | ||
| 91 | } | ||
| 92 | |||
| 93 | private fun setInsets() = | ||
| 94 | ViewCompat.setOnApplyWindowInsetsListener( | ||
| 95 | binding.root | ||
| 96 | ) { _: View, windowInsets: WindowInsetsCompat -> | ||
| 97 | val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) | ||
| 98 | val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) | ||
| 99 | |||
| 100 | val leftInsets = barInsets.left + cutoutInsets.left | ||
| 101 | val rightInsets = barInsets.right + cutoutInsets.right | ||
| 102 | |||
| 103 | val mlpToolbar = binding.toolbarFolders.layoutParams as ViewGroup.MarginLayoutParams | ||
| 104 | mlpToolbar.leftMargin = leftInsets | ||
| 105 | mlpToolbar.rightMargin = rightInsets | ||
| 106 | binding.toolbarFolders.layoutParams = mlpToolbar | ||
| 107 | |||
| 108 | val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab) | ||
| 109 | val mlpFab = | ||
| 110 | binding.buttonAdd.layoutParams as ViewGroup.MarginLayoutParams | ||
| 111 | mlpFab.leftMargin = leftInsets + fabSpacing | ||
| 112 | mlpFab.rightMargin = rightInsets + fabSpacing | ||
| 113 | mlpFab.bottomMargin = barInsets.bottom + fabSpacing | ||
| 114 | binding.buttonAdd.layoutParams = mlpFab | ||
| 115 | |||
| 116 | val mlpListFolders = binding.listFolders.layoutParams as ViewGroup.MarginLayoutParams | ||
| 117 | mlpListFolders.leftMargin = leftInsets | ||
| 118 | mlpListFolders.rightMargin = rightInsets | ||
| 119 | binding.listFolders.layoutParams = mlpListFolders | ||
| 120 | |||
| 121 | binding.listFolders.updatePadding( | ||
| 122 | bottom = barInsets.bottom + | ||
| 123 | resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab) | ||
| 124 | ) | ||
| 125 | |||
| 126 | windowInsets | ||
| 127 | } | ||
| 128 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index 4720daec4..3addc2e63 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt | |||
| @@ -127,18 +127,13 @@ class HomeSettingsFragment : Fragment() { | |||
| 127 | ) | 127 | ) |
| 128 | add( | 128 | add( |
| 129 | HomeSetting( | 129 | HomeSetting( |
| 130 | R.string.select_games_folder, | 130 | R.string.manage_game_folders, |
| 131 | R.string.select_games_folder_description, | 131 | R.string.select_games_folder_description, |
| 132 | R.drawable.ic_add, | 132 | R.drawable.ic_add, |
| 133 | { | 133 | { |
| 134 | mainActivity.getGamesDirectory.launch( | 134 | binding.root.findNavController() |
| 135 | Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data | 135 | .navigate(R.id.action_homeSettingsFragment_to_gameFoldersFragment) |
| 136 | ) | 136 | } |
| 137 | }, | ||
| 138 | { true }, | ||
| 139 | 0, | ||
| 140 | 0, | ||
| 141 | homeViewModel.gamesDir | ||
| 142 | ) | 137 | ) |
| 143 | ) | 138 | ) |
| 144 | add( | 139 | add( |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt index c66bb635a..c4277735d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt | |||
| @@ -42,7 +42,7 @@ import org.yuzu.yuzu_emu.model.SetupPage | |||
| 42 | import org.yuzu.yuzu_emu.model.StepState | 42 | import org.yuzu.yuzu_emu.model.StepState |
| 43 | import org.yuzu.yuzu_emu.ui.main.MainActivity | 43 | import org.yuzu.yuzu_emu.ui.main.MainActivity |
| 44 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization | 44 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization |
| 45 | import org.yuzu.yuzu_emu.utils.GameHelper | 45 | import org.yuzu.yuzu_emu.utils.NativeConfig |
| 46 | import org.yuzu.yuzu_emu.utils.ViewUtils | 46 | import org.yuzu.yuzu_emu.utils.ViewUtils |
| 47 | 47 | ||
| 48 | class SetupFragment : Fragment() { | 48 | class SetupFragment : Fragment() { |
| @@ -184,11 +184,7 @@ class SetupFragment : Fragment() { | |||
| 184 | R.string.add_games_warning_description, | 184 | R.string.add_games_warning_description, |
| 185 | R.string.add_games_warning_help, | 185 | R.string.add_games_warning_help, |
| 186 | { | 186 | { |
| 187 | val preferences = | 187 | if (NativeConfig.getGameDirs().isNotEmpty()) { |
| 188 | PreferenceManager.getDefaultSharedPreferences( | ||
| 189 | YuzuApplication.appContext | ||
| 190 | ) | ||
| 191 | if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) { | ||
| 192 | StepState.COMPLETE | 188 | StepState.COMPLETE |
| 193 | } else { | 189 | } else { |
| 194 | StepState.INCOMPLETE | 190 | StepState.INCOMPLETE |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt new file mode 100644 index 000000000..274bc1c7b --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.model | ||
| 5 | |||
| 6 | import android.os.Parcelable | ||
| 7 | import kotlinx.parcelize.Parcelize | ||
| 8 | |||
| 9 | @Parcelize | ||
| 10 | data class GameDir( | ||
| 11 | val uriString: String, | ||
| 12 | var deepScan: Boolean | ||
| 13 | ) : Parcelable | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt index 8512ed17c..752d98c10 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt | |||
| @@ -12,6 +12,7 @@ import java.util.Locale | |||
| 12 | import kotlinx.coroutines.Dispatchers | 12 | import kotlinx.coroutines.Dispatchers |
| 13 | import kotlinx.coroutines.flow.MutableStateFlow | 13 | import kotlinx.coroutines.flow.MutableStateFlow |
| 14 | import kotlinx.coroutines.flow.StateFlow | 14 | import kotlinx.coroutines.flow.StateFlow |
| 15 | import kotlinx.coroutines.flow.asStateFlow | ||
| 15 | import kotlinx.coroutines.launch | 16 | import kotlinx.coroutines.launch |
| 16 | import kotlinx.coroutines.withContext | 17 | import kotlinx.coroutines.withContext |
| 17 | import kotlinx.serialization.decodeFromString | 18 | import kotlinx.serialization.decodeFromString |
| @@ -20,6 +21,7 @@ import org.yuzu.yuzu_emu.NativeLibrary | |||
| 20 | import org.yuzu.yuzu_emu.YuzuApplication | 21 | import org.yuzu.yuzu_emu.YuzuApplication |
| 21 | import org.yuzu.yuzu_emu.utils.GameHelper | 22 | import org.yuzu.yuzu_emu.utils.GameHelper |
| 22 | import org.yuzu.yuzu_emu.utils.GameMetadata | 23 | import org.yuzu.yuzu_emu.utils.GameMetadata |
| 24 | import org.yuzu.yuzu_emu.utils.NativeConfig | ||
| 23 | 25 | ||
| 24 | class GamesViewModel : ViewModel() { | 26 | class GamesViewModel : ViewModel() { |
| 25 | val games: StateFlow<List<Game>> get() = _games | 27 | val games: StateFlow<List<Game>> get() = _games |
| @@ -40,6 +42,9 @@ class GamesViewModel : ViewModel() { | |||
| 40 | val searchFocused: StateFlow<Boolean> get() = _searchFocused | 42 | val searchFocused: StateFlow<Boolean> get() = _searchFocused |
| 41 | private val _searchFocused = MutableStateFlow(false) | 43 | private val _searchFocused = MutableStateFlow(false) |
| 42 | 44 | ||
| 45 | private val _folders = MutableStateFlow(mutableListOf<GameDir>()) | ||
| 46 | val folders = _folders.asStateFlow() | ||
| 47 | |||
| 43 | init { | 48 | init { |
| 44 | // Ensure keys are loaded so that ROM metadata can be decrypted. | 49 | // Ensure keys are loaded so that ROM metadata can be decrypted. |
| 45 | NativeLibrary.reloadKeys() | 50 | NativeLibrary.reloadKeys() |
| @@ -50,6 +55,7 @@ class GamesViewModel : ViewModel() { | |||
| 50 | 55 | ||
| 51 | viewModelScope.launch { | 56 | viewModelScope.launch { |
| 52 | withContext(Dispatchers.IO) { | 57 | withContext(Dispatchers.IO) { |
| 58 | getGameDirs() | ||
| 53 | if (storedGames!!.isNotEmpty()) { | 59 | if (storedGames!!.isNotEmpty()) { |
| 54 | val deserializedGames = mutableSetOf<Game>() | 60 | val deserializedGames = mutableSetOf<Game>() |
| 55 | storedGames.forEach { | 61 | storedGames.forEach { |
| @@ -104,7 +110,7 @@ class GamesViewModel : ViewModel() { | |||
| 104 | _searchFocused.value = searchFocused | 110 | _searchFocused.value = searchFocused |
| 105 | } | 111 | } |
| 106 | 112 | ||
| 107 | fun reloadGames(directoryChanged: Boolean) { | 113 | fun reloadGames(directoriesChanged: Boolean) { |
| 108 | if (isReloading.value) { | 114 | if (isReloading.value) { |
| 109 | return | 115 | return |
| 110 | } | 116 | } |
| @@ -116,10 +122,61 @@ class GamesViewModel : ViewModel() { | |||
| 116 | setGames(GameHelper.getGames()) | 122 | setGames(GameHelper.getGames()) |
| 117 | _isReloading.value = false | 123 | _isReloading.value = false |
| 118 | 124 | ||
| 119 | if (directoryChanged) { | 125 | if (directoriesChanged) { |
| 120 | setShouldSwapData(true) | 126 | setShouldSwapData(true) |
| 121 | } | 127 | } |
| 122 | } | 128 | } |
| 123 | } | 129 | } |
| 124 | } | 130 | } |
| 131 | |||
| 132 | fun addFolder(gameDir: GameDir) = | ||
| 133 | viewModelScope.launch { | ||
| 134 | withContext(Dispatchers.IO) { | ||
| 135 | NativeConfig.addGameDir(gameDir) | ||
| 136 | getGameDirs() | ||
| 137 | } | ||
| 138 | } | ||
| 139 | |||
| 140 | fun removeFolder(gameDir: GameDir) = | ||
| 141 | viewModelScope.launch { | ||
| 142 | withContext(Dispatchers.IO) { | ||
| 143 | val gameDirs = _folders.value.toMutableList() | ||
| 144 | val removedDirIndex = gameDirs.indexOf(gameDir) | ||
| 145 | if (removedDirIndex != -1) { | ||
| 146 | gameDirs.removeAt(removedDirIndex) | ||
| 147 | NativeConfig.setGameDirs(gameDirs.toTypedArray()) | ||
| 148 | getGameDirs() | ||
| 149 | } | ||
| 150 | } | ||
| 151 | } | ||
| 152 | |||
| 153 | fun updateGameDirs() = | ||
| 154 | viewModelScope.launch { | ||
| 155 | withContext(Dispatchers.IO) { | ||
| 156 | NativeConfig.setGameDirs(_folders.value.toTypedArray()) | ||
| 157 | getGameDirs() | ||
| 158 | } | ||
| 159 | } | ||
| 160 | |||
| 161 | fun onOpenGameFoldersFragment() = | ||
| 162 | viewModelScope.launch { | ||
| 163 | withContext(Dispatchers.IO) { | ||
| 164 | getGameDirs() | ||
| 165 | } | ||
| 166 | } | ||
| 167 | |||
| 168 | fun onCloseGameFoldersFragment() = | ||
| 169 | viewModelScope.launch { | ||
| 170 | withContext(Dispatchers.IO) { | ||
| 171 | getGameDirs(true) | ||
| 172 | } | ||
| 173 | } | ||
| 174 | |||
| 175 | private fun getGameDirs(reloadList: Boolean = false) { | ||
| 176 | val gameDirs = NativeConfig.getGameDirs() | ||
| 177 | _folders.value = gameDirs.toMutableList() | ||
| 178 | if (reloadList) { | ||
| 179 | reloadGames(true) | ||
| 180 | } | ||
| 181 | } | ||
| 125 | } | 182 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt index 756f76721..251b5a667 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt | |||
| @@ -3,15 +3,9 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.model | 4 | package org.yuzu.yuzu_emu.model |
| 5 | 5 | ||
| 6 | import android.net.Uri | ||
| 7 | import androidx.fragment.app.FragmentActivity | ||
| 8 | import androidx.lifecycle.ViewModel | 6 | import androidx.lifecycle.ViewModel |
| 9 | import androidx.lifecycle.ViewModelProvider | ||
| 10 | import androidx.preference.PreferenceManager | ||
| 11 | import kotlinx.coroutines.flow.MutableStateFlow | 7 | import kotlinx.coroutines.flow.MutableStateFlow |
| 12 | import kotlinx.coroutines.flow.StateFlow | 8 | import kotlinx.coroutines.flow.StateFlow |
| 13 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 14 | import org.yuzu.yuzu_emu.utils.GameHelper | ||
| 15 | 9 | ||
| 16 | class HomeViewModel : ViewModel() { | 10 | class HomeViewModel : ViewModel() { |
| 17 | val navigationVisible: StateFlow<Pair<Boolean, Boolean>> get() = _navigationVisible | 11 | val navigationVisible: StateFlow<Pair<Boolean, Boolean>> get() = _navigationVisible |
| @@ -23,14 +17,6 @@ class HomeViewModel : ViewModel() { | |||
| 23 | val shouldPageForward: StateFlow<Boolean> get() = _shouldPageForward | 17 | val shouldPageForward: StateFlow<Boolean> get() = _shouldPageForward |
| 24 | private val _shouldPageForward = MutableStateFlow(false) | 18 | private val _shouldPageForward = MutableStateFlow(false) |
| 25 | 19 | ||
| 26 | val gamesDir: StateFlow<String> get() = _gamesDir | ||
| 27 | private val _gamesDir = MutableStateFlow( | ||
| 28 | Uri.parse( | ||
| 29 | PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) | ||
| 30 | .getString(GameHelper.KEY_GAME_PATH, "") | ||
| 31 | ).path ?: "" | ||
| 32 | ) | ||
| 33 | |||
| 34 | var navigatedToSetup = false | 20 | var navigatedToSetup = false |
| 35 | 21 | ||
| 36 | fun setNavigationVisibility(visible: Boolean, animated: Boolean) { | 22 | fun setNavigationVisibility(visible: Boolean, animated: Boolean) { |
| @@ -50,9 +36,4 @@ class HomeViewModel : ViewModel() { | |||
| 50 | fun setShouldPageForward(pageForward: Boolean) { | 36 | fun setShouldPageForward(pageForward: Boolean) { |
| 51 | _shouldPageForward.value = pageForward | 37 | _shouldPageForward.value = pageForward |
| 52 | } | 38 | } |
| 53 | |||
| 54 | fun setGamesDir(activity: FragmentActivity, dir: String) { | ||
| 55 | ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true) | ||
| 56 | _gamesDir.value = dir | ||
| 57 | } | ||
| 58 | } | 39 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index bd2f4cd25..745901e19 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt | |||
| @@ -40,6 +40,7 @@ import org.yuzu.yuzu_emu.R | |||
| 40 | import org.yuzu.yuzu_emu.activities.EmulationActivity | 40 | import org.yuzu.yuzu_emu.activities.EmulationActivity |
| 41 | import org.yuzu.yuzu_emu.databinding.ActivityMainBinding | 41 | import org.yuzu.yuzu_emu.databinding.ActivityMainBinding |
| 42 | import org.yuzu.yuzu_emu.features.settings.model.Settings | 42 | import org.yuzu.yuzu_emu.features.settings.model.Settings |
| 43 | import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment | ||
| 43 | import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment | 44 | import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment |
| 44 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment | 45 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment |
| 45 | import org.yuzu.yuzu_emu.getPublicFilesDir | 46 | import org.yuzu.yuzu_emu.getPublicFilesDir |
| @@ -293,20 +294,19 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 293 | Intent.FLAG_GRANT_READ_URI_PERMISSION | 294 | Intent.FLAG_GRANT_READ_URI_PERMISSION |
| 294 | ) | 295 | ) |
| 295 | 296 | ||
| 296 | // When a new directory is picked, we currently will reset the existing games | 297 | val uriString = result.toString() |
| 297 | // database. This effectively means that only one game directory is supported. | 298 | val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString } |
| 298 | PreferenceManager.getDefaultSharedPreferences(applicationContext).edit() | 299 | if (folder != null) { |
| 299 | .putString(GameHelper.KEY_GAME_PATH, result.toString()) | 300 | Toast.makeText( |
| 300 | .apply() | 301 | applicationContext, |
| 301 | 302 | R.string.folder_already_added, | |
| 302 | Toast.makeText( | 303 | Toast.LENGTH_SHORT |
| 303 | applicationContext, | 304 | ).show() |
| 304 | R.string.games_dir_selected, | 305 | return |
| 305 | Toast.LENGTH_LONG | 306 | } |
| 306 | ).show() | ||
| 307 | 307 | ||
| 308 | gamesViewModel.reloadGames(true) | 308 | AddGameFolderDialogFragment.newInstance(uriString) |
| 309 | homeViewModel.setGamesDir(this, result.path!!) | 309 | .show(supportFragmentManager, AddGameFolderDialogFragment.TAG) |
| 310 | } | 310 | } |
| 311 | 311 | ||
| 312 | val getProdKey = | 312 | val getProdKey = |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt index 8c3268e9c..bbe7bfa92 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt | |||
| @@ -364,6 +364,27 @@ object FileUtil { | |||
| 364 | .lowercase() | 364 | .lowercase() |
| 365 | } | 365 | } |
| 366 | 366 | ||
| 367 | fun isTreeUriValid(uri: Uri): Boolean { | ||
| 368 | val resolver = context.contentResolver | ||
| 369 | val columns = arrayOf( | ||
| 370 | DocumentsContract.Document.COLUMN_DOCUMENT_ID, | ||
| 371 | DocumentsContract.Document.COLUMN_DISPLAY_NAME, | ||
| 372 | DocumentsContract.Document.COLUMN_MIME_TYPE | ||
| 373 | ) | ||
| 374 | return try { | ||
| 375 | val docId: String = if (isRootTreeUri(uri)) { | ||
| 376 | DocumentsContract.getTreeDocumentId(uri) | ||
| 377 | } else { | ||
| 378 | DocumentsContract.getDocumentId(uri) | ||
| 379 | } | ||
| 380 | val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId) | ||
| 381 | resolver.query(childrenUri, columns, null, null, null) | ||
| 382 | true | ||
| 383 | } catch (_: Exception) { | ||
| 384 | false | ||
| 385 | } | ||
| 386 | } | ||
| 387 | |||
| 367 | @Throws(IOException::class) | 388 | @Throws(IOException::class) |
| 368 | fun getStringFromFile(file: File): String = | 389 | fun getStringFromFile(file: File): String = |
| 369 | String(file.readBytes(), StandardCharsets.UTF_8) | 390 | String(file.readBytes(), StandardCharsets.UTF_8) |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt index e6aca6b44..55010dc59 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt | |||
| @@ -11,10 +11,11 @@ import kotlinx.serialization.json.Json | |||
| 11 | import org.yuzu.yuzu_emu.NativeLibrary | 11 | import org.yuzu.yuzu_emu.NativeLibrary |
| 12 | import org.yuzu.yuzu_emu.YuzuApplication | 12 | import org.yuzu.yuzu_emu.YuzuApplication |
| 13 | import org.yuzu.yuzu_emu.model.Game | 13 | import org.yuzu.yuzu_emu.model.Game |
| 14 | import org.yuzu.yuzu_emu.model.GameDir | ||
| 14 | import org.yuzu.yuzu_emu.model.MinimalDocumentFile | 15 | import org.yuzu.yuzu_emu.model.MinimalDocumentFile |
| 15 | 16 | ||
| 16 | object GameHelper { | 17 | object GameHelper { |
| 17 | const val KEY_GAME_PATH = "game_path" | 18 | private const val KEY_OLD_GAME_PATH = "game_path" |
| 18 | const val KEY_GAMES = "Games" | 19 | const val KEY_GAMES = "Games" |
| 19 | 20 | ||
| 20 | private lateinit var preferences: SharedPreferences | 21 | private lateinit var preferences: SharedPreferences |
| @@ -22,15 +23,43 @@ object GameHelper { | |||
| 22 | fun getGames(): List<Game> { | 23 | fun getGames(): List<Game> { |
| 23 | val games = mutableListOf<Game>() | 24 | val games = mutableListOf<Game>() |
| 24 | val context = YuzuApplication.appContext | 25 | val context = YuzuApplication.appContext |
| 25 | val gamesDir = | ||
| 26 | PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "") | ||
| 27 | val gamesUri = Uri.parse(gamesDir) | ||
| 28 | preferences = PreferenceManager.getDefaultSharedPreferences(context) | 26 | preferences = PreferenceManager.getDefaultSharedPreferences(context) |
| 29 | 27 | ||
| 28 | val gameDirs = mutableListOf<GameDir>() | ||
| 29 | val oldGamesDir = preferences.getString(KEY_OLD_GAME_PATH, "") ?: "" | ||
| 30 | if (oldGamesDir.isNotEmpty()) { | ||
| 31 | gameDirs.add(GameDir(oldGamesDir, true)) | ||
| 32 | preferences.edit().remove(KEY_OLD_GAME_PATH).apply() | ||
| 33 | } | ||
| 34 | gameDirs.addAll(NativeConfig.getGameDirs()) | ||
| 35 | |||
| 30 | // Ensure keys are loaded so that ROM metadata can be decrypted. | 36 | // Ensure keys are loaded so that ROM metadata can be decrypted. |
| 31 | NativeLibrary.reloadKeys() | 37 | NativeLibrary.reloadKeys() |
| 32 | 38 | ||
| 33 | addGamesRecursive(games, FileUtil.listFiles(gamesUri), 3) | 39 | val badDirs = mutableListOf<Int>() |
| 40 | gameDirs.forEachIndexed { index: Int, gameDir: GameDir -> | ||
| 41 | val gameDirUri = Uri.parse(gameDir.uriString) | ||
| 42 | val isValid = FileUtil.isTreeUriValid(gameDirUri) | ||
| 43 | if (isValid) { | ||
| 44 | addGamesRecursive( | ||
| 45 | games, | ||
| 46 | FileUtil.listFiles(gameDirUri), | ||
| 47 | if (gameDir.deepScan) 3 else 1 | ||
| 48 | ) | ||
| 49 | } else { | ||
| 50 | badDirs.add(index) | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | // Remove all game dirs with insufficient permissions from config | ||
| 55 | if (badDirs.isNotEmpty()) { | ||
| 56 | var offset = 0 | ||
| 57 | badDirs.forEach { | ||
| 58 | gameDirs.removeAt(it - offset) | ||
| 59 | offset++ | ||
| 60 | } | ||
| 61 | } | ||
| 62 | NativeConfig.setGameDirs(gameDirs.toTypedArray()) | ||
| 34 | 63 | ||
| 35 | // Cache list of games found on disk | 64 | // Cache list of games found on disk |
| 36 | val serializedGames = mutableSetOf<String>() | 65 | val serializedGames = mutableSetOf<String>() |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt index 87e579fa7..f4e1bb13f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt | |||
| @@ -3,6 +3,8 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.utils | 4 | package org.yuzu.yuzu_emu.utils |
| 5 | 5 | ||
| 6 | import org.yuzu.yuzu_emu.model.GameDir | ||
| 7 | |||
| 6 | object NativeConfig { | 8 | object NativeConfig { |
| 7 | /** | 9 | /** |
| 8 | * Creates a Config object and opens the emulation config. | 10 | * Creates a Config object and opens the emulation config. |
| @@ -54,4 +56,22 @@ object NativeConfig { | |||
| 54 | external fun getConfigHeader(category: Int): String | 56 | external fun getConfigHeader(category: Int): String |
| 55 | 57 | ||
| 56 | external fun getPairedSettingKey(key: String): String | 58 | external fun getPairedSettingKey(key: String): String |
| 59 | |||
| 60 | /** | ||
| 61 | * Gets every [GameDir] in AndroidSettings::values.game_dirs | ||
| 62 | */ | ||
| 63 | @Synchronized | ||
| 64 | external fun getGameDirs(): Array<GameDir> | ||
| 65 | |||
| 66 | /** | ||
| 67 | * Clears the AndroidSettings::values.game_dirs array and replaces them with the provided array | ||
| 68 | */ | ||
| 69 | @Synchronized | ||
| 70 | external fun setGameDirs(dirs: Array<GameDir>) | ||
| 71 | |||
| 72 | /** | ||
| 73 | * Adds a single [GameDir] to the AndroidSettings::values.game_dirs array | ||
| 74 | */ | ||
| 75 | @Synchronized | ||
| 76 | external fun addGameDir(dir: GameDir) | ||
| 57 | } | 77 | } |
diff --git a/src/android/app/src/main/jni/android_config.cpp b/src/android/app/src/main/jni/android_config.cpp index 3041c25c9..767d8ea83 100644 --- a/src/android/app/src/main/jni/android_config.cpp +++ b/src/android/app/src/main/jni/android_config.cpp | |||
| @@ -34,6 +34,7 @@ void AndroidConfig::SaveAllValues() { | |||
| 34 | void AndroidConfig::ReadAndroidValues() { | 34 | void AndroidConfig::ReadAndroidValues() { |
| 35 | if (global) { | 35 | if (global) { |
| 36 | ReadAndroidUIValues(); | 36 | ReadAndroidUIValues(); |
| 37 | ReadUIValues(); | ||
| 37 | } | 38 | } |
| 38 | } | 39 | } |
| 39 | 40 | ||
| @@ -45,9 +46,35 @@ void AndroidConfig::ReadAndroidUIValues() { | |||
| 45 | EndGroup(); | 46 | EndGroup(); |
| 46 | } | 47 | } |
| 47 | 48 | ||
| 49 | void AndroidConfig::ReadUIValues() { | ||
| 50 | BeginGroup(Settings::TranslateCategory(Settings::Category::Ui)); | ||
| 51 | |||
| 52 | ReadPathValues(); | ||
| 53 | |||
| 54 | EndGroup(); | ||
| 55 | } | ||
| 56 | |||
| 57 | void AndroidConfig::ReadPathValues() { | ||
| 58 | BeginGroup(Settings::TranslateCategory(Settings::Category::Paths)); | ||
| 59 | |||
| 60 | const int gamedirs_size = BeginArray(std::string("gamedirs")); | ||
| 61 | for (int i = 0; i < gamedirs_size; ++i) { | ||
| 62 | SetArrayIndex(i); | ||
| 63 | AndroidSettings::GameDir game_dir; | ||
| 64 | game_dir.path = ReadStringSetting(std::string("path")); | ||
| 65 | game_dir.deep_scan = | ||
| 66 | ReadBooleanSetting(std::string("deep_scan"), std::make_optional(false)); | ||
| 67 | AndroidSettings::values.game_dirs.push_back(game_dir); | ||
| 68 | } | ||
| 69 | EndArray(); | ||
| 70 | |||
| 71 | EndGroup(); | ||
| 72 | } | ||
| 73 | |||
| 48 | void AndroidConfig::SaveAndroidValues() { | 74 | void AndroidConfig::SaveAndroidValues() { |
| 49 | if (global) { | 75 | if (global) { |
| 50 | SaveAndroidUIValues(); | 76 | SaveAndroidUIValues(); |
| 77 | SaveUIValues(); | ||
| 51 | } | 78 | } |
| 52 | 79 | ||
| 53 | WriteToIni(); | 80 | WriteToIni(); |
| @@ -61,6 +88,29 @@ void AndroidConfig::SaveAndroidUIValues() { | |||
| 61 | EndGroup(); | 88 | EndGroup(); |
| 62 | } | 89 | } |
| 63 | 90 | ||
| 91 | void AndroidConfig::SaveUIValues() { | ||
| 92 | BeginGroup(Settings::TranslateCategory(Settings::Category::Ui)); | ||
| 93 | |||
| 94 | SavePathValues(); | ||
| 95 | |||
| 96 | EndGroup(); | ||
| 97 | } | ||
| 98 | |||
| 99 | void AndroidConfig::SavePathValues() { | ||
| 100 | BeginGroup(Settings::TranslateCategory(Settings::Category::Paths)); | ||
| 101 | |||
| 102 | BeginArray(std::string("gamedirs")); | ||
| 103 | for (size_t i = 0; i < AndroidSettings::values.game_dirs.size(); ++i) { | ||
| 104 | SetArrayIndex(i); | ||
| 105 | const auto& game_dir = AndroidSettings::values.game_dirs[i]; | ||
| 106 | WriteSetting(std::string("path"), game_dir.path); | ||
| 107 | WriteSetting(std::string("deep_scan"), game_dir.deep_scan, std::make_optional(false)); | ||
| 108 | } | ||
| 109 | EndArray(); | ||
| 110 | |||
| 111 | EndGroup(); | ||
| 112 | } | ||
| 113 | |||
| 64 | std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) { | 114 | std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) { |
| 65 | auto& map = Settings::values.linkage.by_category; | 115 | auto& map = Settings::values.linkage.by_category; |
| 66 | if (map.contains(category)) { | 116 | if (map.contains(category)) { |
diff --git a/src/android/app/src/main/jni/android_config.h b/src/android/app/src/main/jni/android_config.h index e679392fd..f490be016 100644 --- a/src/android/app/src/main/jni/android_config.h +++ b/src/android/app/src/main/jni/android_config.h | |||
| @@ -19,9 +19,9 @@ protected: | |||
| 19 | void ReadAndroidUIValues(); | 19 | void ReadAndroidUIValues(); |
| 20 | void ReadHidbusValues() override {} | 20 | void ReadHidbusValues() override {} |
| 21 | void ReadDebugControlValues() override {} | 21 | void ReadDebugControlValues() override {} |
| 22 | void ReadPathValues() override {} | 22 | void ReadPathValues() override; |
| 23 | void ReadShortcutValues() override {} | 23 | void ReadShortcutValues() override {} |
| 24 | void ReadUIValues() override {} | 24 | void ReadUIValues() override; |
| 25 | void ReadUIGamelistValues() override {} | 25 | void ReadUIGamelistValues() override {} |
| 26 | void ReadUILayoutValues() override {} | 26 | void ReadUILayoutValues() override {} |
| 27 | void ReadMultiplayerValues() override {} | 27 | void ReadMultiplayerValues() override {} |
| @@ -30,9 +30,9 @@ protected: | |||
| 30 | void SaveAndroidUIValues(); | 30 | void SaveAndroidUIValues(); |
| 31 | void SaveHidbusValues() override {} | 31 | void SaveHidbusValues() override {} |
| 32 | void SaveDebugControlValues() override {} | 32 | void SaveDebugControlValues() override {} |
| 33 | void SavePathValues() override {} | 33 | void SavePathValues() override; |
| 34 | void SaveShortcutValues() override {} | 34 | void SaveShortcutValues() override {} |
| 35 | void SaveUIValues() override {} | 35 | void SaveUIValues() override; |
| 36 | void SaveUIGamelistValues() override {} | 36 | void SaveUIGamelistValues() override {} |
| 37 | void SaveUILayoutValues() override {} | 37 | void SaveUILayoutValues() override {} |
| 38 | void SaveMultiplayerValues() override {} | 38 | void SaveMultiplayerValues() override {} |
diff --git a/src/android/app/src/main/jni/android_settings.h b/src/android/app/src/main/jni/android_settings.h index 37bc33918..fc0523206 100644 --- a/src/android/app/src/main/jni/android_settings.h +++ b/src/android/app/src/main/jni/android_settings.h | |||
| @@ -9,9 +9,17 @@ | |||
| 9 | 9 | ||
| 10 | namespace AndroidSettings { | 10 | namespace AndroidSettings { |
| 11 | 11 | ||
| 12 | struct GameDir { | ||
| 13 | std::string path; | ||
| 14 | bool deep_scan = false; | ||
| 15 | }; | ||
| 16 | |||
| 12 | struct Values { | 17 | struct Values { |
| 13 | Settings::Linkage linkage; | 18 | Settings::Linkage linkage; |
| 14 | 19 | ||
| 20 | // Path settings | ||
| 21 | std::vector<GameDir> game_dirs; | ||
| 22 | |||
| 15 | // Android | 23 | // Android |
| 16 | Settings::Setting<bool> picture_in_picture{linkage, false, "picture_in_picture", | 24 | Settings::Setting<bool> picture_in_picture{linkage, false, "picture_in_picture", |
| 17 | Settings::Category::Android}; | 25 | Settings::Category::Android}; |
diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp index 960abf95a..a56ed5662 100644 --- a/src/android/app/src/main/jni/id_cache.cpp +++ b/src/android/app/src/main/jni/id_cache.cpp | |||
| @@ -13,6 +13,8 @@ static JavaVM* s_java_vm; | |||
| 13 | static jclass s_native_library_class; | 13 | static jclass s_native_library_class; |
| 14 | static jclass s_disk_cache_progress_class; | 14 | static jclass s_disk_cache_progress_class; |
| 15 | static jclass s_load_callback_stage_class; | 15 | static jclass s_load_callback_stage_class; |
| 16 | static jclass s_game_dir_class; | ||
| 17 | static jmethodID s_game_dir_constructor; | ||
| 16 | static jmethodID s_exit_emulation_activity; | 18 | static jmethodID s_exit_emulation_activity; |
| 17 | static jmethodID s_disk_cache_load_progress; | 19 | static jmethodID s_disk_cache_load_progress; |
| 18 | static jmethodID s_on_emulation_started; | 20 | static jmethodID s_on_emulation_started; |
| @@ -53,6 +55,14 @@ jclass GetDiskCacheLoadCallbackStageClass() { | |||
| 53 | return s_load_callback_stage_class; | 55 | return s_load_callback_stage_class; |
| 54 | } | 56 | } |
| 55 | 57 | ||
| 58 | jclass GetGameDirClass() { | ||
| 59 | return s_game_dir_class; | ||
| 60 | } | ||
| 61 | |||
| 62 | jmethodID GetGameDirConstructor() { | ||
| 63 | return s_game_dir_constructor; | ||
| 64 | } | ||
| 65 | |||
| 56 | jmethodID GetExitEmulationActivity() { | 66 | jmethodID GetExitEmulationActivity() { |
| 57 | return s_exit_emulation_activity; | 67 | return s_exit_emulation_activity; |
| 58 | } | 68 | } |
| @@ -90,6 +100,11 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { | |||
| 90 | s_load_callback_stage_class = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass( | 100 | s_load_callback_stage_class = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass( |
| 91 | "org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage"))); | 101 | "org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage"))); |
| 92 | 102 | ||
| 103 | const jclass game_dir_class = env->FindClass("org/yuzu/yuzu_emu/model/GameDir"); | ||
| 104 | s_game_dir_class = reinterpret_cast<jclass>(env->NewGlobalRef(game_dir_class)); | ||
| 105 | s_game_dir_constructor = env->GetMethodID(game_dir_class, "<init>", "(Ljava/lang/String;Z)V"); | ||
| 106 | env->DeleteLocalRef(game_dir_class); | ||
| 107 | |||
| 93 | // Initialize methods | 108 | // Initialize methods |
| 94 | s_exit_emulation_activity = | 109 | s_exit_emulation_activity = |
| 95 | env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V"); | 110 | env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V"); |
| @@ -120,6 +135,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { | |||
| 120 | env->DeleteGlobalRef(s_native_library_class); | 135 | env->DeleteGlobalRef(s_native_library_class); |
| 121 | env->DeleteGlobalRef(s_disk_cache_progress_class); | 136 | env->DeleteGlobalRef(s_disk_cache_progress_class); |
| 122 | env->DeleteGlobalRef(s_load_callback_stage_class); | 137 | env->DeleteGlobalRef(s_load_callback_stage_class); |
| 138 | env->DeleteGlobalRef(s_game_dir_class); | ||
| 123 | 139 | ||
| 124 | // UnInitialize applets | 140 | // UnInitialize applets |
| 125 | SoftwareKeyboard::CleanupJNI(env); | 141 | SoftwareKeyboard::CleanupJNI(env); |
diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h index b76158928..855649efa 100644 --- a/src/android/app/src/main/jni/id_cache.h +++ b/src/android/app/src/main/jni/id_cache.h | |||
| @@ -13,6 +13,8 @@ JNIEnv* GetEnvForThread(); | |||
| 13 | jclass GetNativeLibraryClass(); | 13 | jclass GetNativeLibraryClass(); |
| 14 | jclass GetDiskCacheProgressClass(); | 14 | jclass GetDiskCacheProgressClass(); |
| 15 | jclass GetDiskCacheLoadCallbackStageClass(); | 15 | jclass GetDiskCacheLoadCallbackStageClass(); |
| 16 | jclass GetGameDirClass(); | ||
| 17 | jmethodID GetGameDirConstructor(); | ||
| 16 | jmethodID GetExitEmulationActivity(); | 18 | jmethodID GetExitEmulationActivity(); |
| 17 | jmethodID GetDiskCacheLoadProgress(); | 19 | jmethodID GetDiskCacheLoadProgress(); |
| 18 | jmethodID GetOnEmulationStarted(); | 20 | jmethodID GetOnEmulationStarted(); |
diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp index 8e81816e5..763b2164c 100644 --- a/src/android/app/src/main/jni/native_config.cpp +++ b/src/android/app/src/main/jni/native_config.cpp | |||
| @@ -11,6 +11,7 @@ | |||
| 11 | #include "common/settings.h" | 11 | #include "common/settings.h" |
| 12 | #include "frontend_common/config.h" | 12 | #include "frontend_common/config.h" |
| 13 | #include "jni/android_common/android_common.h" | 13 | #include "jni/android_common/android_common.h" |
| 14 | #include "jni/id_cache.h" | ||
| 14 | 15 | ||
| 15 | std::unique_ptr<AndroidConfig> config; | 16 | std::unique_ptr<AndroidConfig> config; |
| 16 | 17 | ||
| @@ -253,4 +254,55 @@ jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* e | |||
| 253 | return ToJString(env, setting->PairedSetting()->GetLabel()); | 254 | return ToJString(env, setting->PairedSetting()->GetLabel()); |
| 254 | } | 255 | } |
| 255 | 256 | ||
| 257 | jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getGameDirs(JNIEnv* env, jobject obj) { | ||
| 258 | jclass gameDirClass = IDCache::GetGameDirClass(); | ||
| 259 | jmethodID gameDirConstructor = IDCache::GetGameDirConstructor(); | ||
| 260 | jobjectArray jgameDirArray = | ||
| 261 | env->NewObjectArray(AndroidSettings::values.game_dirs.size(), gameDirClass, nullptr); | ||
| 262 | for (size_t i = 0; i < AndroidSettings::values.game_dirs.size(); ++i) { | ||
| 263 | jobject jgameDir = | ||
| 264 | env->NewObject(gameDirClass, gameDirConstructor, | ||
| 265 | ToJString(env, AndroidSettings::values.game_dirs[i].path), | ||
| 266 | static_cast<jboolean>(AndroidSettings::values.game_dirs[i].deep_scan)); | ||
| 267 | env->SetObjectArrayElement(jgameDirArray, i, jgameDir); | ||
| 268 | } | ||
| 269 | return jgameDirArray; | ||
| 270 | } | ||
| 271 | |||
| 272 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setGameDirs(JNIEnv* env, jobject obj, | ||
| 273 | jobjectArray gameDirs) { | ||
| 274 | AndroidSettings::values.game_dirs.clear(); | ||
| 275 | int size = env->GetArrayLength(gameDirs); | ||
| 276 | |||
| 277 | if (size == 0) { | ||
| 278 | return; | ||
| 279 | } | ||
| 280 | |||
| 281 | jobject dir = env->GetObjectArrayElement(gameDirs, 0); | ||
| 282 | jclass gameDirClass = IDCache::GetGameDirClass(); | ||
| 283 | jfieldID uriStringField = env->GetFieldID(gameDirClass, "uriString", "Ljava/lang/String;"); | ||
| 284 | jfieldID deepScanBooleanField = env->GetFieldID(gameDirClass, "deepScan", "Z"); | ||
| 285 | for (int i = 0; i < size; ++i) { | ||
| 286 | dir = env->GetObjectArrayElement(gameDirs, i); | ||
| 287 | jstring juriString = static_cast<jstring>(env->GetObjectField(dir, uriStringField)); | ||
| 288 | jboolean jdeepScanBoolean = env->GetBooleanField(dir, deepScanBooleanField); | ||
| 289 | std::string uriString = GetJString(env, juriString); | ||
| 290 | AndroidSettings::values.game_dirs.push_back( | ||
| 291 | AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)}); | ||
| 292 | } | ||
| 293 | } | ||
| 294 | |||
| 295 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_addGameDir(JNIEnv* env, jobject obj, | ||
| 296 | jobject gameDir) { | ||
| 297 | jclass gameDirClass = IDCache::GetGameDirClass(); | ||
| 298 | jfieldID uriStringField = env->GetFieldID(gameDirClass, "uriString", "Ljava/lang/String;"); | ||
| 299 | jfieldID deepScanBooleanField = env->GetFieldID(gameDirClass, "deepScan", "Z"); | ||
| 300 | |||
| 301 | jstring juriString = static_cast<jstring>(env->GetObjectField(gameDir, uriStringField)); | ||
| 302 | jboolean jdeepScanBoolean = env->GetBooleanField(gameDir, deepScanBooleanField); | ||
| 303 | std::string uriString = GetJString(env, juriString); | ||
| 304 | AndroidSettings::values.game_dirs.push_back( | ||
| 305 | AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)}); | ||
| 306 | } | ||
| 307 | |||
| 256 | } // extern "C" | 308 | } // extern "C" |
diff --git a/src/android/app/src/main/res/layout/card_folder.xml b/src/android/app/src/main/res/layout/card_folder.xml new file mode 100644 index 000000000..4e0c04b6b --- /dev/null +++ b/src/android/app/src/main/res/layout/card_folder.xml | |||
| @@ -0,0 +1,70 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 4 | xmlns:tools="http://schemas.android.com/tools" | ||
| 5 | style="?attr/materialCardViewOutlinedStyle" | ||
| 6 | android:layout_width="match_parent" | ||
| 7 | android:layout_height="wrap_content" | ||
| 8 | android:layout_marginHorizontal="16dp" | ||
| 9 | android:layout_marginVertical="12dp" | ||
| 10 | android:focusable="true"> | ||
| 11 | |||
| 12 | <androidx.constraintlayout.widget.ConstraintLayout | ||
| 13 | android:layout_width="match_parent" | ||
| 14 | android:layout_height="wrap_content" | ||
| 15 | android:orientation="horizontal" | ||
| 16 | android:padding="16dp" | ||
| 17 | android:layout_gravity="center_vertical" | ||
| 18 | android:animateLayoutChanges="true"> | ||
| 19 | |||
| 20 | <com.google.android.material.textview.MaterialTextView | ||
| 21 | android:id="@+id/path" | ||
| 22 | style="@style/TextAppearance.Material3.BodyLarge" | ||
| 23 | android:layout_width="0dp" | ||
| 24 | android:layout_height="wrap_content" | ||
| 25 | android:layout_gravity="center_vertical|start" | ||
| 26 | android:ellipsize="none" | ||
| 27 | android:marqueeRepeatLimit="marquee_forever" | ||
| 28 | android:requiresFadingEdge="horizontal" | ||
| 29 | android:singleLine="true" | ||
| 30 | android:textAlignment="viewStart" | ||
| 31 | app:layout_constraintBottom_toBottomOf="parent" | ||
| 32 | app:layout_constraintEnd_toStartOf="@+id/button_layout" | ||
| 33 | app:layout_constraintStart_toStartOf="parent" | ||
| 34 | app:layout_constraintTop_toTopOf="parent" | ||
| 35 | tools:text="@string/select_gpu_driver_default" /> | ||
| 36 | |||
| 37 | <LinearLayout | ||
| 38 | android:id="@+id/button_layout" | ||
| 39 | android:layout_width="wrap_content" | ||
| 40 | android:layout_height="wrap_content" | ||
| 41 | android:orientation="horizontal" | ||
| 42 | app:layout_constraintBottom_toBottomOf="parent" | ||
| 43 | app:layout_constraintEnd_toEndOf="parent" | ||
| 44 | app:layout_constraintTop_toTopOf="parent"> | ||
| 45 | |||
| 46 | <Button | ||
| 47 | android:id="@+id/button_edit" | ||
| 48 | style="@style/Widget.Material3.Button.IconButton" | ||
| 49 | android:layout_width="wrap_content" | ||
| 50 | android:layout_height="wrap_content" | ||
| 51 | android:contentDescription="@string/delete" | ||
| 52 | android:tooltipText="@string/edit" | ||
| 53 | app:icon="@drawable/ic_edit" | ||
| 54 | app:iconTint="?attr/colorControlNormal" /> | ||
| 55 | |||
| 56 | <Button | ||
| 57 | android:id="@+id/button_delete" | ||
| 58 | style="@style/Widget.Material3.Button.IconButton" | ||
| 59 | android:layout_width="wrap_content" | ||
| 60 | android:layout_height="wrap_content" | ||
| 61 | android:contentDescription="@string/delete" | ||
| 62 | android:tooltipText="@string/delete" | ||
| 63 | app:icon="@drawable/ic_delete" | ||
| 64 | app:iconTint="?attr/colorControlNormal" /> | ||
| 65 | |||
| 66 | </LinearLayout> | ||
| 67 | |||
| 68 | </androidx.constraintlayout.widget.ConstraintLayout> | ||
| 69 | |||
| 70 | </com.google.android.material.card.MaterialCardView> | ||
diff --git a/src/android/app/src/main/res/layout/dialog_add_folder.xml b/src/android/app/src/main/res/layout/dialog_add_folder.xml new file mode 100644 index 000000000..01f95e868 --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_add_folder.xml | |||
| @@ -0,0 +1,45 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | xmlns:tools="http://schemas.android.com/tools" | ||
| 4 | android:layout_width="match_parent" | ||
| 5 | android:layout_height="wrap_content" | ||
| 6 | android:padding="24dp" | ||
| 7 | android:orientation="vertical"> | ||
| 8 | |||
| 9 | <com.google.android.material.textview.MaterialTextView | ||
| 10 | android:id="@+id/path" | ||
| 11 | style="@style/TextAppearance.Material3.BodyLarge" | ||
| 12 | android:layout_width="match_parent" | ||
| 13 | android:layout_height="0dp" | ||
| 14 | android:layout_gravity="center_vertical|start" | ||
| 15 | android:layout_weight="1" | ||
| 16 | android:ellipsize="marquee" | ||
| 17 | android:marqueeRepeatLimit="marquee_forever" | ||
| 18 | android:requiresFadingEdge="horizontal" | ||
| 19 | android:singleLine="true" | ||
| 20 | android:textAlignment="viewStart" | ||
| 21 | tools:text="folder/folder/folder/folder" /> | ||
| 22 | |||
| 23 | <LinearLayout | ||
| 24 | android:layout_width="match_parent" | ||
| 25 | android:layout_height="wrap_content" | ||
| 26 | android:orientation="horizontal" | ||
| 27 | android:paddingTop="8dp"> | ||
| 28 | |||
| 29 | <com.google.android.material.textview.MaterialTextView | ||
| 30 | style="@style/TextAppearance.Material3.BodyMedium" | ||
| 31 | android:layout_width="0dp" | ||
| 32 | android:layout_height="wrap_content" | ||
| 33 | android:layout_gravity="center_vertical|start" | ||
| 34 | android:layout_weight="1" | ||
| 35 | android:text="@string/deep_scan" | ||
| 36 | android:textAlignment="viewStart" /> | ||
| 37 | |||
| 38 | <com.google.android.material.checkbox.MaterialCheckBox | ||
| 39 | android:id="@+id/deep_scan_switch" | ||
| 40 | android:layout_width="wrap_content" | ||
| 41 | android:layout_height="wrap_content" /> | ||
| 42 | |||
| 43 | </LinearLayout> | ||
| 44 | |||
| 45 | </LinearLayout> | ||
diff --git a/src/android/app/src/main/res/layout/dialog_folder_properties.xml b/src/android/app/src/main/res/layout/dialog_folder_properties.xml new file mode 100644 index 000000000..248d048cb --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_folder_properties.xml | |||
| @@ -0,0 +1,30 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | android:layout_width="match_parent" | ||
| 4 | android:layout_height="wrap_content" | ||
| 5 | android:padding="24dp" | ||
| 6 | android:orientation="vertical"> | ||
| 7 | |||
| 8 | <LinearLayout | ||
| 9 | android:id="@+id/deep_scan_layout" | ||
| 10 | android:layout_width="match_parent" | ||
| 11 | android:layout_height="wrap_content" | ||
| 12 | android:orientation="horizontal"> | ||
| 13 | |||
| 14 | <com.google.android.material.textview.MaterialTextView | ||
| 15 | style="@style/TextAppearance.Material3.BodyMedium" | ||
| 16 | android:layout_width="0dp" | ||
| 17 | android:layout_height="wrap_content" | ||
| 18 | android:layout_gravity="center_vertical|start" | ||
| 19 | android:layout_weight="1" | ||
| 20 | android:text="@string/deep_scan" | ||
| 21 | android:textAlignment="viewStart" /> | ||
| 22 | |||
| 23 | <com.google.android.material.checkbox.MaterialCheckBox | ||
| 24 | android:id="@+id/deep_scan_switch" | ||
| 25 | android:layout_width="wrap_content" | ||
| 26 | android:layout_height="wrap_content" /> | ||
| 27 | |||
| 28 | </LinearLayout> | ||
| 29 | |||
| 30 | </LinearLayout> | ||
diff --git a/src/android/app/src/main/res/layout/fragment_folders.xml b/src/android/app/src/main/res/layout/fragment_folders.xml new file mode 100644 index 000000000..74f2f3754 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_folders.xml | |||
| @@ -0,0 +1,48 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 4 | android:id="@+id/coordinator_folders" | ||
| 5 | android:layout_width="match_parent" | ||
| 6 | android:layout_height="match_parent" | ||
| 7 | android:background="?attr/colorSurface"> | ||
| 8 | |||
| 9 | <androidx.coordinatorlayout.widget.CoordinatorLayout | ||
| 10 | android:layout_width="match_parent" | ||
| 11 | android:layout_height="match_parent"> | ||
| 12 | |||
| 13 | <com.google.android.material.appbar.AppBarLayout | ||
| 14 | android:id="@+id/appbar_folders" | ||
| 15 | android:layout_width="match_parent" | ||
| 16 | android:layout_height="wrap_content" | ||
| 17 | android:fitsSystemWindows="true" | ||
| 18 | app:liftOnScrollTargetViewId="@id/list_folders"> | ||
| 19 | |||
| 20 | <com.google.android.material.appbar.MaterialToolbar | ||
| 21 | android:id="@+id/toolbar_folders" | ||
| 22 | android:layout_width="match_parent" | ||
| 23 | android:layout_height="?attr/actionBarSize" | ||
| 24 | app:navigationIcon="@drawable/ic_back" | ||
| 25 | app:title="@string/game_folders" /> | ||
| 26 | |||
| 27 | </com.google.android.material.appbar.AppBarLayout> | ||
| 28 | |||
| 29 | <androidx.recyclerview.widget.RecyclerView | ||
| 30 | android:id="@+id/list_folders" | ||
| 31 | android:layout_width="match_parent" | ||
| 32 | android:layout_height="wrap_content" | ||
| 33 | android:clipToPadding="false" | ||
| 34 | app:layout_behavior="@string/appbar_scrolling_view_behavior" /> | ||
| 35 | |||
| 36 | </androidx.coordinatorlayout.widget.CoordinatorLayout> | ||
| 37 | |||
| 38 | <com.google.android.material.floatingactionbutton.FloatingActionButton | ||
| 39 | android:id="@+id/button_add" | ||
| 40 | android:layout_width="wrap_content" | ||
| 41 | android:layout_height="wrap_content" | ||
| 42 | android:layout_gravity="bottom|end" | ||
| 43 | android:contentDescription="@string/add_games" | ||
| 44 | app:srcCompat="@drawable/ic_add" | ||
| 45 | app:layout_constraintBottom_toBottomOf="parent" | ||
| 46 | app:layout_constraintEnd_toEndOf="parent" /> | ||
| 47 | |||
| 48 | </androidx.constraintlayout.widget.ConstraintLayout> | ||
diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml index 6d4c1f86d..cf70b4bc4 100644 --- a/src/android/app/src/main/res/navigation/home_navigation.xml +++ b/src/android/app/src/main/res/navigation/home_navigation.xml | |||
| @@ -28,6 +28,9 @@ | |||
| 28 | <action | 28 | <action |
| 29 | android:id="@+id/action_homeSettingsFragment_to_appletLauncherFragment" | 29 | android:id="@+id/action_homeSettingsFragment_to_appletLauncherFragment" |
| 30 | app:destination="@id/appletLauncherFragment" /> | 30 | app:destination="@id/appletLauncherFragment" /> |
| 31 | <action | ||
| 32 | android:id="@+id/action_homeSettingsFragment_to_gameFoldersFragment" | ||
| 33 | app:destination="@id/gameFoldersFragment" /> | ||
| 31 | </fragment> | 34 | </fragment> |
| 32 | 35 | ||
| 33 | <fragment | 36 | <fragment |
| @@ -117,5 +120,9 @@ | |||
| 117 | android:id="@+id/cabinetLauncherDialogFragment" | 120 | android:id="@+id/cabinetLauncherDialogFragment" |
| 118 | android:name="org.yuzu.yuzu_emu.fragments.CabinetLauncherDialogFragment" | 121 | android:name="org.yuzu.yuzu_emu.fragments.CabinetLauncherDialogFragment" |
| 119 | android:label="CabinetLauncherDialogFragment" /> | 122 | android:label="CabinetLauncherDialogFragment" /> |
| 123 | <fragment | ||
| 124 | android:id="@+id/gameFoldersFragment" | ||
| 125 | android:name="org.yuzu.yuzu_emu.fragments.GameFoldersFragment" | ||
| 126 | android:label="GameFoldersFragment" /> | ||
| 120 | 127 | ||
| 121 | </navigation> | 128 | </navigation> |
diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml index ef855ea6f..380d14213 100644 --- a/src/android/app/src/main/res/values/dimens.xml +++ b/src/android/app/src/main/res/values/dimens.xml | |||
| @@ -13,7 +13,7 @@ | |||
| 13 | <dimen name="menu_width">256dp</dimen> | 13 | <dimen name="menu_width">256dp</dimen> |
| 14 | <dimen name="card_width">165dp</dimen> | 14 | <dimen name="card_width">165dp</dimen> |
| 15 | <dimen name="icon_inset">24dp</dimen> | 15 | <dimen name="icon_inset">24dp</dimen> |
| 16 | <dimen name="spacing_bottom_list_fab">72dp</dimen> | 16 | <dimen name="spacing_bottom_list_fab">76dp</dimen> |
| 17 | <dimen name="spacing_fab">24dp</dimen> | 17 | <dimen name="spacing_fab">24dp</dimen> |
| 18 | 18 | ||
| 19 | <dimen name="dialog_margin">20dp</dimen> | 19 | <dimen name="dialog_margin">20dp</dimen> |
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 471af8795..fa9b153b6 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml | |||
| @@ -38,6 +38,7 @@ | |||
| 38 | <string name="empty_gamelist">No files were found or no game directory has been selected yet.</string> | 38 | <string name="empty_gamelist">No files were found or no game directory has been selected yet.</string> |
| 39 | <string name="search_and_filter_games">Search and filter games</string> | 39 | <string name="search_and_filter_games">Search and filter games</string> |
| 40 | <string name="select_games_folder">Select games folder</string> | 40 | <string name="select_games_folder">Select games folder</string> |
| 41 | <string name="manage_game_folders">Manage game folders</string> | ||
| 41 | <string name="select_games_folder_description">Allows yuzu to populate the games list</string> | 42 | <string name="select_games_folder_description">Allows yuzu to populate the games list</string> |
| 42 | <string name="add_games_warning">Skip selecting games folder?</string> | 43 | <string name="add_games_warning">Skip selecting games folder?</string> |
| 43 | <string name="add_games_warning_description">Games won\'t be displayed in the Games list if a folder isn\'t selected.</string> | 44 | <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 @@ | |||
| 124 | <string name="manage_yuzu_data_description">Import/export firmware, keys, user data, and more!</string> | 125 | <string name="manage_yuzu_data_description">Import/export firmware, keys, user data, and more!</string> |
| 125 | <string name="share_save_file">Share save file</string> | 126 | <string name="share_save_file">Share save file</string> |
| 126 | <string name="export_save_failed">Failed to export save</string> | 127 | <string name="export_save_failed">Failed to export save</string> |
| 128 | <string name="game_folders">Game folders</string> | ||
| 129 | <string name="deep_scan">Deep scan</string> | ||
| 130 | <string name="add_game_folder">Add game folder</string> | ||
| 131 | <string name="folder_already_added">This folder was already added!</string> | ||
| 132 | <string name="game_folder_properties">Game folder properties</string> | ||
| 127 | 133 | ||
| 128 | <!-- Applet launcher strings --> | 134 | <!-- Applet launcher strings --> |
| 129 | <string name="applets">Applet launcher</string> | 135 | <string name="applets">Applet launcher</string> |
| @@ -257,6 +263,7 @@ | |||
| 257 | <string name="cancelling">Cancelling</string> | 263 | <string name="cancelling">Cancelling</string> |
| 258 | <string name="install">Install</string> | 264 | <string name="install">Install</string> |
| 259 | <string name="delete">Delete</string> | 265 | <string name="delete">Delete</string> |
| 266 | <string name="edit">Edit</string> | ||
| 260 | <string name="export_success">Exported successfully</string> | 267 | <string name="export_success">Exported successfully</string> |
| 261 | 268 | ||
| 262 | <!-- GPU driver installation --> | 269 | <!-- GPU driver installation --> |
diff --git a/src/frontend_common/config.cpp b/src/frontend_common/config.cpp index 7474cb0f9..1a0491c2c 100644 --- a/src/frontend_common/config.cpp +++ b/src/frontend_common/config.cpp | |||
| @@ -924,12 +924,14 @@ std::string Config::AdjustOutputString(const std::string& string) { | |||
| 924 | 924 | ||
| 925 | // Windows requires that two forward slashes are used at the start of a path for unmapped | 925 | // Windows requires that two forward slashes are used at the start of a path for unmapped |
| 926 | // network drives so we have to watch for that here | 926 | // network drives so we have to watch for that here |
| 927 | #ifndef ANDROID | ||
| 927 | if (string.substr(0, 2) == "//") { | 928 | if (string.substr(0, 2) == "//") { |
| 928 | boost::replace_all(adjusted_string, "//", "/"); | 929 | boost::replace_all(adjusted_string, "//", "/"); |
| 929 | adjusted_string.insert(0, "/"); | 930 | adjusted_string.insert(0, "/"); |
| 930 | } else { | 931 | } else { |
| 931 | boost::replace_all(adjusted_string, "//", "/"); | 932 | boost::replace_all(adjusted_string, "//", "/"); |
| 932 | } | 933 | } |
| 934 | #endif | ||
| 933 | 935 | ||
| 934 | // Needed for backwards compatibility with QSettings deserialization | 936 | // Needed for backwards compatibility with QSettings deserialization |
| 935 | for (const auto& special_character : special_characters) { | 937 | for (const auto& special_character : special_characters) { |