diff options
119 files changed, 3598 insertions, 918 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index e0f01127c..010c44951 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt | |||
| @@ -230,8 +230,6 @@ object NativeLibrary { | |||
| 230 | */ | 230 | */ |
| 231 | external fun onTouchReleased(finger_id: Int) | 231 | external fun onTouchReleased(finger_id: Int) |
| 232 | 232 | ||
| 233 | external fun initGameIni(gameID: String?) | ||
| 234 | |||
| 235 | external fun setAppDirectory(directory: String) | 233 | external fun setAppDirectory(directory: String) |
| 236 | 234 | ||
| 237 | /** | 235 | /** |
| @@ -241,6 +239,8 @@ object NativeLibrary { | |||
| 241 | */ | 239 | */ |
| 242 | external fun installFileToNand(filename: String, extension: String): Int | 240 | external fun installFileToNand(filename: String, extension: String): Int |
| 243 | 241 | ||
| 242 | external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean | ||
| 243 | |||
| 244 | external fun initializeGpuDriver( | 244 | external fun initializeGpuDriver( |
| 245 | hookLibDir: String?, | 245 | hookLibDir: String?, |
| 246 | customDriverDir: String?, | 246 | customDriverDir: String?, |
| @@ -252,18 +252,11 @@ object NativeLibrary { | |||
| 252 | 252 | ||
| 253 | external fun initializeSystem(reload: Boolean) | 253 | external fun initializeSystem(reload: Boolean) |
| 254 | 254 | ||
| 255 | external fun defaultCPUCore(): Int | ||
| 256 | |||
| 257 | /** | 255 | /** |
| 258 | * Begins emulation. | 256 | * Begins emulation. |
| 259 | */ | 257 | */ |
| 260 | external fun run(path: String?) | 258 | external fun run(path: String?) |
| 261 | 259 | ||
| 262 | /** | ||
| 263 | * Begins emulation from the specified savestate. | ||
| 264 | */ | ||
| 265 | external fun run(path: String?, savestatePath: String?, deleteSavestate: Boolean) | ||
| 266 | |||
| 267 | // Surface Handling | 260 | // Surface Handling |
| 268 | external fun surfaceChanged(surf: Surface?) | 261 | external fun surfaceChanged(surf: Surface?) |
| 269 | 262 | ||
| @@ -304,10 +297,9 @@ object NativeLibrary { | |||
| 304 | */ | 297 | */ |
| 305 | external fun getCpuBackend(): String | 298 | external fun getCpuBackend(): String |
| 306 | 299 | ||
| 307 | /** | 300 | external fun applySettings() |
| 308 | * Notifies the core emulation that the orientation has changed. | 301 | |
| 309 | */ | 302 | external fun logSettings() |
| 310 | external fun notifyOrientationChange(layout_option: Int, rotation: Int) | ||
| 311 | 303 | ||
| 312 | enum class CoreError { | 304 | enum class CoreError { |
| 313 | ErrorSystemFiles, | 305 | ErrorSystemFiles, |
| @@ -539,6 +531,35 @@ object NativeLibrary { | |||
| 539 | external fun isFirmwareAvailable(): Boolean | 531 | external fun isFirmwareAvailable(): Boolean |
| 540 | 532 | ||
| 541 | /** | 533 | /** |
| 534 | * Checks the PatchManager for any addons that are available | ||
| 535 | * | ||
| 536 | * @param path Path to game file. Can be a [Uri]. | ||
| 537 | * @param programId String representation of a game's program ID | ||
| 538 | * @return Array of pairs where the first value is the name of an addon and the second is the version | ||
| 539 | */ | ||
| 540 | external fun getAddonsForFile(path: String, programId: String): Array<Pair<String, String>>? | ||
| 541 | |||
| 542 | /** | ||
| 543 | * Gets the save location for a specific game | ||
| 544 | * | ||
| 545 | * @param programId String representation of a game's program ID | ||
| 546 | * @return Save data path that may not exist yet | ||
| 547 | */ | ||
| 548 | external fun getSavePath(programId: String): String | ||
| 549 | |||
| 550 | /** | ||
| 551 | * Adds a file to the manual filesystem provider in our EmulationSession instance | ||
| 552 | * @param path Path to the file we're adding. Can be a string representation of a [Uri] or | ||
| 553 | * a normal path | ||
| 554 | */ | ||
| 555 | external fun addFileToFilesystemProvider(path: String) | ||
| 556 | |||
| 557 | /** | ||
| 558 | * Clears all files added to the manual filesystem provider in our EmulationSession instance | ||
| 559 | */ | ||
| 560 | external fun clearFilesystemProvider() | ||
| 561 | |||
| 562 | /** | ||
| 542 | * Button type for use in onTouchEvent | 563 | * Button type for use in onTouchEvent |
| 543 | */ | 564 | */ |
| 544 | object ButtonType { | 565 | object ButtonType { |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt index f41d7bdbf..9b08f008d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt | |||
| @@ -172,7 +172,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 172 | 172 | ||
| 173 | override fun onUserLeaveHint() { | 173 | override fun onUserLeaveHint() { |
| 174 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { | 174 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { |
| 175 | if (BooleanSetting.PICTURE_IN_PICTURE.boolean && !isInPictureInPictureMode) { | 175 | if (BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && !isInPictureInPictureMode) { |
| 176 | val pictureInPictureParamsBuilder = PictureInPictureParams.Builder() | 176 | val pictureInPictureParamsBuilder = PictureInPictureParams.Builder() |
| 177 | .getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder() | 177 | .getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder() |
| 178 | enterPictureInPictureMode(pictureInPictureParamsBuilder.build()) | 178 | enterPictureInPictureMode(pictureInPictureParamsBuilder.build()) |
| @@ -284,7 +284,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 284 | 284 | ||
| 285 | private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder(): | 285 | private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder(): |
| 286 | PictureInPictureParams.Builder { | 286 | PictureInPictureParams.Builder { |
| 287 | val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.int) { | 287 | val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.getInt()) { |
| 288 | 0 -> Rational(16, 9) | 288 | 0 -> Rational(16, 9) |
| 289 | 1 -> Rational(4, 3) | 289 | 1 -> Rational(4, 3) |
| 290 | 2 -> Rational(21, 9) | 290 | 2 -> Rational(21, 9) |
| @@ -331,7 +331,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 331 | pictureInPictureActions.add(pauseRemoteAction) | 331 | pictureInPictureActions.add(pauseRemoteAction) |
| 332 | } | 332 | } |
| 333 | 333 | ||
| 334 | if (BooleanSetting.AUDIO_MUTED.boolean) { | 334 | if (BooleanSetting.AUDIO_MUTED.getBoolean()) { |
| 335 | val unmuteIcon = Icon.createWithResource( | 335 | val unmuteIcon = Icon.createWithResource( |
| 336 | this@EmulationActivity, | 336 | this@EmulationActivity, |
| 337 | R.drawable.ic_pip_unmute | 337 | R.drawable.ic_pip_unmute |
| @@ -376,7 +376,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 376 | val isEmulationActive = emulationViewModel.emulationStarted.value && | 376 | val isEmulationActive = emulationViewModel.emulationStarted.value && |
| 377 | !emulationViewModel.isEmulationStopping.value | 377 | !emulationViewModel.isEmulationStopping.value |
| 378 | pictureInPictureParamsBuilder.setAutoEnterEnabled( | 378 | pictureInPictureParamsBuilder.setAutoEnterEnabled( |
| 379 | BooleanSetting.PICTURE_IN_PICTURE.boolean && isEmulationActive | 379 | BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && isEmulationActive |
| 380 | ) | 380 | ) |
| 381 | } | 381 | } |
| 382 | setPictureInPictureParams(pictureInPictureParamsBuilder.build()) | 382 | setPictureInPictureParams(pictureInPictureParamsBuilder.build()) |
| @@ -390,9 +390,13 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 390 | if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation() | 390 | if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation() |
| 391 | } | 391 | } |
| 392 | if (intent.action == actionUnmute) { | 392 | if (intent.action == actionUnmute) { |
| 393 | if (BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(false) | 393 | if (BooleanSetting.AUDIO_MUTED.getBoolean()) { |
| 394 | BooleanSetting.AUDIO_MUTED.setBoolean(false) | ||
| 395 | } | ||
| 394 | } else if (intent.action == actionMute) { | 396 | } else if (intent.action == actionMute) { |
| 395 | if (!BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(true) | 397 | if (!BooleanSetting.AUDIO_MUTED.getBoolean()) { |
| 398 | BooleanSetting.AUDIO_MUTED.setBoolean(true) | ||
| 399 | } | ||
| 396 | } | 400 | } |
| 397 | buildPictureInPictureParams() | 401 | buildPictureInPictureParams() |
| 398 | } | 402 | } |
| @@ -423,7 +427,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||
| 423 | } catch (ignored: Exception) { | 427 | } catch (ignored: Exception) { |
| 424 | } | 428 | } |
| 425 | // Always resume audio, since there is no UI button | 429 | // Always resume audio, since there is no UI button |
| 426 | if (BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(false) | 430 | if (BooleanSetting.AUDIO_MUTED.getBoolean()) { |
| 431 | BooleanSetting.AUDIO_MUTED.setBoolean(false) | ||
| 432 | } | ||
| 427 | } | 433 | } |
| 428 | } | 434 | } |
| 429 | 435 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt new file mode 100644 index 000000000..15c7ca3c9 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt | |||
| @@ -0,0 +1,52 @@ | |||
| 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.view.LayoutInflater | ||
| 7 | import android.view.ViewGroup | ||
| 8 | import androidx.recyclerview.widget.AsyncDifferConfig | ||
| 9 | import androidx.recyclerview.widget.DiffUtil | ||
| 10 | import androidx.recyclerview.widget.ListAdapter | ||
| 11 | import androidx.recyclerview.widget.RecyclerView | ||
| 12 | import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding | ||
| 13 | import org.yuzu.yuzu_emu.model.Addon | ||
| 14 | |||
| 15 | class AddonAdapter : ListAdapter<Addon, AddonAdapter.AddonViewHolder>( | ||
| 16 | AsyncDifferConfig.Builder(DiffCallback()).build() | ||
| 17 | ) { | ||
| 18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder { | ||
| 19 | ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false) | ||
| 20 | .also { return AddonViewHolder(it) } | ||
| 21 | } | ||
| 22 | |||
| 23 | override fun getItemCount(): Int = currentList.size | ||
| 24 | |||
| 25 | override fun onBindViewHolder(holder: AddonViewHolder, position: Int) = | ||
| 26 | holder.bind(currentList[position]) | ||
| 27 | |||
| 28 | inner class AddonViewHolder(val binding: ListItemAddonBinding) : | ||
| 29 | RecyclerView.ViewHolder(binding.root) { | ||
| 30 | fun bind(addon: Addon) { | ||
| 31 | binding.root.setOnClickListener { | ||
| 32 | binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked | ||
| 33 | } | ||
| 34 | binding.title.text = addon.title | ||
| 35 | binding.version.text = addon.version | ||
| 36 | binding.addonSwitch.setOnCheckedChangeListener { _, checked -> | ||
| 37 | addon.enabled = checked | ||
| 38 | } | ||
| 39 | binding.addonSwitch.isChecked = addon.enabled | ||
| 40 | } | ||
| 41 | } | ||
| 42 | |||
| 43 | private class DiffCallback : DiffUtil.ItemCallback<Addon>() { | ||
| 44 | override fun areItemsTheSame(oldItem: Addon, newItem: Addon): Boolean { | ||
| 45 | return oldItem == newItem | ||
| 46 | } | ||
| 47 | |||
| 48 | override fun areContentsTheSame(oldItem: Addon, newItem: Addon): Boolean { | ||
| 49 | return oldItem == newItem | ||
| 50 | } | ||
| 51 | } | ||
| 52 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt index a21a705c1..4a05c5be9 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt | |||
| @@ -15,7 +15,7 @@ import org.yuzu.yuzu_emu.HomeNavigationDirections | |||
| 15 | import org.yuzu.yuzu_emu.NativeLibrary | 15 | import org.yuzu.yuzu_emu.NativeLibrary |
| 16 | import org.yuzu.yuzu_emu.R | 16 | import org.yuzu.yuzu_emu.R |
| 17 | import org.yuzu.yuzu_emu.YuzuApplication | 17 | import org.yuzu.yuzu_emu.YuzuApplication |
| 18 | import org.yuzu.yuzu_emu.databinding.CardAppletOptionBinding | 18 | import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding |
| 19 | import org.yuzu.yuzu_emu.model.Applet | 19 | import org.yuzu.yuzu_emu.model.Applet |
| 20 | import org.yuzu.yuzu_emu.model.AppletInfo | 20 | import org.yuzu.yuzu_emu.model.AppletInfo |
| 21 | import org.yuzu.yuzu_emu.model.Game | 21 | import org.yuzu.yuzu_emu.model.Game |
| @@ -28,7 +28,7 @@ class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) : | |||
| 28 | parent: ViewGroup, | 28 | parent: ViewGroup, |
| 29 | viewType: Int | 29 | viewType: Int |
| 30 | ): AppletAdapter.AppletViewHolder { | 30 | ): AppletAdapter.AppletViewHolder { |
| 31 | CardAppletOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) | 31 | CardSimpleOutlinedBinding.inflate(LayoutInflater.from(parent.context), parent, false) |
| 32 | .apply { root.setOnClickListener(this@AppletAdapter) } | 32 | .apply { root.setOnClickListener(this@AppletAdapter) } |
| 33 | .also { return AppletViewHolder(it) } | 33 | .also { return AppletViewHolder(it) } |
| 34 | } | 34 | } |
| @@ -65,7 +65,7 @@ class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) : | |||
| 65 | view.findNavController().navigate(action) | 65 | view.findNavController().navigate(action) |
| 66 | } | 66 | } |
| 67 | 67 | ||
| 68 | inner class AppletViewHolder(val binding: CardAppletOptionBinding) : | 68 | inner class AppletViewHolder(val binding: CardSimpleOutlinedBinding) : |
| 69 | RecyclerView.ViewHolder(binding.root) { | 69 | RecyclerView.ViewHolder(binding.root) { |
| 70 | lateinit var applet: Applet | 70 | lateinit var applet: Applet |
| 71 | 71 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt index 0e818cab9..d290a656c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt | |||
| @@ -42,7 +42,7 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) : | |||
| 42 | if (driverViewModel.selectedDriver > position) { | 42 | if (driverViewModel.selectedDriver > position) { |
| 43 | driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1) | 43 | driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1) |
| 44 | } | 44 | } |
| 45 | if (GpuDriverHelper.customDriverData == driverData.second) { | 45 | if (GpuDriverHelper.customDriverSettingData == driverData.second) { |
| 46 | driverViewModel.setSelectedDriverIndex(0) | 46 | driverViewModel.setSelectedDriverIndex(0) |
| 47 | } | 47 | } |
| 48 | driverViewModel.driversToDelete.add(driverData.first) | 48 | driverViewModel.driversToDelete.add(driverData.first) |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt index 2ef638559..a578f0de8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt | |||
| @@ -44,19 +44,20 @@ import org.yuzu.yuzu_emu.utils.GameIconUtils | |||
| 44 | 44 | ||
| 45 | class GameAdapter(private val activity: AppCompatActivity) : | 45 | class GameAdapter(private val activity: AppCompatActivity) : |
| 46 | ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()), | 46 | ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()), |
| 47 | View.OnClickListener { | 47 | View.OnClickListener, |
| 48 | View.OnLongClickListener { | ||
| 48 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { | 49 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { |
| 49 | // Create a new view. | 50 | // Create a new view. |
| 50 | val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) | 51 | val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) |
| 51 | binding.cardGame.setOnClickListener(this) | 52 | binding.cardGame.setOnClickListener(this) |
| 53 | binding.cardGame.setOnLongClickListener(this) | ||
| 52 | 54 | ||
| 53 | // Use that view to create a ViewHolder. | 55 | // Use that view to create a ViewHolder. |
| 54 | return GameViewHolder(binding) | 56 | return GameViewHolder(binding) |
| 55 | } | 57 | } |
| 56 | 58 | ||
| 57 | override fun onBindViewHolder(holder: GameViewHolder, position: Int) { | 59 | override fun onBindViewHolder(holder: GameViewHolder, position: Int) = |
| 58 | holder.bind(currentList[position]) | 60 | holder.bind(currentList[position]) |
| 59 | } | ||
| 60 | 61 | ||
| 61 | override fun getItemCount(): Int = currentList.size | 62 | override fun getItemCount(): Int = currentList.size |
| 62 | 63 | ||
| @@ -125,8 +126,15 @@ class GameAdapter(private val activity: AppCompatActivity) : | |||
| 125 | } | 126 | } |
| 126 | } | 127 | } |
| 127 | 128 | ||
| 128 | val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game) | 129 | val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game, true) |
| 130 | view.findNavController().navigate(action) | ||
| 131 | } | ||
| 132 | |||
| 133 | override fun onLongClick(view: View): Boolean { | ||
| 134 | val holder = view.tag as GameViewHolder | ||
| 135 | val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(holder.game) | ||
| 129 | view.findNavController().navigate(action) | 136 | view.findNavController().navigate(action) |
| 137 | return true | ||
| 130 | } | 138 | } |
| 131 | 139 | ||
| 132 | inner class GameViewHolder(val binding: CardGameBinding) : | 140 | inner class GameViewHolder(val binding: CardGameBinding) : |
| @@ -157,7 +165,7 @@ class GameAdapter(private val activity: AppCompatActivity) : | |||
| 157 | 165 | ||
| 158 | private class DiffCallback : DiffUtil.ItemCallback<Game>() { | 166 | private class DiffCallback : DiffUtil.ItemCallback<Game>() { |
| 159 | override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean { | 167 | override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean { |
| 160 | return oldItem.programId == newItem.programId | 168 | return oldItem == newItem |
| 161 | } | 169 | } |
| 162 | 170 | ||
| 163 | override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean { | 171 | override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean { |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt new file mode 100644 index 000000000..95841d786 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt | |||
| @@ -0,0 +1,140 @@ | |||
| 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.text.TextUtils | ||
| 7 | import android.view.LayoutInflater | ||
| 8 | import android.view.View | ||
| 9 | import android.view.ViewGroup | ||
| 10 | import androidx.core.content.res.ResourcesCompat | ||
| 11 | import androidx.lifecycle.Lifecycle | ||
| 12 | import androidx.lifecycle.LifecycleOwner | ||
| 13 | import androidx.lifecycle.lifecycleScope | ||
| 14 | import androidx.lifecycle.repeatOnLifecycle | ||
| 15 | import androidx.recyclerview.widget.RecyclerView | ||
| 16 | import kotlinx.coroutines.launch | ||
| 17 | import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding | ||
| 18 | import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding | ||
| 19 | import org.yuzu.yuzu_emu.model.GameProperty | ||
| 20 | import org.yuzu.yuzu_emu.model.InstallableProperty | ||
| 21 | import org.yuzu.yuzu_emu.model.SubmenuProperty | ||
| 22 | |||
| 23 | class GamePropertiesAdapter( | ||
| 24 | private val viewLifecycle: LifecycleOwner, | ||
| 25 | private var properties: List<GameProperty> | ||
| 26 | ) : | ||
| 27 | RecyclerView.Adapter<GamePropertiesAdapter.GamePropertyViewHolder>() { | ||
| 28 | override fun onCreateViewHolder( | ||
| 29 | parent: ViewGroup, | ||
| 30 | viewType: Int | ||
| 31 | ): GamePropertyViewHolder { | ||
| 32 | val inflater = LayoutInflater.from(parent.context) | ||
| 33 | return when (viewType) { | ||
| 34 | PropertyType.Submenu.ordinal -> { | ||
| 35 | SubmenuPropertyViewHolder( | ||
| 36 | CardSimpleOutlinedBinding.inflate( | ||
| 37 | inflater, | ||
| 38 | parent, | ||
| 39 | false | ||
| 40 | ) | ||
| 41 | ) | ||
| 42 | } | ||
| 43 | |||
| 44 | else -> InstallablePropertyViewHolder( | ||
| 45 | CardInstallableIconBinding.inflate( | ||
| 46 | inflater, | ||
| 47 | parent, | ||
| 48 | false | ||
| 49 | ) | ||
| 50 | ) | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | override fun getItemCount(): Int = properties.size | ||
| 55 | |||
| 56 | override fun onBindViewHolder(holder: GamePropertyViewHolder, position: Int) = | ||
| 57 | holder.bind(properties[position]) | ||
| 58 | |||
| 59 | override fun getItemViewType(position: Int): Int { | ||
| 60 | return when (properties[position]) { | ||
| 61 | is SubmenuProperty -> PropertyType.Submenu.ordinal | ||
| 62 | else -> PropertyType.Installable.ordinal | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 66 | sealed class GamePropertyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { | ||
| 67 | abstract fun bind(property: GameProperty) | ||
| 68 | } | ||
| 69 | |||
| 70 | inner class SubmenuPropertyViewHolder(val binding: CardSimpleOutlinedBinding) : | ||
| 71 | GamePropertyViewHolder(binding.root) { | ||
| 72 | override fun bind(property: GameProperty) { | ||
| 73 | val submenuProperty = property as SubmenuProperty | ||
| 74 | |||
| 75 | binding.root.setOnClickListener { | ||
| 76 | submenuProperty.action.invoke() | ||
| 77 | } | ||
| 78 | |||
| 79 | binding.title.setText(submenuProperty.titleId) | ||
| 80 | binding.description.setText(submenuProperty.descriptionId) | ||
| 81 | binding.icon.setImageDrawable( | ||
| 82 | ResourcesCompat.getDrawable( | ||
| 83 | binding.icon.context.resources, | ||
| 84 | submenuProperty.iconId, | ||
| 85 | binding.icon.context.theme | ||
| 86 | ) | ||
| 87 | ) | ||
| 88 | |||
| 89 | binding.details.postDelayed({ | ||
| 90 | binding.details.isSelected = true | ||
| 91 | binding.details.ellipsize = TextUtils.TruncateAt.MARQUEE | ||
| 92 | }, 3000) | ||
| 93 | |||
| 94 | if (submenuProperty.details != null) { | ||
| 95 | binding.details.visibility = View.VISIBLE | ||
| 96 | binding.details.text = submenuProperty.details.invoke() | ||
| 97 | } else if (submenuProperty.detailsFlow != null) { | ||
| 98 | binding.details.visibility = View.VISIBLE | ||
| 99 | viewLifecycle.lifecycleScope.launch { | ||
| 100 | viewLifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| 101 | submenuProperty.detailsFlow.collect { binding.details.text = it } | ||
| 102 | } | ||
| 103 | } | ||
| 104 | } else { | ||
| 105 | binding.details.visibility = View.GONE | ||
| 106 | } | ||
| 107 | } | ||
| 108 | } | ||
| 109 | |||
| 110 | inner class InstallablePropertyViewHolder(val binding: CardInstallableIconBinding) : | ||
| 111 | GamePropertyViewHolder(binding.root) { | ||
| 112 | override fun bind(property: GameProperty) { | ||
| 113 | val installableProperty = property as InstallableProperty | ||
| 114 | |||
| 115 | binding.title.setText(installableProperty.titleId) | ||
| 116 | binding.description.setText(installableProperty.descriptionId) | ||
| 117 | binding.icon.setImageDrawable( | ||
| 118 | ResourcesCompat.getDrawable( | ||
| 119 | binding.icon.context.resources, | ||
| 120 | installableProperty.iconId, | ||
| 121 | binding.icon.context.theme | ||
| 122 | ) | ||
| 123 | ) | ||
| 124 | |||
| 125 | if (installableProperty.install != null) { | ||
| 126 | binding.buttonInstall.visibility = View.VISIBLE | ||
| 127 | binding.buttonInstall.setOnClickListener { installableProperty.install.invoke() } | ||
| 128 | } | ||
| 129 | if (installableProperty.export != null) { | ||
| 130 | binding.buttonExport.visibility = View.VISIBLE | ||
| 131 | binding.buttonExport.setOnClickListener { installableProperty.export.invoke() } | ||
| 132 | } | ||
| 133 | } | ||
| 134 | } | ||
| 135 | |||
| 136 | enum class PropertyType { | ||
| 137 | Submenu, | ||
| 138 | Installable | ||
| 139 | } | ||
| 140 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt index aeda8d222..0ba465356 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt | |||
| @@ -4,7 +4,6 @@ | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.model | 4 | package org.yuzu.yuzu_emu.features.settings.model |
| 5 | 5 | ||
| 6 | interface AbstractBooleanSetting : AbstractSetting { | 6 | interface AbstractBooleanSetting : AbstractSetting { |
| 7 | val boolean: Boolean | 7 | fun getBoolean(needsGlobal: Boolean = false): Boolean |
| 8 | |||
| 9 | fun setBoolean(value: Boolean) | 8 | fun setBoolean(value: Boolean) |
| 10 | } | 9 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt index 606519ad8..cf6300535 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt | |||
| @@ -4,7 +4,6 @@ | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.model | 4 | package org.yuzu.yuzu_emu.features.settings.model |
| 5 | 5 | ||
| 6 | interface AbstractByteSetting : AbstractSetting { | 6 | interface AbstractByteSetting : AbstractSetting { |
| 7 | val byte: Byte | 7 | fun getByte(needsGlobal: Boolean = false): Byte |
| 8 | |||
| 9 | fun setByte(value: Byte) | 8 | fun setByte(value: Byte) |
| 10 | } | 9 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt index 974925eed..c6c0bcf34 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt | |||
| @@ -4,7 +4,6 @@ | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.model | 4 | package org.yuzu.yuzu_emu.features.settings.model |
| 5 | 5 | ||
| 6 | interface AbstractFloatSetting : AbstractSetting { | 6 | interface AbstractFloatSetting : AbstractSetting { |
| 7 | val float: Float | 7 | fun getFloat(needsGlobal: Boolean = false): Float |
| 8 | |||
| 9 | fun setFloat(value: Float) | 8 | fun setFloat(value: Float) |
| 10 | } | 9 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt index 89b285b10..826402c34 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt | |||
| @@ -4,7 +4,6 @@ | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.model | 4 | package org.yuzu.yuzu_emu.features.settings.model |
| 5 | 5 | ||
| 6 | interface AbstractIntSetting : AbstractSetting { | 6 | interface AbstractIntSetting : AbstractSetting { |
| 7 | val int: Int | 7 | fun getInt(needsGlobal: Boolean = false): Int |
| 8 | |||
| 9 | fun setInt(value: Int) | 8 | fun setInt(value: Int) |
| 10 | } | 9 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt index 4873942db..2b62cc06b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt | |||
| @@ -4,7 +4,6 @@ | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.model | 4 | package org.yuzu.yuzu_emu.features.settings.model |
| 5 | 5 | ||
| 6 | interface AbstractLongSetting : AbstractSetting { | 6 | interface AbstractLongSetting : AbstractSetting { |
| 7 | val long: Long | 7 | fun getLong(needsGlobal: Boolean = false): Long |
| 8 | |||
| 9 | fun setLong(value: Long) | 8 | fun setLong(value: Long) |
| 10 | } | 9 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt index 8b6d29fe5..3b78c7cf0 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt | |||
| @@ -7,12 +7,7 @@ import org.yuzu.yuzu_emu.utils.NativeConfig | |||
| 7 | 7 | ||
| 8 | interface AbstractSetting { | 8 | interface AbstractSetting { |
| 9 | val key: String | 9 | val key: String |
| 10 | val category: Settings.Category | ||
| 11 | val defaultValue: Any | 10 | val defaultValue: Any |
| 12 | val androidDefault: Any? | ||
| 13 | get() = null | ||
| 14 | val valueAsString: String | ||
| 15 | get() = "" | ||
| 16 | 11 | ||
| 17 | val isRuntimeModifiable: Boolean | 12 | val isRuntimeModifiable: Boolean |
| 18 | get() = NativeConfig.getIsRuntimeModifiable(key) | 13 | get() = NativeConfig.getIsRuntimeModifiable(key) |
| @@ -20,5 +15,17 @@ interface AbstractSetting { | |||
| 20 | val pairedSettingKey: String | 15 | val pairedSettingKey: String |
| 21 | get() = NativeConfig.getPairedSettingKey(key) | 16 | get() = NativeConfig.getPairedSettingKey(key) |
| 22 | 17 | ||
| 18 | val isSwitchable: Boolean | ||
| 19 | get() = NativeConfig.getIsSwitchable(key) | ||
| 20 | |||
| 21 | var global: Boolean | ||
| 22 | get() = NativeConfig.usingGlobal(key) | ||
| 23 | set(value) = NativeConfig.setGlobal(key, value) | ||
| 24 | |||
| 25 | val isSaveable: Boolean | ||
| 26 | get() = NativeConfig.getIsSaveable(key) | ||
| 27 | |||
| 28 | fun getValueAsString(needsGlobal: Boolean = false): String | ||
| 29 | |||
| 23 | fun reset() | 30 | fun reset() |
| 24 | } | 31 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt index 91407ccbb..8bfa81e4a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt | |||
| @@ -4,7 +4,6 @@ | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.model | 4 | package org.yuzu.yuzu_emu.features.settings.model |
| 5 | 5 | ||
| 6 | interface AbstractShortSetting : AbstractSetting { | 6 | interface AbstractShortSetting : AbstractSetting { |
| 7 | val short: Short | 7 | fun getShort(needsGlobal: Boolean = false): Short |
| 8 | |||
| 9 | fun setShort(value: Short) | 8 | fun setShort(value: Short) |
| 10 | } | 9 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt index c8935cc48..6ff8fd3f9 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt | |||
| @@ -4,7 +4,6 @@ | |||
| 4 | package org.yuzu.yuzu_emu.features.settings.model | 4 | package org.yuzu.yuzu_emu.features.settings.model |
| 5 | 5 | ||
| 6 | interface AbstractStringSetting : AbstractSetting { | 6 | interface AbstractStringSetting : AbstractSetting { |
| 7 | val string: String | 7 | fun getString(needsGlobal: Boolean = false): String |
| 8 | |||
| 9 | fun setString(value: String) | 8 | fun setString(value: String) |
| 10 | } | 9 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt index 8476ce867..16f06cd0a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt | |||
| @@ -5,36 +5,34 @@ package org.yuzu.yuzu_emu.features.settings.model | |||
| 5 | 5 | ||
| 6 | import org.yuzu.yuzu_emu.utils.NativeConfig | 6 | import org.yuzu.yuzu_emu.utils.NativeConfig |
| 7 | 7 | ||
| 8 | enum class BooleanSetting( | 8 | enum class BooleanSetting(override val key: String) : AbstractBooleanSetting { |
| 9 | override val key: String, | 9 | AUDIO_MUTED("audio_muted"), |
| 10 | override val category: Settings.Category, | 10 | CPU_DEBUG_MODE("cpu_debug_mode"), |
| 11 | override val androidDefault: Boolean? = null | 11 | FASTMEM("cpuopt_fastmem"), |
| 12 | ) : AbstractBooleanSetting { | 12 | FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives"), |
| 13 | AUDIO_MUTED("audio_muted", Settings.Category.Audio), | 13 | RENDERER_USE_SPEED_LIMIT("use_speed_limit"), |
| 14 | CPU_DEBUG_MODE("cpu_debug_mode", Settings.Category.Cpu), | 14 | USE_DOCKED_MODE("use_docked_mode"), |
| 15 | FASTMEM("cpuopt_fastmem", Settings.Category.Cpu), | 15 | RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache"), |
| 16 | FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives", Settings.Category.Cpu), | 16 | RENDERER_FORCE_MAX_CLOCK("force_max_clock"), |
| 17 | RENDERER_USE_SPEED_LIMIT("use_speed_limit", Settings.Category.Core), | 17 | RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"), |
| 18 | USE_DOCKED_MODE("use_docked_mode", Settings.Category.System, false), | 18 | RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"), |
| 19 | RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache", Settings.Category.Renderer), | 19 | RENDERER_DEBUG("debug"), |
| 20 | RENDERER_FORCE_MAX_CLOCK("force_max_clock", Settings.Category.Renderer), | 20 | PICTURE_IN_PICTURE("picture_in_picture"), |
| 21 | RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders", Settings.Category.Renderer), | 21 | USE_CUSTOM_RTC("custom_rtc_enabled"); |
| 22 | RENDERER_REACTIVE_FLUSHING("use_reactive_flushing", Settings.Category.Renderer, false), | 22 | |
| 23 | RENDERER_DEBUG("debug", Settings.Category.Renderer), | 23 | override fun getBoolean(needsGlobal: Boolean): Boolean = |
| 24 | PICTURE_IN_PICTURE("picture_in_picture", Settings.Category.Android), | 24 | NativeConfig.getBoolean(key, needsGlobal) |
| 25 | USE_CUSTOM_RTC("custom_rtc_enabled", Settings.Category.System); | 25 | |
| 26 | 26 | override fun setBoolean(value: Boolean) { | |
| 27 | override val boolean: Boolean | 27 | if (NativeConfig.isPerGameConfigLoaded()) { |
| 28 | get() = NativeConfig.getBoolean(key, false) | 28 | global = false |
| 29 | 29 | } | |
| 30 | override fun setBoolean(value: Boolean) = NativeConfig.setBoolean(key, value) | 30 | NativeConfig.setBoolean(key, value) |
| 31 | |||
| 32 | override val defaultValue: Boolean by lazy { | ||
| 33 | androidDefault ?: NativeConfig.getBoolean(key, true) | ||
| 34 | } | 31 | } |
| 35 | 32 | ||
| 36 | override val valueAsString: String | 33 | override val defaultValue: Boolean by lazy { NativeConfig.getDefaultToString(key).toBoolean() } |
| 37 | get() = if (boolean) "1" else "0" | 34 | |
| 35 | override fun getValueAsString(needsGlobal: Boolean): String = getBoolean(needsGlobal).toString() | ||
| 38 | 36 | ||
| 39 | override fun reset() = NativeConfig.setBoolean(key, defaultValue) | 37 | override fun reset() = NativeConfig.setBoolean(key, defaultValue) |
| 40 | } | 38 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt index 6ec0a765e..7b7fac211 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt | |||
| @@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model | |||
| 5 | 5 | ||
| 6 | import org.yuzu.yuzu_emu.utils.NativeConfig | 6 | import org.yuzu.yuzu_emu.utils.NativeConfig |
| 7 | 7 | ||
| 8 | enum class ByteSetting( | 8 | enum class ByteSetting(override val key: String) : AbstractByteSetting { |
| 9 | override val key: String, | 9 | AUDIO_VOLUME("volume"); |
| 10 | override val category: Settings.Category | ||
| 11 | ) : AbstractByteSetting { | ||
| 12 | AUDIO_VOLUME("volume", Settings.Category.Audio); | ||
| 13 | 10 | ||
| 14 | override val byte: Byte | 11 | override fun getByte(needsGlobal: Boolean): Byte = NativeConfig.getByte(key, needsGlobal) |
| 15 | get() = NativeConfig.getByte(key, false) | ||
| 16 | 12 | ||
| 17 | override fun setByte(value: Byte) = NativeConfig.setByte(key, value) | 13 | override fun setByte(value: Byte) { |
| 14 | if (NativeConfig.isPerGameConfigLoaded()) { | ||
| 15 | global = false | ||
| 16 | } | ||
| 17 | NativeConfig.setByte(key, value) | ||
| 18 | } | ||
| 18 | 19 | ||
| 19 | override val defaultValue: Byte by lazy { NativeConfig.getByte(key, true) } | 20 | override val defaultValue: Byte by lazy { NativeConfig.getDefaultToString(key).toByte() } |
| 20 | 21 | ||
| 21 | override val valueAsString: String | 22 | override fun getValueAsString(needsGlobal: Boolean): String = getByte(needsGlobal).toString() |
| 22 | get() = byte.toString() | ||
| 23 | 23 | ||
| 24 | override fun reset() = NativeConfig.setByte(key, defaultValue) | 24 | override fun reset() = NativeConfig.setByte(key, defaultValue) |
| 25 | } | 25 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt index 0181d06f2..4644824d8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt | |||
| @@ -5,22 +5,22 @@ package org.yuzu.yuzu_emu.features.settings.model | |||
| 5 | 5 | ||
| 6 | import org.yuzu.yuzu_emu.utils.NativeConfig | 6 | import org.yuzu.yuzu_emu.utils.NativeConfig |
| 7 | 7 | ||
| 8 | enum class FloatSetting( | 8 | enum class FloatSetting(override val key: String) : AbstractFloatSetting { |
| 9 | override val key: String, | ||
| 10 | override val category: Settings.Category | ||
| 11 | ) : AbstractFloatSetting { | ||
| 12 | // No float settings currently exist | 9 | // No float settings currently exist |
| 13 | EMPTY_SETTING("", Settings.Category.UiGeneral); | 10 | EMPTY_SETTING(""); |
| 14 | 11 | ||
| 15 | override val float: Float | 12 | override fun getFloat(needsGlobal: Boolean): Float = NativeConfig.getFloat(key, false) |
| 16 | get() = NativeConfig.getFloat(key, false) | ||
| 17 | 13 | ||
| 18 | override fun setFloat(value: Float) = NativeConfig.setFloat(key, value) | 14 | override fun setFloat(value: Float) { |
| 15 | if (NativeConfig.isPerGameConfigLoaded()) { | ||
| 16 | global = false | ||
| 17 | } | ||
| 18 | NativeConfig.setFloat(key, value) | ||
| 19 | } | ||
| 19 | 20 | ||
| 20 | override val defaultValue: Float by lazy { NativeConfig.getFloat(key, true) } | 21 | override val defaultValue: Float by lazy { NativeConfig.getDefaultToString(key).toFloat() } |
| 21 | 22 | ||
| 22 | override val valueAsString: String | 23 | override fun getValueAsString(needsGlobal: Boolean): String = getFloat(needsGlobal).toString() |
| 23 | get() = float.toString() | ||
| 24 | 24 | ||
| 25 | override fun reset() = NativeConfig.setFloat(key, defaultValue) | 25 | override fun reset() = NativeConfig.setFloat(key, defaultValue) |
| 26 | } | 26 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt index ef10b209f..21e4e1afd 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt | |||
| @@ -5,36 +5,33 @@ package org.yuzu.yuzu_emu.features.settings.model | |||
| 5 | 5 | ||
| 6 | import org.yuzu.yuzu_emu.utils.NativeConfig | 6 | import org.yuzu.yuzu_emu.utils.NativeConfig |
| 7 | 7 | ||
| 8 | enum class IntSetting( | 8 | enum class IntSetting(override val key: String) : AbstractIntSetting { |
| 9 | override val key: String, | 9 | CPU_BACKEND("cpu_backend"), |
| 10 | override val category: Settings.Category, | 10 | CPU_ACCURACY("cpu_accuracy"), |
| 11 | override val androidDefault: Int? = null | 11 | REGION_INDEX("region_index"), |
| 12 | ) : AbstractIntSetting { | 12 | LANGUAGE_INDEX("language_index"), |
| 13 | CPU_BACKEND("cpu_backend", Settings.Category.Cpu), | 13 | RENDERER_BACKEND("backend"), |
| 14 | CPU_ACCURACY("cpu_accuracy", Settings.Category.Cpu), | 14 | RENDERER_ACCURACY("gpu_accuracy"), |
| 15 | REGION_INDEX("region_index", Settings.Category.System), | 15 | RENDERER_RESOLUTION("resolution_setup"), |
| 16 | LANGUAGE_INDEX("language_index", Settings.Category.System), | 16 | RENDERER_VSYNC("use_vsync"), |
| 17 | RENDERER_BACKEND("backend", Settings.Category.Renderer), | 17 | RENDERER_SCALING_FILTER("scaling_filter"), |
| 18 | RENDERER_ACCURACY("gpu_accuracy", Settings.Category.Renderer, 0), | 18 | RENDERER_ANTI_ALIASING("anti_aliasing"), |
| 19 | RENDERER_RESOLUTION("resolution_setup", Settings.Category.Renderer), | 19 | RENDERER_SCREEN_LAYOUT("screen_layout"), |
| 20 | RENDERER_VSYNC("use_vsync", Settings.Category.Renderer), | 20 | RENDERER_ASPECT_RATIO("aspect_ratio"), |
| 21 | RENDERER_SCALING_FILTER("scaling_filter", Settings.Category.Renderer), | 21 | AUDIO_OUTPUT_ENGINE("output_engine"); |
| 22 | RENDERER_ANTI_ALIASING("anti_aliasing", Settings.Category.Renderer), | 22 | |
| 23 | RENDERER_SCREEN_LAYOUT("screen_layout", Settings.Category.Android), | 23 | override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal) |
| 24 | RENDERER_ASPECT_RATIO("aspect_ratio", Settings.Category.Renderer), | 24 | |
| 25 | AUDIO_OUTPUT_ENGINE("output_engine", Settings.Category.Audio); | 25 | override fun setInt(value: Int) { |
| 26 | 26 | if (NativeConfig.isPerGameConfigLoaded()) { | |
| 27 | override val int: Int | 27 | global = false |
| 28 | get() = NativeConfig.getInt(key, false) | 28 | } |
| 29 | 29 | NativeConfig.setInt(key, value) | |
| 30 | override fun setInt(value: Int) = NativeConfig.setInt(key, value) | ||
| 31 | |||
| 32 | override val defaultValue: Int by lazy { | ||
| 33 | androidDefault ?: NativeConfig.getInt(key, true) | ||
| 34 | } | 30 | } |
| 35 | 31 | ||
| 36 | override val valueAsString: String | 32 | override val defaultValue: Int by lazy { NativeConfig.getDefaultToString(key).toInt() } |
| 37 | get() = int.toString() | 33 | |
| 34 | override fun getValueAsString(needsGlobal: Boolean): String = getInt(needsGlobal).toString() | ||
| 38 | 35 | ||
| 39 | override fun reset() = NativeConfig.setInt(key, defaultValue) | 36 | override fun reset() = NativeConfig.setInt(key, defaultValue) |
| 40 | } | 37 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt index c526fc4cf..e3efd516c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt | |||
| @@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model | |||
| 5 | 5 | ||
| 6 | import org.yuzu.yuzu_emu.utils.NativeConfig | 6 | import org.yuzu.yuzu_emu.utils.NativeConfig |
| 7 | 7 | ||
| 8 | enum class LongSetting( | 8 | enum class LongSetting(override val key: String) : AbstractLongSetting { |
| 9 | override val key: String, | 9 | CUSTOM_RTC("custom_rtc"); |
| 10 | override val category: Settings.Category | ||
| 11 | ) : AbstractLongSetting { | ||
| 12 | CUSTOM_RTC("custom_rtc", Settings.Category.System); | ||
| 13 | 10 | ||
| 14 | override val long: Long | 11 | override fun getLong(needsGlobal: Boolean): Long = NativeConfig.getLong(key, needsGlobal) |
| 15 | get() = NativeConfig.getLong(key, false) | ||
| 16 | 12 | ||
| 17 | override fun setLong(value: Long) = NativeConfig.setLong(key, value) | 13 | override fun setLong(value: Long) { |
| 14 | if (NativeConfig.isPerGameConfigLoaded()) { | ||
| 15 | global = false | ||
| 16 | } | ||
| 17 | NativeConfig.setLong(key, value) | ||
| 18 | } | ||
| 18 | 19 | ||
| 19 | override val defaultValue: Long by lazy { NativeConfig.getLong(key, true) } | 20 | override val defaultValue: Long by lazy { NativeConfig.getDefaultToString(key).toLong() } |
| 20 | 21 | ||
| 21 | override val valueAsString: String | 22 | override fun getValueAsString(needsGlobal: Boolean): String = getLong(needsGlobal).toString() |
| 22 | get() = long.toString() | ||
| 23 | 23 | ||
| 24 | override fun reset() = NativeConfig.setLong(key, defaultValue) | 24 | override fun reset() = NativeConfig.setLong(key, defaultValue) |
| 25 | } | 25 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt index e3cd66185..9551fc05e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt | |||
| @@ -6,62 +6,11 @@ package org.yuzu.yuzu_emu.features.settings.model | |||
| 6 | import org.yuzu.yuzu_emu.R | 6 | import org.yuzu.yuzu_emu.R |
| 7 | 7 | ||
| 8 | object Settings { | 8 | object Settings { |
| 9 | enum class Category { | ||
| 10 | Android, | ||
| 11 | Audio, | ||
| 12 | Core, | ||
| 13 | Cpu, | ||
| 14 | CpuDebug, | ||
| 15 | CpuUnsafe, | ||
| 16 | Renderer, | ||
| 17 | RendererAdvanced, | ||
| 18 | RendererDebug, | ||
| 19 | System, | ||
| 20 | SystemAudio, | ||
| 21 | DataStorage, | ||
| 22 | Debugging, | ||
| 23 | DebuggingGraphics, | ||
| 24 | Miscellaneous, | ||
| 25 | Network, | ||
| 26 | WebService, | ||
| 27 | AddOns, | ||
| 28 | Controls, | ||
| 29 | Ui, | ||
| 30 | UiGeneral, | ||
| 31 | UiLayout, | ||
| 32 | UiGameList, | ||
| 33 | Screenshots, | ||
| 34 | Shortcuts, | ||
| 35 | Multiplayer, | ||
| 36 | Services, | ||
| 37 | Paths, | ||
| 38 | MaxEnum | ||
| 39 | } | ||
| 40 | |||
| 41 | val settingsList = listOf<AbstractSetting>( | ||
| 42 | *BooleanSetting.values(), | ||
| 43 | *ByteSetting.values(), | ||
| 44 | *ShortSetting.values(), | ||
| 45 | *IntSetting.values(), | ||
| 46 | *FloatSetting.values(), | ||
| 47 | *LongSetting.values(), | ||
| 48 | *StringSetting.values() | ||
| 49 | ) | ||
| 50 | |||
| 51 | const val SECTION_GENERAL = "General" | ||
| 52 | const val SECTION_SYSTEM = "System" | ||
| 53 | const val SECTION_RENDERER = "Renderer" | ||
| 54 | const val SECTION_AUDIO = "Audio" | ||
| 55 | const val SECTION_CPU = "Cpu" | ||
| 56 | const val SECTION_THEME = "Theme" | ||
| 57 | const val SECTION_DEBUG = "Debug" | ||
| 58 | |||
| 59 | enum class MenuTag(val titleId: Int) { | 9 | enum class MenuTag(val titleId: Int) { |
| 60 | SECTION_ROOT(R.string.advanced_settings), | 10 | SECTION_ROOT(R.string.advanced_settings), |
| 61 | SECTION_SYSTEM(R.string.preferences_system), | 11 | SECTION_SYSTEM(R.string.preferences_system), |
| 62 | SECTION_RENDERER(R.string.preferences_graphics), | 12 | SECTION_RENDERER(R.string.preferences_graphics), |
| 63 | SECTION_AUDIO(R.string.preferences_audio), | 13 | SECTION_AUDIO(R.string.preferences_audio), |
| 64 | SECTION_CPU(R.string.cpu), | ||
| 65 | SECTION_THEME(R.string.preferences_theme), | 14 | SECTION_THEME(R.string.preferences_theme), |
| 66 | SECTION_DEBUG(R.string.preferences_debug); | 15 | SECTION_DEBUG(R.string.preferences_debug); |
| 67 | } | 16 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt index c9a0c664c..16eb4ffdd 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt | |||
| @@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model | |||
| 5 | 5 | ||
| 6 | import org.yuzu.yuzu_emu.utils.NativeConfig | 6 | import org.yuzu.yuzu_emu.utils.NativeConfig |
| 7 | 7 | ||
| 8 | enum class ShortSetting( | 8 | enum class ShortSetting(override val key: String) : AbstractShortSetting { |
| 9 | override val key: String, | 9 | RENDERER_SPEED_LIMIT("speed_limit"); |
| 10 | override val category: Settings.Category | ||
| 11 | ) : AbstractShortSetting { | ||
| 12 | RENDERER_SPEED_LIMIT("speed_limit", Settings.Category.Core); | ||
| 13 | 10 | ||
| 14 | override val short: Short | 11 | override fun getShort(needsGlobal: Boolean): Short = NativeConfig.getShort(key, needsGlobal) |
| 15 | get() = NativeConfig.getShort(key, false) | ||
| 16 | 12 | ||
| 17 | override fun setShort(value: Short) = NativeConfig.setShort(key, value) | 13 | override fun setShort(value: Short) { |
| 14 | if (NativeConfig.isPerGameConfigLoaded()) { | ||
| 15 | global = false | ||
| 16 | } | ||
| 17 | NativeConfig.setShort(key, value) | ||
| 18 | } | ||
| 18 | 19 | ||
| 19 | override val defaultValue: Short by lazy { NativeConfig.getShort(key, true) } | 20 | override val defaultValue: Short by lazy { NativeConfig.getDefaultToString(key).toShort() } |
| 20 | 21 | ||
| 21 | override val valueAsString: String | 22 | override fun getValueAsString(needsGlobal: Boolean): String = getShort(needsGlobal).toString() |
| 22 | get() = short.toString() | ||
| 23 | 23 | ||
| 24 | override fun reset() = NativeConfig.setShort(key, defaultValue) | 24 | override fun reset() = NativeConfig.setShort(key, defaultValue) |
| 25 | } | 25 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt index 9bb3e66d4..a0d8cfede 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt | |||
| @@ -5,22 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model | |||
| 5 | 5 | ||
| 6 | import org.yuzu.yuzu_emu.utils.NativeConfig | 6 | import org.yuzu.yuzu_emu.utils.NativeConfig |
| 7 | 7 | ||
| 8 | enum class StringSetting( | 8 | enum class StringSetting(override val key: String) : AbstractStringSetting { |
| 9 | override val key: String, | 9 | DRIVER_PATH("driver_path"); |
| 10 | override val category: Settings.Category | ||
| 11 | ) : AbstractStringSetting { | ||
| 12 | // No string settings currently exist | ||
| 13 | EMPTY_SETTING("", Settings.Category.UiGeneral); | ||
| 14 | 10 | ||
| 15 | override val string: String | 11 | override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal) |
| 16 | get() = NativeConfig.getString(key, false) | ||
| 17 | 12 | ||
| 18 | override fun setString(value: String) = NativeConfig.setString(key, value) | 13 | override fun setString(value: String) { |
| 14 | if (NativeConfig.isPerGameConfigLoaded()) { | ||
| 15 | global = false | ||
| 16 | } | ||
| 17 | NativeConfig.setString(key, value) | ||
| 18 | } | ||
| 19 | 19 | ||
| 20 | override val defaultValue: String by lazy { NativeConfig.getString(key, true) } | 20 | override val defaultValue: String by lazy { NativeConfig.getDefaultToString(key) } |
| 21 | 21 | ||
| 22 | override val valueAsString: String | 22 | override fun getValueAsString(needsGlobal: Boolean): String = getString(needsGlobal) |
| 23 | get() = string | ||
| 24 | 23 | ||
| 25 | override fun reset() = NativeConfig.setString(key, defaultValue) | 24 | override fun reset() = NativeConfig.setString(key, defaultValue) |
| 26 | } | 25 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt index 8bc164197..1d81f5f2b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt | |||
| @@ -12,7 +12,6 @@ class DateTimeSetting( | |||
| 12 | ) : SettingsItem(longSetting, titleId, descriptionId) { | 12 | ) : SettingsItem(longSetting, titleId, descriptionId) { |
| 13 | override val type = TYPE_DATETIME_SETTING | 13 | override val type = TYPE_DATETIME_SETTING |
| 14 | 14 | ||
| 15 | var value: Long | 15 | fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal) |
| 16 | get() = longSetting.long | 16 | fun setValue(value: Long) = (setting as AbstractLongSetting).setLong(value) |
| 17 | set(value) = (setting as AbstractLongSetting).setLong(value) | ||
| 18 | } | 17 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt index e198b18a0..2e97aee2c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt | |||
| @@ -11,8 +11,8 @@ import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting | |||
| 11 | import org.yuzu.yuzu_emu.features.settings.model.ByteSetting | 11 | import org.yuzu.yuzu_emu.features.settings.model.ByteSetting |
| 12 | import org.yuzu.yuzu_emu.features.settings.model.IntSetting | 12 | import org.yuzu.yuzu_emu.features.settings.model.IntSetting |
| 13 | import org.yuzu.yuzu_emu.features.settings.model.LongSetting | 13 | import org.yuzu.yuzu_emu.features.settings.model.LongSetting |
| 14 | import org.yuzu.yuzu_emu.features.settings.model.Settings | ||
| 15 | import org.yuzu.yuzu_emu.features.settings.model.ShortSetting | 14 | import org.yuzu.yuzu_emu.features.settings.model.ShortSetting |
| 15 | import org.yuzu.yuzu_emu.utils.NativeConfig | ||
| 16 | 16 | ||
| 17 | /** | 17 | /** |
| 18 | * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments. | 18 | * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments. |
| @@ -30,10 +30,26 @@ abstract class SettingsItem( | |||
| 30 | 30 | ||
| 31 | val isEditable: Boolean | 31 | val isEditable: Boolean |
| 32 | get() { | 32 | get() { |
| 33 | // Can't edit settings that aren't saveable in per-game config even if they are switchable | ||
| 34 | if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) { | ||
| 35 | return false | ||
| 36 | } | ||
| 37 | |||
| 33 | if (!NativeLibrary.isRunning()) return true | 38 | if (!NativeLibrary.isRunning()) return true |
| 39 | |||
| 40 | // Prevent editing settings that were modified in per-game config while editing global | ||
| 41 | // config | ||
| 42 | if (!NativeConfig.isPerGameConfigLoaded() && !setting.global) { | ||
| 43 | return false | ||
| 44 | } | ||
| 45 | |||
| 34 | return setting.isRuntimeModifiable | 46 | return setting.isRuntimeModifiable |
| 35 | } | 47 | } |
| 36 | 48 | ||
| 49 | val needsRuntimeGlobal: Boolean | ||
| 50 | get() = NativeLibrary.isRunning() && !setting.global && | ||
| 51 | !NativeConfig.isPerGameConfigLoaded() | ||
| 52 | |||
| 37 | companion object { | 53 | companion object { |
| 38 | const val TYPE_HEADER = 0 | 54 | const val TYPE_HEADER = 0 |
| 39 | const val TYPE_SWITCH = 1 | 55 | const val TYPE_SWITCH = 1 |
| @@ -48,8 +64,9 @@ abstract class SettingsItem( | |||
| 48 | 64 | ||
| 49 | val emptySetting = object : AbstractSetting { | 65 | val emptySetting = object : AbstractSetting { |
| 50 | override val key: String = "" | 66 | override val key: String = "" |
| 51 | override val category: Settings.Category = Settings.Category.Ui | ||
| 52 | override val defaultValue: Any = false | 67 | override val defaultValue: Any = false |
| 68 | override val isSaveable = true | ||
| 69 | override fun getValueAsString(needsGlobal: Boolean): String = "" | ||
| 53 | override fun reset() {} | 70 | override fun reset() {} |
| 54 | } | 71 | } |
| 55 | 72 | ||
| @@ -270,9 +287,9 @@ abstract class SettingsItem( | |||
| 270 | ) | 287 | ) |
| 271 | 288 | ||
| 272 | val fastmem = object : AbstractBooleanSetting { | 289 | val fastmem = object : AbstractBooleanSetting { |
| 273 | override val boolean: Boolean | 290 | override fun getBoolean(needsGlobal: Boolean): Boolean = |
| 274 | get() = | 291 | BooleanSetting.FASTMEM.getBoolean() && |
| 275 | BooleanSetting.FASTMEM.boolean && BooleanSetting.FASTMEM_EXCLUSIVES.boolean | 292 | BooleanSetting.FASTMEM_EXCLUSIVES.getBoolean() |
| 276 | 293 | ||
| 277 | override fun setBoolean(value: Boolean) { | 294 | override fun setBoolean(value: Boolean) { |
| 278 | BooleanSetting.FASTMEM.setBoolean(value) | 295 | BooleanSetting.FASTMEM.setBoolean(value) |
| @@ -280,9 +297,24 @@ abstract class SettingsItem( | |||
| 280 | } | 297 | } |
| 281 | 298 | ||
| 282 | override val key: String = FASTMEM_COMBINED | 299 | override val key: String = FASTMEM_COMBINED |
| 283 | override val category = Settings.Category.Cpu | ||
| 284 | override val isRuntimeModifiable: Boolean = false | 300 | override val isRuntimeModifiable: Boolean = false |
| 285 | override val defaultValue: Boolean = true | 301 | override val defaultValue: Boolean = true |
| 302 | override val isSwitchable: Boolean = true | ||
| 303 | override var global: Boolean | ||
| 304 | get() { | ||
| 305 | return BooleanSetting.FASTMEM.global && | ||
| 306 | BooleanSetting.FASTMEM_EXCLUSIVES.global | ||
| 307 | } | ||
| 308 | set(value) { | ||
| 309 | BooleanSetting.FASTMEM.global = value | ||
| 310 | BooleanSetting.FASTMEM_EXCLUSIVES.global = value | ||
| 311 | } | ||
| 312 | |||
| 313 | override val isSaveable = true | ||
| 314 | |||
| 315 | override fun getValueAsString(needsGlobal: Boolean): String = | ||
| 316 | getBoolean().toString() | ||
| 317 | |||
| 286 | override fun reset() = setBoolean(defaultValue) | 318 | override fun reset() = setBoolean(defaultValue) |
| 287 | } | 319 | } |
| 288 | put(SwitchSetting(fastmem, R.string.fastmem, 0)) | 320 | put(SwitchSetting(fastmem, R.string.fastmem, 0)) |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt index 705527a73..97a5a9e59 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt | |||
| @@ -15,16 +15,11 @@ class SingleChoiceSetting( | |||
| 15 | ) : SettingsItem(setting, titleId, descriptionId) { | 15 | ) : SettingsItem(setting, titleId, descriptionId) { |
| 16 | override val type = TYPE_SINGLE_CHOICE | 16 | override val type = TYPE_SINGLE_CHOICE |
| 17 | 17 | ||
| 18 | var selectedValue: Int | 18 | fun getSelectedValue(needsGlobal: Boolean = false) = |
| 19 | get() { | 19 | when (setting) { |
| 20 | return when (setting) { | 20 | is AbstractIntSetting -> setting.getInt(needsGlobal) |
| 21 | is AbstractIntSetting -> setting.int | 21 | else -> -1 |
| 22 | else -> -1 | ||
| 23 | } | ||
| 24 | } | ||
| 25 | set(value) { | ||
| 26 | when (setting) { | ||
| 27 | is AbstractIntSetting -> setting.setInt(value) | ||
| 28 | } | ||
| 29 | } | 22 | } |
| 23 | |||
| 24 | fun setSelectedValue(value: Int) = (setting as AbstractIntSetting).setInt(value) | ||
| 30 | } | 25 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt index c3b5df02c..b9b709bf7 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt | |||
| @@ -20,22 +20,20 @@ class SliderSetting( | |||
| 20 | ) : SettingsItem(setting, titleId, descriptionId) { | 20 | ) : SettingsItem(setting, titleId, descriptionId) { |
| 21 | override val type = TYPE_SLIDER | 21 | override val type = TYPE_SLIDER |
| 22 | 22 | ||
| 23 | var selectedValue: Int | 23 | fun getSelectedValue(needsGlobal: Boolean = false) = |
| 24 | get() { | 24 | when (setting) { |
| 25 | return when (setting) { | 25 | is AbstractByteSetting -> setting.getByte(needsGlobal).toInt() |
| 26 | is AbstractByteSetting -> setting.byte.toInt() | 26 | is AbstractShortSetting -> setting.getShort(needsGlobal).toInt() |
| 27 | is AbstractShortSetting -> setting.short.toInt() | 27 | is AbstractIntSetting -> setting.getInt(needsGlobal) |
| 28 | is AbstractIntSetting -> setting.int | 28 | is AbstractFloatSetting -> setting.getFloat(needsGlobal).roundToInt() |
| 29 | is AbstractFloatSetting -> setting.float.roundToInt() | 29 | else -> -1 |
| 30 | else -> -1 | ||
| 31 | } | ||
| 32 | } | 30 | } |
| 33 | set(value) { | 31 | |
| 34 | when (setting) { | 32 | fun setSelectedValue(value: Int) = |
| 35 | is AbstractByteSetting -> setting.setByte(value.toByte()) | 33 | when (setting) { |
| 36 | is AbstractShortSetting -> setting.setShort(value.toShort()) | 34 | is AbstractByteSetting -> setting.setByte(value.toByte()) |
| 37 | is AbstractIntSetting -> setting.setInt(value) | 35 | is AbstractShortSetting -> setting.setShort(value.toShort()) |
| 38 | is AbstractFloatSetting -> setting.setFloat(value.toFloat()) | 36 | is AbstractFloatSetting -> setting.setFloat(value.toFloat()) |
| 39 | } | 37 | else -> (setting as AbstractIntSetting).setInt(value) |
| 40 | } | 38 | } |
| 41 | } | 39 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt index 871dab4f3..ba7920f50 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt | |||
| @@ -17,14 +17,13 @@ class StringSingleChoiceSetting( | |||
| 17 | fun getValueAt(index: Int): String = | 17 | fun getValueAt(index: Int): String = |
| 18 | if (index >= 0 && index < values.size) values[index] else "" | 18 | if (index >= 0 && index < values.size) values[index] else "" |
| 19 | 19 | ||
| 20 | var selectedValue: String | 20 | fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal) |
| 21 | get() = stringSetting.string | 21 | fun setSelectedValue(value: String) = stringSetting.setString(value) |
| 22 | set(value) = stringSetting.setString(value) | ||
| 23 | 22 | ||
| 24 | val selectValueIndex: Int | 23 | val selectValueIndex: Int |
| 25 | get() { | 24 | get() { |
| 26 | for (i in values.indices) { | 25 | for (i in values.indices) { |
| 27 | if (values[i] == selectedValue) { | 26 | if (values[i] == getSelectedValue()) { |
| 28 | return i | 27 | return i |
| 29 | } | 28 | } |
| 30 | } | 29 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt index 416967e64..44d47dd69 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt | |||
| @@ -14,18 +14,18 @@ class SwitchSetting( | |||
| 14 | ) : SettingsItem(setting, titleId, descriptionId) { | 14 | ) : SettingsItem(setting, titleId, descriptionId) { |
| 15 | override val type = TYPE_SWITCH | 15 | override val type = TYPE_SWITCH |
| 16 | 16 | ||
| 17 | var checked: Boolean | 17 | fun getIsChecked(needsGlobal: Boolean = false): Boolean { |
| 18 | get() { | 18 | return when (setting) { |
| 19 | return when (setting) { | 19 | is AbstractIntSetting -> setting.getInt(needsGlobal) == 1 |
| 20 | is AbstractIntSetting -> setting.int == 1 | 20 | is AbstractBooleanSetting -> setting.getBoolean(needsGlobal) |
| 21 | is AbstractBooleanSetting -> setting.boolean | 21 | else -> false |
| 22 | else -> false | ||
| 23 | } | ||
| 24 | } | 22 | } |
| 25 | set(value) { | 23 | } |
| 26 | when (setting) { | 24 | |
| 27 | is AbstractIntSetting -> setting.setInt(if (value) 1 else 0) | 25 | fun setChecked(value: Boolean) { |
| 28 | is AbstractBooleanSetting -> setting.setBoolean(value) | 26 | when (setting) { |
| 29 | } | 27 | is AbstractIntSetting -> setting.setInt(if (value) 1 else 0) |
| 28 | is AbstractBooleanSetting -> setting.setBoolean(value) | ||
| 30 | } | 29 | } |
| 30 | } | ||
| 31 | } | 31 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt index 64bfc6dd0..6f072241a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt | |||
| @@ -19,10 +19,9 @@ import androidx.lifecycle.repeatOnLifecycle | |||
| 19 | import androidx.navigation.fragment.NavHostFragment | 19 | import androidx.navigation.fragment.NavHostFragment |
| 20 | import androidx.navigation.navArgs | 20 | import androidx.navigation.navArgs |
| 21 | import com.google.android.material.color.MaterialColors | 21 | import com.google.android.material.color.MaterialColors |
| 22 | import kotlinx.coroutines.CoroutineScope | ||
| 23 | import kotlinx.coroutines.Dispatchers | ||
| 24 | import kotlinx.coroutines.flow.collectLatest | 22 | import kotlinx.coroutines.flow.collectLatest |
| 25 | import kotlinx.coroutines.launch | 23 | import kotlinx.coroutines.launch |
| 24 | import org.yuzu.yuzu_emu.NativeLibrary | ||
| 26 | import java.io.IOException | 25 | import java.io.IOException |
| 27 | import org.yuzu.yuzu_emu.R | 26 | import org.yuzu.yuzu_emu.R |
| 28 | import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding | 27 | import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding |
| @@ -46,6 +45,9 @@ class SettingsActivity : AppCompatActivity() { | |||
| 46 | binding = ActivitySettingsBinding.inflate(layoutInflater) | 45 | binding = ActivitySettingsBinding.inflate(layoutInflater) |
| 47 | setContentView(binding.root) | 46 | setContentView(binding.root) |
| 48 | 47 | ||
| 48 | if (!NativeConfig.isPerGameConfigLoaded() && args.game != null) { | ||
| 49 | SettingsFile.loadCustomConfig(args.game!!) | ||
| 50 | } | ||
| 49 | settingsViewModel.game = args.game | 51 | settingsViewModel.game = args.game |
| 50 | 52 | ||
| 51 | val navHostFragment = | 53 | val navHostFragment = |
| @@ -126,7 +128,6 @@ class SettingsActivity : AppCompatActivity() { | |||
| 126 | 128 | ||
| 127 | override fun onStart() { | 129 | override fun onStart() { |
| 128 | super.onStart() | 130 | super.onStart() |
| 129 | // TODO: Load custom settings contextually | ||
| 130 | if (!DirectoryInitialization.areDirectoriesReady) { | 131 | if (!DirectoryInitialization.areDirectoriesReady) { |
| 131 | DirectoryInitialization.start() | 132 | DirectoryInitialization.start() |
| 132 | } | 133 | } |
| @@ -134,24 +135,35 @@ class SettingsActivity : AppCompatActivity() { | |||
| 134 | 135 | ||
| 135 | override fun onStop() { | 136 | override fun onStop() { |
| 136 | super.onStop() | 137 | super.onStop() |
| 137 | CoroutineScope(Dispatchers.IO).launch { | 138 | Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...") |
| 138 | NativeConfig.saveSettings() | 139 | if (isFinishing) { |
| 140 | NativeLibrary.applySettings() | ||
| 141 | if (args.game == null) { | ||
| 142 | NativeConfig.saveGlobalConfig() | ||
| 143 | } else if (NativeConfig.isPerGameConfigLoaded()) { | ||
| 144 | NativeLibrary.logSettings() | ||
| 145 | NativeConfig.savePerGameConfig() | ||
| 146 | NativeConfig.unloadPerGameConfig() | ||
| 147 | } | ||
| 139 | } | 148 | } |
| 140 | } | 149 | } |
| 141 | 150 | ||
| 142 | override fun onDestroy() { | ||
| 143 | settingsViewModel.clear() | ||
| 144 | super.onDestroy() | ||
| 145 | } | ||
| 146 | |||
| 147 | fun onSettingsReset() { | 151 | fun onSettingsReset() { |
| 148 | // Delete settings file because the user may have changed values that do not exist in the UI | 152 | // Delete settings file because the user may have changed values that do not exist in the UI |
| 149 | NativeConfig.unloadConfig() | 153 | if (args.game == null) { |
| 150 | val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG) | 154 | NativeConfig.unloadGlobalConfig() |
| 151 | if (!settingsFile.delete()) { | 155 | val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG) |
| 152 | throw IOException("Failed to delete $settingsFile") | 156 | if (!settingsFile.delete()) { |
| 157 | throw IOException("Failed to delete $settingsFile") | ||
| 158 | } | ||
| 159 | NativeConfig.initializeGlobalConfig() | ||
| 160 | } else { | ||
| 161 | NativeConfig.unloadPerGameConfig() | ||
| 162 | val settingsFile = SettingsFile.getCustomSettingsFile(args.game!!) | ||
| 163 | if (!settingsFile.delete()) { | ||
| 164 | throw IOException("Failed to delete $settingsFile") | ||
| 165 | } | ||
| 153 | } | 166 | } |
| 154 | NativeConfig.initializeConfig() | ||
| 155 | 167 | ||
| 156 | Toast.makeText( | 168 | Toast.makeText( |
| 157 | applicationContext, | 169 | applicationContext, |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt index af2c1e582..be9b3031b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt | |||
| @@ -102,8 +102,9 @@ class SettingsAdapter( | |||
| 102 | return currentList[position].type | 102 | return currentList[position].type |
| 103 | } | 103 | } |
| 104 | 104 | ||
| 105 | fun onBooleanClick(item: SwitchSetting, checked: Boolean) { | 105 | fun onBooleanClick(item: SwitchSetting, checked: Boolean, position: Int) { |
| 106 | item.checked = checked | 106 | item.setChecked(checked) |
| 107 | notifyItemChanged(position) | ||
| 107 | settingsViewModel.setShouldReloadSettingsList(true) | 108 | settingsViewModel.setShouldReloadSettingsList(true) |
| 108 | } | 109 | } |
| 109 | 110 | ||
| @@ -126,7 +127,7 @@ class SettingsAdapter( | |||
| 126 | } | 127 | } |
| 127 | 128 | ||
| 128 | fun onDateTimeClick(item: DateTimeSetting, position: Int) { | 129 | fun onDateTimeClick(item: DateTimeSetting, position: Int) { |
| 129 | val storedTime = item.value * 1000 | 130 | val storedTime = item.getValue() * 1000 |
| 130 | 131 | ||
| 131 | // Helper to extract hour and minute from epoch time | 132 | // Helper to extract hour and minute from epoch time |
| 132 | val calendar: Calendar = Calendar.getInstance() | 133 | val calendar: Calendar = Calendar.getInstance() |
| @@ -159,9 +160,9 @@ class SettingsAdapter( | |||
| 159 | var epochTime: Long = datePicker.selection!! / 1000 | 160 | var epochTime: Long = datePicker.selection!! / 1000 |
| 160 | epochTime += timePicker.hour.toLong() * 60 * 60 | 161 | epochTime += timePicker.hour.toLong() * 60 * 60 |
| 161 | epochTime += timePicker.minute.toLong() * 60 | 162 | epochTime += timePicker.minute.toLong() * 60 |
| 162 | if (item.value != epochTime) { | 163 | if (item.getValue() != epochTime) { |
| 163 | notifyItemChanged(position) | 164 | notifyItemChanged(position) |
| 164 | item.value = epochTime | 165 | item.setValue(epochTime) |
| 165 | } | 166 | } |
| 166 | } | 167 | } |
| 167 | datePicker.show( | 168 | datePicker.show( |
| @@ -195,6 +196,12 @@ class SettingsAdapter( | |||
| 195 | return true | 196 | return true |
| 196 | } | 197 | } |
| 197 | 198 | ||
| 199 | fun onClearClick(item: SettingsItem, position: Int) { | ||
| 200 | item.setting.global = true | ||
| 201 | notifyItemChanged(position) | ||
| 202 | settingsViewModel.setShouldReloadSettingsList(true) | ||
| 203 | } | ||
| 204 | |||
| 198 | private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() { | 205 | private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() { |
| 199 | override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean { | 206 | override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean { |
| 200 | return oldItem.setting.key == newItem.setting.key | 207 | return oldItem.setting.key == newItem.setting.key |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt index 769baf744..d7ab0b5d9 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt | |||
| @@ -66,7 +66,13 @@ class SettingsFragment : Fragment() { | |||
| 66 | args.menuTag | 66 | args.menuTag |
| 67 | ) | 67 | ) |
| 68 | 68 | ||
| 69 | binding.toolbarSettingsLayout.title = getString(args.menuTag.titleId) | 69 | binding.toolbarSettingsLayout.title = if (args.menuTag == Settings.MenuTag.SECTION_ROOT && |
| 70 | args.game != null | ||
| 71 | ) { | ||
| 72 | args.game!!.title | ||
| 73 | } else { | ||
| 74 | getString(args.menuTag.titleId) | ||
| 75 | } | ||
| 70 | binding.listSettings.apply { | 76 | binding.listSettings.apply { |
| 71 | adapter = settingsAdapter | 77 | adapter = settingsAdapter |
| 72 | layoutManager = LinearLayoutManager(requireContext()) | 78 | layoutManager = LinearLayoutManager(requireContext()) |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 7425728c6..a7e965589 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt | |||
| @@ -7,6 +7,7 @@ import android.content.SharedPreferences | |||
| 7 | import android.os.Build | 7 | import android.os.Build |
| 8 | import android.widget.Toast | 8 | import android.widget.Toast |
| 9 | import androidx.preference.PreferenceManager | 9 | import androidx.preference.PreferenceManager |
| 10 | import org.yuzu.yuzu_emu.NativeLibrary | ||
| 10 | import org.yuzu.yuzu_emu.R | 11 | import org.yuzu.yuzu_emu.R |
| 11 | import org.yuzu.yuzu_emu.YuzuApplication | 12 | import org.yuzu.yuzu_emu.YuzuApplication |
| 12 | import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting | 13 | import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting |
| @@ -31,12 +32,27 @@ class SettingsFragmentPresenter( | |||
| 31 | private val preferences: SharedPreferences | 32 | private val preferences: SharedPreferences |
| 32 | get() = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) | 33 | get() = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) |
| 33 | 34 | ||
| 34 | // Extension for populating settings list based on paired settings | 35 | // Extension for altering settings list based on each setting's properties |
| 35 | fun ArrayList<SettingsItem>.add(key: String) { | 36 | fun ArrayList<SettingsItem>.add(key: String) { |
| 36 | val item = SettingsItem.settingsItems[key]!! | 37 | val item = SettingsItem.settingsItems[key]!! |
| 38 | if (settingsViewModel.game != null && !item.setting.isSwitchable) { | ||
| 39 | return | ||
| 40 | } | ||
| 41 | |||
| 42 | if (!NativeConfig.isPerGameConfigLoaded() && !NativeLibrary.isRunning()) { | ||
| 43 | item.setting.global = true | ||
| 44 | } | ||
| 45 | |||
| 37 | val pairedSettingKey = item.setting.pairedSettingKey | 46 | val pairedSettingKey = item.setting.pairedSettingKey |
| 38 | if (pairedSettingKey.isNotEmpty()) { | 47 | if (pairedSettingKey.isNotEmpty()) { |
| 39 | val pairedSettingValue = NativeConfig.getBoolean(pairedSettingKey, false) | 48 | val pairedSettingValue = NativeConfig.getBoolean( |
| 49 | pairedSettingKey, | ||
| 50 | if (NativeLibrary.isRunning() && !NativeConfig.isPerGameConfigLoaded()) { | ||
| 51 | !NativeConfig.usingGlobal(pairedSettingKey) | ||
| 52 | } else { | ||
| 53 | NativeConfig.usingGlobal(pairedSettingKey) | ||
| 54 | } | ||
| 55 | ) | ||
| 40 | if (!pairedSettingValue) return | 56 | if (!pairedSettingValue) return |
| 41 | } | 57 | } |
| 42 | add(item) | 58 | add(item) |
| @@ -153,8 +169,8 @@ class SettingsFragmentPresenter( | |||
| 153 | private fun addThemeSettings(sl: ArrayList<SettingsItem>) { | 169 | private fun addThemeSettings(sl: ArrayList<SettingsItem>) { |
| 154 | sl.apply { | 170 | sl.apply { |
| 155 | val theme: AbstractIntSetting = object : AbstractIntSetting { | 171 | val theme: AbstractIntSetting = object : AbstractIntSetting { |
| 156 | override val int: Int | 172 | override fun getInt(needsGlobal: Boolean): Int = |
| 157 | get() = preferences.getInt(Settings.PREF_THEME, 0) | 173 | preferences.getInt(Settings.PREF_THEME, 0) |
| 158 | 174 | ||
| 159 | override fun setInt(value: Int) { | 175 | override fun setInt(value: Int) { |
| 160 | preferences.edit() | 176 | preferences.edit() |
| @@ -164,8 +180,8 @@ class SettingsFragmentPresenter( | |||
| 164 | } | 180 | } |
| 165 | 181 | ||
| 166 | override val key: String = Settings.PREF_THEME | 182 | override val key: String = Settings.PREF_THEME |
| 167 | override val category = Settings.Category.UiGeneral | ||
| 168 | override val isRuntimeModifiable: Boolean = false | 183 | override val isRuntimeModifiable: Boolean = false |
| 184 | override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString() | ||
| 169 | override val defaultValue: Int = 0 | 185 | override val defaultValue: Int = 0 |
| 170 | override fun reset() { | 186 | override fun reset() { |
| 171 | preferences.edit() | 187 | preferences.edit() |
| @@ -197,8 +213,8 @@ class SettingsFragmentPresenter( | |||
| 197 | } | 213 | } |
| 198 | 214 | ||
| 199 | val themeMode: AbstractIntSetting = object : AbstractIntSetting { | 215 | val themeMode: AbstractIntSetting = object : AbstractIntSetting { |
| 200 | override val int: Int | 216 | override fun getInt(needsGlobal: Boolean): Int = |
| 201 | get() = preferences.getInt(Settings.PREF_THEME_MODE, -1) | 217 | preferences.getInt(Settings.PREF_THEME_MODE, -1) |
| 202 | 218 | ||
| 203 | override fun setInt(value: Int) { | 219 | override fun setInt(value: Int) { |
| 204 | preferences.edit() | 220 | preferences.edit() |
| @@ -208,8 +224,8 @@ class SettingsFragmentPresenter( | |||
| 208 | } | 224 | } |
| 209 | 225 | ||
| 210 | override val key: String = Settings.PREF_THEME_MODE | 226 | override val key: String = Settings.PREF_THEME_MODE |
| 211 | override val category = Settings.Category.UiGeneral | ||
| 212 | override val isRuntimeModifiable: Boolean = false | 227 | override val isRuntimeModifiable: Boolean = false |
| 228 | override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString() | ||
| 213 | override val defaultValue: Int = -1 | 229 | override val defaultValue: Int = -1 |
| 214 | override fun reset() { | 230 | override fun reset() { |
| 215 | preferences.edit() | 231 | preferences.edit() |
| @@ -230,8 +246,8 @@ class SettingsFragmentPresenter( | |||
| 230 | ) | 246 | ) |
| 231 | 247 | ||
| 232 | val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting { | 248 | val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting { |
| 233 | override val boolean: Boolean | 249 | override fun getBoolean(needsGlobal: Boolean): Boolean = |
| 234 | get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) | 250 | preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) |
| 235 | 251 | ||
| 236 | override fun setBoolean(value: Boolean) { | 252 | override fun setBoolean(value: Boolean) { |
| 237 | preferences.edit() | 253 | preferences.edit() |
| @@ -241,8 +257,10 @@ class SettingsFragmentPresenter( | |||
| 241 | } | 257 | } |
| 242 | 258 | ||
| 243 | override val key: String = Settings.PREF_BLACK_BACKGROUNDS | 259 | override val key: String = Settings.PREF_BLACK_BACKGROUNDS |
| 244 | override val category = Settings.Category.UiGeneral | ||
| 245 | override val isRuntimeModifiable: Boolean = false | 260 | override val isRuntimeModifiable: Boolean = false |
| 261 | override fun getValueAsString(needsGlobal: Boolean): String = | ||
| 262 | getBoolean().toString() | ||
| 263 | |||
| 246 | override val defaultValue: Boolean = false | 264 | override val defaultValue: Boolean = false |
| 247 | override fun reset() { | 265 | override fun reset() { |
| 248 | preferences.edit() | 266 | preferences.edit() |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt index 525f013f8..5ad0899dd 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt | |||
| @@ -13,6 +13,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding | |||
| 13 | import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting | 13 | import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting |
| 14 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | 14 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem |
| 15 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | 15 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter |
| 16 | import org.yuzu.yuzu_emu.utils.NativeConfig | ||
| 16 | 17 | ||
| 17 | class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : | 18 | class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : |
| 18 | SettingViewHolder(binding.root, adapter) { | 19 | SettingViewHolder(binding.root, adapter) { |
| @@ -29,12 +30,23 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA | |||
| 29 | } | 30 | } |
| 30 | 31 | ||
| 31 | binding.textSettingValue.visibility = View.VISIBLE | 32 | binding.textSettingValue.visibility = View.VISIBLE |
| 32 | val epochTime = setting.value | 33 | val epochTime = setting.getValue() |
| 33 | val instant = Instant.ofEpochMilli(epochTime * 1000) | 34 | val instant = Instant.ofEpochMilli(epochTime * 1000) |
| 34 | val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")) | 35 | val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")) |
| 35 | val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) | 36 | val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) |
| 36 | binding.textSettingValue.text = dateFormatter.format(zonedTime) | 37 | binding.textSettingValue.text = dateFormatter.format(zonedTime) |
| 37 | 38 | ||
| 39 | binding.buttonClear.visibility = if (setting.setting.global || | ||
| 40 | !NativeConfig.isPerGameConfigLoaded() | ||
| 41 | ) { | ||
| 42 | View.GONE | ||
| 43 | } else { | ||
| 44 | View.VISIBLE | ||
| 45 | } | ||
| 46 | binding.buttonClear.setOnClickListener { | ||
| 47 | adapter.onClearClick(setting, bindingAdapterPosition) | ||
| 48 | } | ||
| 49 | |||
| 38 | setStyle(setting.isEditable, binding) | 50 | setStyle(setting.isEditable, binding) |
| 39 | } | 51 | } |
| 40 | 52 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt index 036195624..507184238 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt | |||
| @@ -38,6 +38,7 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA | |||
| 38 | binding.textSettingDescription.visibility = View.GONE | 38 | binding.textSettingDescription.visibility = View.GONE |
| 39 | } | 39 | } |
| 40 | binding.textSettingValue.visibility = View.GONE | 40 | binding.textSettingValue.visibility = View.GONE |
| 41 | binding.buttonClear.visibility = View.GONE | ||
| 41 | 42 | ||
| 42 | setStyle(setting.isEditable, binding) | 43 | setStyle(setting.isEditable, binding) |
| 43 | } | 44 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt index 0fd1d2eaa..d26887df8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt | |||
| @@ -41,6 +41,7 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings | |||
| 41 | binding.textSettingName.alpha = opacity | 41 | binding.textSettingName.alpha = opacity |
| 42 | binding.textSettingDescription.alpha = opacity | 42 | binding.textSettingDescription.alpha = opacity |
| 43 | binding.textSettingValue.alpha = opacity | 43 | binding.textSettingValue.alpha = opacity |
| 44 | binding.buttonClear.isEnabled = isEditable | ||
| 44 | } | 45 | } |
| 45 | 46 | ||
| 46 | fun setStyle(isEditable: Boolean, binding: ListItemSettingSwitchBinding) { | 47 | fun setStyle(isEditable: Boolean, binding: ListItemSettingSwitchBinding) { |
| @@ -48,5 +49,6 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings | |||
| 48 | val opacity = if (isEditable) 1.0f else 0.5f | 49 | val opacity = if (isEditable) 1.0f else 0.5f |
| 49 | binding.textSettingName.alpha = opacity | 50 | binding.textSettingName.alpha = opacity |
| 50 | binding.textSettingDescription.alpha = opacity | 51 | binding.textSettingDescription.alpha = opacity |
| 52 | binding.buttonClear.isEnabled = isEditable | ||
| 51 | } | 53 | } |
| 52 | } | 54 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt index 80d1b22c1..02dab3785 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt | |||
| @@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | |||
| 9 | import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting | 9 | import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting |
| 10 | import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting | 10 | import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting |
| 11 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | 11 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter |
| 12 | import org.yuzu.yuzu_emu.utils.NativeConfig | ||
| 12 | 13 | ||
| 13 | class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : | 14 | class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : |
| 14 | SettingViewHolder(binding.root, adapter) { | 15 | SettingViewHolder(binding.root, adapter) { |
| @@ -29,20 +30,31 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti | |||
| 29 | val resMgr = binding.textSettingValue.context.resources | 30 | val resMgr = binding.textSettingValue.context.resources |
| 30 | val values = resMgr.getIntArray(item.valuesId) | 31 | val values = resMgr.getIntArray(item.valuesId) |
| 31 | for (i in values.indices) { | 32 | for (i in values.indices) { |
| 32 | if (values[i] == item.selectedValue) { | 33 | if (values[i] == item.getSelectedValue()) { |
| 33 | binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i] | 34 | binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i] |
| 34 | break | 35 | break |
| 35 | } | 36 | } |
| 36 | } | 37 | } |
| 37 | } else if (item is StringSingleChoiceSetting) { | 38 | } else if (item is StringSingleChoiceSetting) { |
| 38 | for (i in item.values.indices) { | 39 | for (i in item.values.indices) { |
| 39 | if (item.values[i] == item.selectedValue) { | 40 | if (item.values[i] == item.getSelectedValue()) { |
| 40 | binding.textSettingValue.text = item.choices[i] | 41 | binding.textSettingValue.text = item.choices[i] |
| 41 | break | 42 | break |
| 42 | } | 43 | } |
| 43 | } | 44 | } |
| 44 | } | 45 | } |
| 45 | 46 | ||
| 47 | binding.buttonClear.visibility = if (setting.setting.global || | ||
| 48 | !NativeConfig.isPerGameConfigLoaded() | ||
| 49 | ) { | ||
| 50 | View.GONE | ||
| 51 | } else { | ||
| 52 | View.VISIBLE | ||
| 53 | } | ||
| 54 | binding.buttonClear.setOnClickListener { | ||
| 55 | adapter.onClearClick(setting, bindingAdapterPosition) | ||
| 56 | } | ||
| 57 | |||
| 46 | setStyle(setting.isEditable, binding) | 58 | setStyle(setting.isEditable, binding) |
| 47 | } | 59 | } |
| 48 | 60 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt index b83c90100..596c18012 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt | |||
| @@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding | |||
| 9 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | 9 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem |
| 10 | import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting | 10 | import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting |
| 11 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | 11 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter |
| 12 | import org.yuzu.yuzu_emu.utils.NativeConfig | ||
| 12 | 13 | ||
| 13 | class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : | 14 | class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : |
| 14 | SettingViewHolder(binding.root, adapter) { | 15 | SettingViewHolder(binding.root, adapter) { |
| @@ -26,10 +27,21 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda | |||
| 26 | binding.textSettingValue.visibility = View.VISIBLE | 27 | binding.textSettingValue.visibility = View.VISIBLE |
| 27 | binding.textSettingValue.text = String.format( | 28 | binding.textSettingValue.text = String.format( |
| 28 | binding.textSettingValue.context.getString(R.string.value_with_units), | 29 | binding.textSettingValue.context.getString(R.string.value_with_units), |
| 29 | setting.selectedValue, | 30 | setting.getSelectedValue(), |
| 30 | setting.units | 31 | setting.units |
| 31 | ) | 32 | ) |
| 32 | 33 | ||
| 34 | binding.buttonClear.visibility = if (setting.setting.global || | ||
| 35 | !NativeConfig.isPerGameConfigLoaded() | ||
| 36 | ) { | ||
| 37 | View.GONE | ||
| 38 | } else { | ||
| 39 | View.VISIBLE | ||
| 40 | } | ||
| 41 | binding.buttonClear.setOnClickListener { | ||
| 42 | adapter.onClearClick(setting, bindingAdapterPosition) | ||
| 43 | } | ||
| 44 | |||
| 33 | setStyle(setting.isEditable, binding) | 45 | setStyle(setting.isEditable, binding) |
| 34 | } | 46 | } |
| 35 | 47 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt index 8100c65dd..20d35a17d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt | |||
| @@ -37,6 +37,7 @@ class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAd | |||
| 37 | binding.textSettingDescription.visibility = View.GONE | 37 | binding.textSettingDescription.visibility = View.GONE |
| 38 | } | 38 | } |
| 39 | binding.textSettingValue.visibility = View.GONE | 39 | binding.textSettingValue.visibility = View.GONE |
| 40 | binding.buttonClear.visibility = View.GONE | ||
| 40 | } | 41 | } |
| 41 | 42 | ||
| 42 | override fun onClick(clicked: View) { | 43 | override fun onClick(clicked: View) { |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt index 57fdeaa20..d26bf9374 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt | |||
| @@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding | |||
| 9 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem | 9 | import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem |
| 10 | import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting | 10 | import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting |
| 11 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter | 11 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter |
| 12 | import org.yuzu.yuzu_emu.utils.NativeConfig | ||
| 12 | 13 | ||
| 13 | class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) : | 14 | class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) : |
| 14 | SettingViewHolder(binding.root, adapter) { | 15 | SettingViewHolder(binding.root, adapter) { |
| @@ -27,9 +28,20 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter | |||
| 27 | } | 28 | } |
| 28 | 29 | ||
| 29 | binding.switchWidget.setOnCheckedChangeListener(null) | 30 | binding.switchWidget.setOnCheckedChangeListener(null) |
| 30 | binding.switchWidget.isChecked = setting.checked | 31 | binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal) |
| 31 | binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> | 32 | binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> |
| 32 | adapter.onBooleanClick(item, binding.switchWidget.isChecked) | 33 | adapter.onBooleanClick(item, binding.switchWidget.isChecked, bindingAdapterPosition) |
| 34 | } | ||
| 35 | |||
| 36 | binding.buttonClear.visibility = if (setting.setting.global || | ||
| 37 | !NativeConfig.isPerGameConfigLoaded() | ||
| 38 | ) { | ||
| 39 | View.GONE | ||
| 40 | } else { | ||
| 41 | View.VISIBLE | ||
| 42 | } | ||
| 43 | binding.buttonClear.setOnClickListener { | ||
| 44 | adapter.onClearClick(setting, bindingAdapterPosition) | ||
| 33 | } | 45 | } |
| 34 | 46 | ||
| 35 | setStyle(setting.isEditable, binding) | 47 | setStyle(setting.isEditable, binding) |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt index 3ae5b4653..5d523be67 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt | |||
| @@ -3,15 +3,27 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.features.settings.utils | 4 | package org.yuzu.yuzu_emu.features.settings.utils |
| 5 | 5 | ||
| 6 | import android.net.Uri | ||
| 7 | import org.yuzu.yuzu_emu.model.Game | ||
| 6 | import java.io.* | 8 | import java.io.* |
| 7 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization | 9 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization |
| 10 | import org.yuzu.yuzu_emu.utils.FileUtil | ||
| 11 | import org.yuzu.yuzu_emu.utils.NativeConfig | ||
| 8 | 12 | ||
| 9 | /** | 13 | /** |
| 10 | * Contains static methods for interacting with .ini files in which settings are stored. | 14 | * Contains static methods for interacting with .ini files in which settings are stored. |
| 11 | */ | 15 | */ |
| 12 | object SettingsFile { | 16 | object SettingsFile { |
| 13 | const val FILE_NAME_CONFIG = "config" | 17 | const val FILE_NAME_CONFIG = "config.ini" |
| 14 | 18 | ||
| 15 | fun getSettingsFile(fileName: String): File = | 19 | fun getSettingsFile(fileName: String): File = |
| 16 | File(DirectoryInitialization.userDirectory + "/config/" + fileName + ".ini") | 20 | File(DirectoryInitialization.userDirectory + "/config/" + fileName) |
| 21 | |||
| 22 | fun getCustomSettingsFile(game: Game): File = | ||
| 23 | File(DirectoryInitialization.userDirectory + "/config/custom/" + game.settingsName + ".ini") | ||
| 24 | |||
| 25 | fun loadCustomConfig(game: Game) { | ||
| 26 | val fileName = FileUtil.getFilename(Uri.parse(game.path)) | ||
| 27 | NativeConfig.initializePerGameConfig(game.programId, fileName) | ||
| 28 | } | ||
| 17 | } | 29 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt new file mode 100644 index 000000000..0dce8ad8d --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt | |||
| @@ -0,0 +1,214 @@ | |||
| 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.annotation.SuppressLint | ||
| 7 | import android.content.Intent | ||
| 8 | import android.os.Bundle | ||
| 9 | import android.view.LayoutInflater | ||
| 10 | import android.view.View | ||
| 11 | import android.view.ViewGroup | ||
| 12 | import androidx.activity.result.contract.ActivityResultContracts | ||
| 13 | import androidx.core.view.ViewCompat | ||
| 14 | import androidx.core.view.WindowInsetsCompat | ||
| 15 | import androidx.core.view.updatePadding | ||
| 16 | import androidx.documentfile.provider.DocumentFile | ||
| 17 | import androidx.fragment.app.Fragment | ||
| 18 | import androidx.fragment.app.activityViewModels | ||
| 19 | import androidx.lifecycle.Lifecycle | ||
| 20 | import androidx.lifecycle.lifecycleScope | ||
| 21 | import androidx.lifecycle.repeatOnLifecycle | ||
| 22 | import androidx.navigation.findNavController | ||
| 23 | import androidx.navigation.fragment.navArgs | ||
| 24 | import androidx.recyclerview.widget.LinearLayoutManager | ||
| 25 | import com.google.android.material.transition.MaterialSharedAxis | ||
| 26 | import kotlinx.coroutines.launch | ||
| 27 | import org.yuzu.yuzu_emu.R | ||
| 28 | import org.yuzu.yuzu_emu.adapters.AddonAdapter | ||
| 29 | import org.yuzu.yuzu_emu.databinding.FragmentAddonsBinding | ||
| 30 | import org.yuzu.yuzu_emu.model.AddonViewModel | ||
| 31 | import org.yuzu.yuzu_emu.model.HomeViewModel | ||
| 32 | import org.yuzu.yuzu_emu.utils.AddonUtil | ||
| 33 | import org.yuzu.yuzu_emu.utils.FileUtil.copyFilesTo | ||
| 34 | import java.io.File | ||
| 35 | |||
| 36 | class AddonsFragment : Fragment() { | ||
| 37 | private var _binding: FragmentAddonsBinding? = null | ||
| 38 | private val binding get() = _binding!! | ||
| 39 | |||
| 40 | private val homeViewModel: HomeViewModel by activityViewModels() | ||
| 41 | private val addonViewModel: AddonViewModel by activityViewModels() | ||
| 42 | |||
| 43 | private val args by navArgs<AddonsFragmentArgs>() | ||
| 44 | |||
| 45 | override fun onCreate(savedInstanceState: Bundle?) { | ||
| 46 | super.onCreate(savedInstanceState) | ||
| 47 | addonViewModel.onOpenAddons(args.game) | ||
| 48 | enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) | ||
| 49 | returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) | ||
| 50 | reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) | ||
| 51 | } | ||
| 52 | |||
| 53 | override fun onCreateView( | ||
| 54 | inflater: LayoutInflater, | ||
| 55 | container: ViewGroup?, | ||
| 56 | savedInstanceState: Bundle? | ||
| 57 | ): View { | ||
| 58 | _binding = FragmentAddonsBinding.inflate(inflater) | ||
| 59 | return binding.root | ||
| 60 | } | ||
| 61 | |||
| 62 | // This is using the correct scope, lint is just acting up | ||
| 63 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") | ||
| 64 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| 65 | super.onViewCreated(view, savedInstanceState) | ||
| 66 | homeViewModel.setNavigationVisibility(visible = false, animated = false) | ||
| 67 | homeViewModel.setStatusBarShadeVisibility(false) | ||
| 68 | |||
| 69 | binding.toolbarAddons.setNavigationOnClickListener { | ||
| 70 | binding.root.findNavController().popBackStack() | ||
| 71 | } | ||
| 72 | |||
| 73 | binding.toolbarAddons.title = getString(R.string.addons_game, args.game.title) | ||
| 74 | |||
| 75 | binding.listAddons.apply { | ||
| 76 | layoutManager = LinearLayoutManager(requireContext()) | ||
| 77 | adapter = AddonAdapter() | ||
| 78 | } | ||
| 79 | |||
| 80 | viewLifecycleOwner.lifecycleScope.apply { | ||
| 81 | launch { | ||
| 82 | repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| 83 | addonViewModel.addonList.collect { | ||
| 84 | (binding.listAddons.adapter as AddonAdapter).submitList(it) | ||
| 85 | } | ||
| 86 | } | ||
| 87 | } | ||
| 88 | launch { | ||
| 89 | repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| 90 | addonViewModel.showModInstallPicker.collect { | ||
| 91 | if (it) { | ||
| 92 | installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) | ||
| 93 | addonViewModel.showModInstallPicker(false) | ||
| 94 | } | ||
| 95 | } | ||
| 96 | } | ||
| 97 | } | ||
| 98 | launch { | ||
| 99 | repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| 100 | addonViewModel.showModNoticeDialog.collect { | ||
| 101 | if (it) { | ||
| 102 | MessageDialogFragment.newInstance( | ||
| 103 | requireActivity(), | ||
| 104 | titleId = R.string.addon_notice, | ||
| 105 | descriptionId = R.string.addon_notice_description, | ||
| 106 | positiveAction = { addonViewModel.showModInstallPicker(true) } | ||
| 107 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 108 | addonViewModel.showModNoticeDialog(false) | ||
| 109 | } | ||
| 110 | } | ||
| 111 | } | ||
| 112 | } | ||
| 113 | } | ||
| 114 | |||
| 115 | binding.buttonInstall.setOnClickListener { | ||
| 116 | ContentTypeSelectionDialogFragment().show( | ||
| 117 | parentFragmentManager, | ||
| 118 | ContentTypeSelectionDialogFragment.TAG | ||
| 119 | ) | ||
| 120 | } | ||
| 121 | |||
| 122 | setInsets() | ||
| 123 | } | ||
| 124 | |||
| 125 | override fun onResume() { | ||
| 126 | super.onResume() | ||
| 127 | addonViewModel.refreshAddons() | ||
| 128 | } | ||
| 129 | |||
| 130 | override fun onDestroy() { | ||
| 131 | super.onDestroy() | ||
| 132 | addonViewModel.onCloseAddons() | ||
| 133 | } | ||
| 134 | |||
| 135 | val installAddon = | ||
| 136 | registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> | ||
| 137 | if (result == null) { | ||
| 138 | return@registerForActivityResult | ||
| 139 | } | ||
| 140 | |||
| 141 | val externalAddonDirectory = DocumentFile.fromTreeUri(requireContext(), result) | ||
| 142 | if (externalAddonDirectory == null) { | ||
| 143 | MessageDialogFragment.newInstance( | ||
| 144 | requireActivity(), | ||
| 145 | titleId = R.string.invalid_directory, | ||
| 146 | descriptionId = R.string.invalid_directory_description | ||
| 147 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 148 | return@registerForActivityResult | ||
| 149 | } | ||
| 150 | |||
| 151 | val isValid = externalAddonDirectory.listFiles() | ||
| 152 | .any { AddonUtil.validAddonDirectories.contains(it.name) } | ||
| 153 | val errorMessage = MessageDialogFragment.newInstance( | ||
| 154 | requireActivity(), | ||
| 155 | titleId = R.string.invalid_directory, | ||
| 156 | descriptionId = R.string.invalid_directory_description | ||
| 157 | ) | ||
| 158 | if (isValid) { | ||
| 159 | IndeterminateProgressDialogFragment.newInstance( | ||
| 160 | requireActivity(), | ||
| 161 | R.string.installing_game_content, | ||
| 162 | false | ||
| 163 | ) { | ||
| 164 | val parentDirectoryName = externalAddonDirectory.name | ||
| 165 | val internalAddonDirectory = | ||
| 166 | File(args.game.addonDir + parentDirectoryName) | ||
| 167 | try { | ||
| 168 | externalAddonDirectory.copyFilesTo(internalAddonDirectory) | ||
| 169 | } catch (_: Exception) { | ||
| 170 | return@newInstance errorMessage | ||
| 171 | } | ||
| 172 | addonViewModel.refreshAddons() | ||
| 173 | return@newInstance getString(R.string.addon_installed_successfully) | ||
| 174 | }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) | ||
| 175 | } else { | ||
| 176 | errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 177 | } | ||
| 178 | } | ||
| 179 | |||
| 180 | private fun setInsets() = | ||
| 181 | ViewCompat.setOnApplyWindowInsetsListener( | ||
| 182 | binding.root | ||
| 183 | ) { _: View, windowInsets: WindowInsetsCompat -> | ||
| 184 | val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) | ||
| 185 | val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) | ||
| 186 | |||
| 187 | val leftInsets = barInsets.left + cutoutInsets.left | ||
| 188 | val rightInsets = barInsets.right + cutoutInsets.right | ||
| 189 | |||
| 190 | val mlpToolbar = binding.toolbarAddons.layoutParams as ViewGroup.MarginLayoutParams | ||
| 191 | mlpToolbar.leftMargin = leftInsets | ||
| 192 | mlpToolbar.rightMargin = rightInsets | ||
| 193 | binding.toolbarAddons.layoutParams = mlpToolbar | ||
| 194 | |||
| 195 | val mlpAddonsList = binding.listAddons.layoutParams as ViewGroup.MarginLayoutParams | ||
| 196 | mlpAddonsList.leftMargin = leftInsets | ||
| 197 | mlpAddonsList.rightMargin = rightInsets | ||
| 198 | binding.listAddons.layoutParams = mlpAddonsList | ||
| 199 | binding.listAddons.updatePadding( | ||
| 200 | bottom = barInsets.bottom + | ||
| 201 | resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab) | ||
| 202 | ) | ||
| 203 | |||
| 204 | val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab) | ||
| 205 | val mlpFab = | ||
| 206 | binding.buttonInstall.layoutParams as ViewGroup.MarginLayoutParams | ||
| 207 | mlpFab.leftMargin = leftInsets + fabSpacing | ||
| 208 | mlpFab.rightMargin = rightInsets + fabSpacing | ||
| 209 | mlpFab.bottomMargin = barInsets.bottom + fabSpacing | ||
| 210 | binding.buttonInstall.layoutParams = mlpFab | ||
| 211 | |||
| 212 | windowInsets | ||
| 213 | } | ||
| 214 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ContentTypeSelectionDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ContentTypeSelectionDialogFragment.kt new file mode 100644 index 000000000..c1d8b9ea5 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ContentTypeSelectionDialogFragment.kt | |||
| @@ -0,0 +1,68 @@ | |||
| 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 androidx.preference.PreferenceManager | ||
| 12 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||
| 13 | import org.yuzu.yuzu_emu.R | ||
| 14 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 15 | import org.yuzu.yuzu_emu.model.AddonViewModel | ||
| 16 | import org.yuzu.yuzu_emu.ui.main.MainActivity | ||
| 17 | |||
| 18 | class ContentTypeSelectionDialogFragment : DialogFragment() { | ||
| 19 | private val addonViewModel: AddonViewModel by activityViewModels() | ||
| 20 | |||
| 21 | private val preferences get() = | ||
| 22 | PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) | ||
| 23 | |||
| 24 | private var selectedItem = 0 | ||
| 25 | |||
| 26 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | ||
| 27 | val launchOptions = | ||
| 28 | arrayOf(getString(R.string.updates_and_dlc), getString(R.string.mods_and_cheats)) | ||
| 29 | |||
| 30 | if (savedInstanceState != null) { | ||
| 31 | selectedItem = savedInstanceState.getInt(SELECTED_ITEM) | ||
| 32 | } | ||
| 33 | |||
| 34 | val mainActivity = requireActivity() as MainActivity | ||
| 35 | return MaterialAlertDialogBuilder(requireContext()) | ||
| 36 | .setTitle(R.string.select_content_type) | ||
| 37 | .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> | ||
| 38 | when (selectedItem) { | ||
| 39 | 0 -> mainActivity.installGameUpdate.launch(arrayOf("*/*")) | ||
| 40 | else -> { | ||
| 41 | if (!preferences.getBoolean(MOD_NOTICE_SEEN, false)) { | ||
| 42 | preferences.edit().putBoolean(MOD_NOTICE_SEEN, true).apply() | ||
| 43 | addonViewModel.showModNoticeDialog(true) | ||
| 44 | return@setPositiveButton | ||
| 45 | } | ||
| 46 | addonViewModel.showModInstallPicker(true) | ||
| 47 | } | ||
| 48 | } | ||
| 49 | } | ||
| 50 | .setSingleChoiceItems(launchOptions, 0) { _: DialogInterface, i: Int -> | ||
| 51 | selectedItem = i | ||
| 52 | } | ||
| 53 | .setNegativeButton(android.R.string.cancel, null) | ||
| 54 | .show() | ||
| 55 | } | ||
| 56 | |||
| 57 | override fun onSaveInstanceState(outState: Bundle) { | ||
| 58 | super.onSaveInstanceState(outState) | ||
| 59 | outState.putInt(SELECTED_ITEM, selectedItem) | ||
| 60 | } | ||
| 61 | |||
| 62 | companion object { | ||
| 63 | const val TAG = "ContentTypeSelectionDialogFragment" | ||
| 64 | |||
| 65 | private const val SELECTED_ITEM = "SelectedItem" | ||
| 66 | private const val MOD_NOTICE_SEEN = "ModNoticeSeen" | ||
| 67 | } | ||
| 68 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt index df21d74b2..cc71254dc 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt | |||
| @@ -15,6 +15,7 @@ import androidx.fragment.app.Fragment | |||
| 15 | import androidx.fragment.app.activityViewModels | 15 | import androidx.fragment.app.activityViewModels |
| 16 | import androidx.lifecycle.lifecycleScope | 16 | import androidx.lifecycle.lifecycleScope |
| 17 | import androidx.navigation.findNavController | 17 | import androidx.navigation.findNavController |
| 18 | import androidx.navigation.fragment.navArgs | ||
| 18 | import androidx.recyclerview.widget.GridLayoutManager | 19 | import androidx.recyclerview.widget.GridLayoutManager |
| 19 | import com.google.android.material.transition.MaterialSharedAxis | 20 | import com.google.android.material.transition.MaterialSharedAxis |
| 20 | import kotlinx.coroutines.flow.collectLatest | 21 | import kotlinx.coroutines.flow.collectLatest |
| @@ -36,6 +37,8 @@ class DriverManagerFragment : Fragment() { | |||
| 36 | private val homeViewModel: HomeViewModel by activityViewModels() | 37 | private val homeViewModel: HomeViewModel by activityViewModels() |
| 37 | private val driverViewModel: DriverViewModel by activityViewModels() | 38 | private val driverViewModel: DriverViewModel by activityViewModels() |
| 38 | 39 | ||
| 40 | private val args by navArgs<DriverManagerFragmentArgs>() | ||
| 41 | |||
| 39 | override fun onCreate(savedInstanceState: Bundle?) { | 42 | override fun onCreate(savedInstanceState: Bundle?) { |
| 40 | super.onCreate(savedInstanceState) | 43 | super.onCreate(savedInstanceState) |
| 41 | enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) | 44 | enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) |
| @@ -57,7 +60,9 @@ class DriverManagerFragment : Fragment() { | |||
| 57 | homeViewModel.setNavigationVisibility(visible = false, animated = true) | 60 | homeViewModel.setNavigationVisibility(visible = false, animated = true) |
| 58 | homeViewModel.setStatusBarShadeVisibility(visible = false) | 61 | homeViewModel.setStatusBarShadeVisibility(visible = false) |
| 59 | 62 | ||
| 60 | if (!driverViewModel.isInteractionAllowed) { | 63 | driverViewModel.onOpenDriverManager(args.game) |
| 64 | |||
| 65 | if (!driverViewModel.isInteractionAllowed.value) { | ||
| 61 | DriversLoadingDialogFragment().show( | 66 | DriversLoadingDialogFragment().show( |
| 62 | childFragmentManager, | 67 | childFragmentManager, |
| 63 | DriversLoadingDialogFragment.TAG | 68 | DriversLoadingDialogFragment.TAG |
| @@ -102,10 +107,9 @@ class DriverManagerFragment : Fragment() { | |||
| 102 | setInsets() | 107 | setInsets() |
| 103 | } | 108 | } |
| 104 | 109 | ||
| 105 | // Start installing requested driver | 110 | override fun onDestroy() { |
| 106 | override fun onStop() { | 111 | super.onDestroy() |
| 107 | super.onStop() | 112 | driverViewModel.onCloseDriverManager(args.game) |
| 108 | driverViewModel.onCloseDriverManager() | ||
| 109 | } | 113 | } |
| 110 | 114 | ||
| 111 | private fun setInsets() = | 115 | private fun setInsets() = |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt index f8c34346a..6a47b29f0 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt | |||
| @@ -47,25 +47,9 @@ class DriversLoadingDialogFragment : DialogFragment() { | |||
| 47 | viewLifecycleOwner.lifecycleScope.apply { | 47 | viewLifecycleOwner.lifecycleScope.apply { |
| 48 | launch { | 48 | launch { |
| 49 | repeatOnLifecycle(Lifecycle.State.RESUMED) { | 49 | repeatOnLifecycle(Lifecycle.State.RESUMED) { |
| 50 | driverViewModel.areDriversLoading.collect { checkForDismiss() } | 50 | driverViewModel.isInteractionAllowed.collect { if (it) dismiss() } |
| 51 | } | 51 | } |
| 52 | } | 52 | } |
| 53 | launch { | ||
| 54 | repeatOnLifecycle(Lifecycle.State.RESUMED) { | ||
| 55 | driverViewModel.isDriverReady.collect { checkForDismiss() } | ||
| 56 | } | ||
| 57 | } | ||
| 58 | launch { | ||
| 59 | repeatOnLifecycle(Lifecycle.State.RESUMED) { | ||
| 60 | driverViewModel.isDeletingDrivers.collect { checkForDismiss() } | ||
| 61 | } | ||
| 62 | } | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 66 | private fun checkForDismiss() { | ||
| 67 | if (driverViewModel.isInteractionAllowed) { | ||
| 68 | dismiss() | ||
| 69 | } | 53 | } |
| 70 | } | 54 | } |
| 71 | 55 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index 734c1d5ca..d7b38f62d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt | |||
| @@ -52,6 +52,7 @@ import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding | |||
| 52 | import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding | 52 | import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding |
| 53 | import org.yuzu.yuzu_emu.features.settings.model.IntSetting | 53 | import org.yuzu.yuzu_emu.features.settings.model.IntSetting |
| 54 | import org.yuzu.yuzu_emu.features.settings.model.Settings | 54 | import org.yuzu.yuzu_emu.features.settings.model.Settings |
| 55 | import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile | ||
| 55 | import org.yuzu.yuzu_emu.model.DriverViewModel | 56 | import org.yuzu.yuzu_emu.model.DriverViewModel |
| 56 | import org.yuzu.yuzu_emu.model.Game | 57 | import org.yuzu.yuzu_emu.model.Game |
| 57 | import org.yuzu.yuzu_emu.model.EmulationViewModel | 58 | import org.yuzu.yuzu_emu.model.EmulationViewModel |
| @@ -127,6 +128,17 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 127 | return | 128 | return |
| 128 | } | 129 | } |
| 129 | 130 | ||
| 131 | // Always load custom settings when launching a game from an intent | ||
| 132 | if (args.custom || intentGame != null) { | ||
| 133 | SettingsFile.loadCustomConfig(game) | ||
| 134 | NativeConfig.unloadPerGameConfig() | ||
| 135 | } else { | ||
| 136 | NativeConfig.reloadGlobalConfig() | ||
| 137 | } | ||
| 138 | |||
| 139 | // Install the selected driver asynchronously as the game starts | ||
| 140 | driverViewModel.onLaunchGame() | ||
| 141 | |||
| 130 | // So this fragment doesn't restart on configuration changes; i.e. rotation. | 142 | // So this fragment doesn't restart on configuration changes; i.e. rotation. |
| 131 | retainInstance = true | 143 | retainInstance = true |
| 132 | preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) | 144 | preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) |
| @@ -217,6 +229,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 217 | true | 229 | true |
| 218 | } | 230 | } |
| 219 | 231 | ||
| 232 | R.id.menu_settings_per_game -> { | ||
| 233 | val action = HomeNavigationDirections.actionGlobalSettingsActivity( | ||
| 234 | args.game, | ||
| 235 | Settings.MenuTag.SECTION_ROOT | ||
| 236 | ) | ||
| 237 | binding.root.findNavController().navigate(action) | ||
| 238 | true | ||
| 239 | } | ||
| 240 | |||
| 220 | R.id.menu_overlay_controls -> { | 241 | R.id.menu_overlay_controls -> { |
| 221 | showOverlayOptions() | 242 | showOverlayOptions() |
| 222 | true | 243 | true |
| @@ -332,15 +353,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 332 | } | 353 | } |
| 333 | launch { | 354 | launch { |
| 334 | repeatOnLifecycle(Lifecycle.State.RESUMED) { | 355 | repeatOnLifecycle(Lifecycle.State.RESUMED) { |
| 335 | driverViewModel.isDriverReady.collect { | 356 | driverViewModel.isInteractionAllowed.collect { |
| 336 | if (it && !emulationState.isRunning) { | 357 | if (it) { |
| 337 | if (!DirectoryInitialization.areDirectoriesReady) { | 358 | onEmulationStart() |
| 338 | DirectoryInitialization.start() | ||
| 339 | } | ||
| 340 | |||
| 341 | updateScreenLayout() | ||
| 342 | |||
| 343 | emulationState.run(emulationActivity!!.isActivityRecreated) | ||
| 344 | } | 359 | } |
| 345 | } | 360 | } |
| 346 | } | 361 | } |
| @@ -348,6 +363,18 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 348 | } | 363 | } |
| 349 | } | 364 | } |
| 350 | 365 | ||
| 366 | private fun onEmulationStart() { | ||
| 367 | if (!NativeLibrary.isRunning() && !NativeLibrary.isPaused()) { | ||
| 368 | if (!DirectoryInitialization.areDirectoriesReady) { | ||
| 369 | DirectoryInitialization.start() | ||
| 370 | } | ||
| 371 | |||
| 372 | updateScreenLayout() | ||
| 373 | |||
| 374 | emulationState.run(emulationActivity!!.isActivityRecreated) | ||
| 375 | } | ||
| 376 | } | ||
| 377 | |||
| 351 | override fun onConfigurationChanged(newConfig: Configuration) { | 378 | override fun onConfigurationChanged(newConfig: Configuration) { |
| 352 | super.onConfigurationChanged(newConfig) | 379 | super.onConfigurationChanged(newConfig) |
| 353 | if (_binding == null) { | 380 | if (_binding == null) { |
| @@ -435,7 +462,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 435 | @SuppressLint("SourceLockedOrientationActivity") | 462 | @SuppressLint("SourceLockedOrientationActivity") |
| 436 | private fun updateOrientation() { | 463 | private fun updateOrientation() { |
| 437 | emulationActivity?.let { | 464 | emulationActivity?.let { |
| 438 | it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) { | 465 | it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.getInt()) { |
| 439 | Settings.LayoutOption_MobileLandscape -> | 466 | Settings.LayoutOption_MobileLandscape -> |
| 440 | ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE | 467 | ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE |
| 441 | Settings.LayoutOption_MobilePortrait -> | 468 | Settings.LayoutOption_MobilePortrait -> |
| @@ -617,7 +644,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 617 | @SuppressLint("SourceLockedOrientationActivity") | 644 | @SuppressLint("SourceLockedOrientationActivity") |
| 618 | private fun startConfiguringControls() { | 645 | private fun startConfiguringControls() { |
| 619 | // Lock the current orientation to prevent editing inconsistencies | 646 | // Lock the current orientation to prevent editing inconsistencies |
| 620 | if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) { | 647 | if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == Settings.LayoutOption_Unspecified) { |
| 621 | emulationActivity?.let { | 648 | emulationActivity?.let { |
| 622 | it.requestedOrientation = | 649 | it.requestedOrientation = |
| 623 | if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { | 650 | if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { |
| @@ -635,7 +662,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||
| 635 | binding.doneControlConfig.visibility = View.GONE | 662 | binding.doneControlConfig.visibility = View.GONE |
| 636 | binding.surfaceInputOverlay.setIsInEditMode(false) | 663 | binding.surfaceInputOverlay.setIsInEditMode(false) |
| 637 | // Unlock the orientation if it was locked for editing | 664 | // Unlock the orientation if it was locked for editing |
| 638 | if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) { | 665 | if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == Settings.LayoutOption_Unspecified) { |
| 639 | emulationActivity?.let { | 666 | emulationActivity?.let { |
| 640 | it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED | 667 | it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED |
| 641 | } | 668 | } |
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 index b6c2e4635..1ea1e036e 100644 --- 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 | |||
| @@ -13,6 +13,7 @@ import org.yuzu.yuzu_emu.R | |||
| 13 | import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding | 13 | import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding |
| 14 | import org.yuzu.yuzu_emu.model.GameDir | 14 | import org.yuzu.yuzu_emu.model.GameDir |
| 15 | import org.yuzu.yuzu_emu.model.GamesViewModel | 15 | import org.yuzu.yuzu_emu.model.GamesViewModel |
| 16 | import org.yuzu.yuzu_emu.utils.NativeConfig | ||
| 16 | import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable | 17 | import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable |
| 17 | 18 | ||
| 18 | class GameFolderPropertiesDialogFragment : DialogFragment() { | 19 | class GameFolderPropertiesDialogFragment : DialogFragment() { |
| @@ -49,6 +50,11 @@ class GameFolderPropertiesDialogFragment : DialogFragment() { | |||
| 49 | .show() | 50 | .show() |
| 50 | } | 51 | } |
| 51 | 52 | ||
| 53 | override fun onStop() { | ||
| 54 | super.onStop() | ||
| 55 | NativeConfig.saveGlobalConfig() | ||
| 56 | } | ||
| 57 | |||
| 52 | override fun onSaveInstanceState(outState: Bundle) { | 58 | override fun onSaveInstanceState(outState: Bundle) { |
| 53 | super.onSaveInstanceState(outState) | 59 | super.onSaveInstanceState(outState) |
| 54 | outState.putBoolean(DEEP_SCAN, deepScan) | 60 | outState.putBoolean(DEEP_SCAN, deepScan) |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt new file mode 100644 index 000000000..fa2a4c9f9 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt | |||
| @@ -0,0 +1,148 @@ | |||
| 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.ClipData | ||
| 7 | import android.content.ClipboardManager | ||
| 8 | import android.content.Context | ||
| 9 | import android.net.Uri | ||
| 10 | import android.os.Build | ||
| 11 | import android.os.Bundle | ||
| 12 | import android.view.LayoutInflater | ||
| 13 | import android.view.View | ||
| 14 | import android.view.ViewGroup | ||
| 15 | import android.widget.Toast | ||
| 16 | import androidx.core.view.ViewCompat | ||
| 17 | import androidx.core.view.WindowInsetsCompat | ||
| 18 | import androidx.core.view.updatePadding | ||
| 19 | import androidx.fragment.app.Fragment | ||
| 20 | import androidx.fragment.app.activityViewModels | ||
| 21 | import androidx.navigation.findNavController | ||
| 22 | import androidx.navigation.fragment.navArgs | ||
| 23 | import com.google.android.material.transition.MaterialSharedAxis | ||
| 24 | import org.yuzu.yuzu_emu.R | ||
| 25 | import org.yuzu.yuzu_emu.databinding.FragmentGameInfoBinding | ||
| 26 | import org.yuzu.yuzu_emu.model.HomeViewModel | ||
| 27 | import org.yuzu.yuzu_emu.utils.GameMetadata | ||
| 28 | |||
| 29 | class GameInfoFragment : Fragment() { | ||
| 30 | private var _binding: FragmentGameInfoBinding? = null | ||
| 31 | private val binding get() = _binding!! | ||
| 32 | |||
| 33 | private val homeViewModel: HomeViewModel by activityViewModels() | ||
| 34 | |||
| 35 | private val args by navArgs<GameInfoFragmentArgs>() | ||
| 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 | // Check for an up-to-date version string | ||
| 44 | args.game.version = GameMetadata.getVersion(args.game.path, true) | ||
| 45 | } | ||
| 46 | |||
| 47 | override fun onCreateView( | ||
| 48 | inflater: LayoutInflater, | ||
| 49 | container: ViewGroup?, | ||
| 50 | savedInstanceState: Bundle? | ||
| 51 | ): View { | ||
| 52 | _binding = FragmentGameInfoBinding.inflate(inflater) | ||
| 53 | return binding.root | ||
| 54 | } | ||
| 55 | |||
| 56 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| 57 | super.onViewCreated(view, savedInstanceState) | ||
| 58 | homeViewModel.setNavigationVisibility(visible = false, animated = false) | ||
| 59 | homeViewModel.setStatusBarShadeVisibility(false) | ||
| 60 | |||
| 61 | binding.apply { | ||
| 62 | toolbarInfo.title = args.game.title | ||
| 63 | toolbarInfo.setNavigationOnClickListener { | ||
| 64 | view.findNavController().popBackStack() | ||
| 65 | } | ||
| 66 | |||
| 67 | val pathString = Uri.parse(args.game.path).path ?: "" | ||
| 68 | path.setHint(R.string.path) | ||
| 69 | pathField.setText(pathString) | ||
| 70 | pathField.setOnClickListener { copyToClipboard(getString(R.string.path), pathString) } | ||
| 71 | |||
| 72 | programId.setHint(R.string.program_id) | ||
| 73 | programIdField.setText(args.game.programIdHex) | ||
| 74 | programIdField.setOnClickListener { | ||
| 75 | copyToClipboard(getString(R.string.program_id), args.game.programIdHex) | ||
| 76 | } | ||
| 77 | |||
| 78 | if (args.game.developer.isNotEmpty()) { | ||
| 79 | developer.setHint(R.string.developer) | ||
| 80 | developerField.setText(args.game.developer) | ||
| 81 | developerField.setOnClickListener { | ||
| 82 | copyToClipboard(getString(R.string.developer), args.game.developer) | ||
| 83 | } | ||
| 84 | } else { | ||
| 85 | developer.visibility = View.GONE | ||
| 86 | } | ||
| 87 | |||
| 88 | version.setHint(R.string.version) | ||
| 89 | versionField.setText(args.game.version) | ||
| 90 | versionField.setOnClickListener { | ||
| 91 | copyToClipboard(getString(R.string.version), args.game.version) | ||
| 92 | } | ||
| 93 | |||
| 94 | buttonCopy.setOnClickListener { | ||
| 95 | val details = """ | ||
| 96 | ${args.game.title} | ||
| 97 | ${getString(R.string.path)} - $pathString | ||
| 98 | ${getString(R.string.program_id)} - ${args.game.programIdHex} | ||
| 99 | ${getString(R.string.developer)} - ${args.game.developer} | ||
| 100 | ${getString(R.string.version)} - ${args.game.version} | ||
| 101 | """.trimIndent() | ||
| 102 | copyToClipboard(args.game.title, details) | ||
| 103 | } | ||
| 104 | } | ||
| 105 | |||
| 106 | setInsets() | ||
| 107 | } | ||
| 108 | |||
| 109 | private fun copyToClipboard(label: String, body: String) { | ||
| 110 | val clipBoard = | ||
| 111 | requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager | ||
| 112 | val clip = ClipData.newPlainText(label, body) | ||
| 113 | clipBoard.setPrimaryClip(clip) | ||
| 114 | |||
| 115 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { | ||
| 116 | Toast.makeText( | ||
| 117 | requireContext(), | ||
| 118 | R.string.copied_to_clipboard, | ||
| 119 | Toast.LENGTH_SHORT | ||
| 120 | ).show() | ||
| 121 | } | ||
| 122 | } | ||
| 123 | |||
| 124 | private fun setInsets() = | ||
| 125 | ViewCompat.setOnApplyWindowInsetsListener( | ||
| 126 | binding.root | ||
| 127 | ) { _: View, windowInsets: WindowInsetsCompat -> | ||
| 128 | val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) | ||
| 129 | val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) | ||
| 130 | |||
| 131 | val leftInsets = barInsets.left + cutoutInsets.left | ||
| 132 | val rightInsets = barInsets.right + cutoutInsets.right | ||
| 133 | |||
| 134 | val mlpToolbar = binding.toolbarInfo.layoutParams as ViewGroup.MarginLayoutParams | ||
| 135 | mlpToolbar.leftMargin = leftInsets | ||
| 136 | mlpToolbar.rightMargin = rightInsets | ||
| 137 | binding.toolbarInfo.layoutParams = mlpToolbar | ||
| 138 | |||
| 139 | val mlpScrollAbout = binding.scrollInfo.layoutParams as ViewGroup.MarginLayoutParams | ||
| 140 | mlpScrollAbout.leftMargin = leftInsets | ||
| 141 | mlpScrollAbout.rightMargin = rightInsets | ||
| 142 | binding.scrollInfo.layoutParams = mlpScrollAbout | ||
| 143 | |||
| 144 | binding.contentInfo.updatePadding(bottom = barInsets.bottom) | ||
| 145 | |||
| 146 | windowInsets | ||
| 147 | } | ||
| 148 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt new file mode 100644 index 000000000..b1d3c0040 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt | |||
| @@ -0,0 +1,456 @@ | |||
| 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.annotation.SuppressLint | ||
| 7 | import android.os.Bundle | ||
| 8 | import android.text.TextUtils | ||
| 9 | import android.view.LayoutInflater | ||
| 10 | import android.view.View | ||
| 11 | import android.view.ViewGroup | ||
| 12 | import android.widget.Toast | ||
| 13 | import androidx.activity.result.contract.ActivityResultContracts | ||
| 14 | import androidx.core.view.ViewCompat | ||
| 15 | import androidx.core.view.WindowInsetsCompat | ||
| 16 | import androidx.core.view.updatePadding | ||
| 17 | import androidx.fragment.app.Fragment | ||
| 18 | import androidx.fragment.app.activityViewModels | ||
| 19 | import androidx.lifecycle.Lifecycle | ||
| 20 | import androidx.lifecycle.lifecycleScope | ||
| 21 | import androidx.lifecycle.repeatOnLifecycle | ||
| 22 | import androidx.navigation.findNavController | ||
| 23 | import androidx.navigation.fragment.navArgs | ||
| 24 | import androidx.recyclerview.widget.GridLayoutManager | ||
| 25 | import com.google.android.material.transition.MaterialSharedAxis | ||
| 26 | import kotlinx.coroutines.Dispatchers | ||
| 27 | import kotlinx.coroutines.launch | ||
| 28 | import kotlinx.coroutines.withContext | ||
| 29 | import org.yuzu.yuzu_emu.HomeNavigationDirections | ||
| 30 | import org.yuzu.yuzu_emu.R | ||
| 31 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 32 | import org.yuzu.yuzu_emu.adapters.GamePropertiesAdapter | ||
| 33 | import org.yuzu.yuzu_emu.databinding.FragmentGamePropertiesBinding | ||
| 34 | import org.yuzu.yuzu_emu.features.settings.model.Settings | ||
| 35 | import org.yuzu.yuzu_emu.model.DriverViewModel | ||
| 36 | import org.yuzu.yuzu_emu.model.GameProperty | ||
| 37 | import org.yuzu.yuzu_emu.model.GamesViewModel | ||
| 38 | import org.yuzu.yuzu_emu.model.HomeViewModel | ||
| 39 | import org.yuzu.yuzu_emu.model.InstallableProperty | ||
| 40 | import org.yuzu.yuzu_emu.model.SubmenuProperty | ||
| 41 | import org.yuzu.yuzu_emu.model.TaskState | ||
| 42 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization | ||
| 43 | import org.yuzu.yuzu_emu.utils.FileUtil | ||
| 44 | import org.yuzu.yuzu_emu.utils.GameIconUtils | ||
| 45 | import org.yuzu.yuzu_emu.utils.GpuDriverHelper | ||
| 46 | import org.yuzu.yuzu_emu.utils.MemoryUtil | ||
| 47 | import java.io.BufferedInputStream | ||
| 48 | import java.io.BufferedOutputStream | ||
| 49 | import java.io.File | ||
| 50 | |||
| 51 | class GamePropertiesFragment : Fragment() { | ||
| 52 | private var _binding: FragmentGamePropertiesBinding? = null | ||
| 53 | private val binding get() = _binding!! | ||
| 54 | |||
| 55 | private val homeViewModel: HomeViewModel by activityViewModels() | ||
| 56 | private val gamesViewModel: GamesViewModel by activityViewModels() | ||
| 57 | private val driverViewModel: DriverViewModel by activityViewModels() | ||
| 58 | |||
| 59 | private val args by navArgs<GamePropertiesFragmentArgs>() | ||
| 60 | |||
| 61 | override fun onCreate(savedInstanceState: Bundle?) { | ||
| 62 | super.onCreate(savedInstanceState) | ||
| 63 | enterTransition = MaterialSharedAxis(MaterialSharedAxis.Y, true) | ||
| 64 | returnTransition = MaterialSharedAxis(MaterialSharedAxis.Y, false) | ||
| 65 | reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) | ||
| 66 | } | ||
| 67 | |||
| 68 | override fun onCreateView( | ||
| 69 | inflater: LayoutInflater, | ||
| 70 | container: ViewGroup?, | ||
| 71 | savedInstanceState: Bundle? | ||
| 72 | ): View { | ||
| 73 | _binding = FragmentGamePropertiesBinding.inflate(layoutInflater) | ||
| 74 | return binding.root | ||
| 75 | } | ||
| 76 | |||
| 77 | // This is using the correct scope, lint is just acting up | ||
| 78 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") | ||
| 79 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| 80 | super.onViewCreated(view, savedInstanceState) | ||
| 81 | homeViewModel.setNavigationVisibility(visible = false, animated = true) | ||
| 82 | homeViewModel.setStatusBarShadeVisibility(true) | ||
| 83 | |||
| 84 | binding.buttonBack.setOnClickListener { | ||
| 85 | view.findNavController().popBackStack() | ||
| 86 | } | ||
| 87 | |||
| 88 | GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen) | ||
| 89 | binding.title.text = args.game.title | ||
| 90 | binding.title.postDelayed( | ||
| 91 | { | ||
| 92 | binding.title.ellipsize = TextUtils.TruncateAt.MARQUEE | ||
| 93 | binding.title.isSelected = true | ||
| 94 | }, | ||
| 95 | 3000 | ||
| 96 | ) | ||
| 97 | |||
| 98 | binding.buttonStart.setOnClickListener { | ||
| 99 | LaunchGameDialogFragment.newInstance(args.game) | ||
| 100 | .show(childFragmentManager, LaunchGameDialogFragment.TAG) | ||
| 101 | } | ||
| 102 | |||
| 103 | reloadList() | ||
| 104 | |||
| 105 | viewLifecycleOwner.lifecycleScope.apply { | ||
| 106 | launch { | ||
| 107 | repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| 108 | homeViewModel.openImportSaves.collect { | ||
| 109 | if (it) { | ||
| 110 | importSaves.launch(arrayOf("application/zip")) | ||
| 111 | homeViewModel.setOpenImportSaves(false) | ||
| 112 | } | ||
| 113 | } | ||
| 114 | } | ||
| 115 | } | ||
| 116 | launch { | ||
| 117 | repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| 118 | homeViewModel.reloadPropertiesList.collect { | ||
| 119 | if (it) { | ||
| 120 | reloadList() | ||
| 121 | homeViewModel.reloadPropertiesList(false) | ||
| 122 | } | ||
| 123 | } | ||
| 124 | } | ||
| 125 | } | ||
| 126 | } | ||
| 127 | |||
| 128 | setInsets() | ||
| 129 | } | ||
| 130 | |||
| 131 | override fun onDestroy() { | ||
| 132 | super.onDestroy() | ||
| 133 | gamesViewModel.reloadGames(true) | ||
| 134 | } | ||
| 135 | |||
| 136 | private fun reloadList() { | ||
| 137 | _binding ?: return | ||
| 138 | |||
| 139 | driverViewModel.updateDriverNameForGame(args.game) | ||
| 140 | val properties = mutableListOf<GameProperty>().apply { | ||
| 141 | add( | ||
| 142 | SubmenuProperty( | ||
| 143 | R.string.info, | ||
| 144 | R.string.info_description, | ||
| 145 | R.drawable.ic_info_outline | ||
| 146 | ) { | ||
| 147 | val action = GamePropertiesFragmentDirections | ||
| 148 | .actionPerGamePropertiesFragmentToGameInfoFragment(args.game) | ||
| 149 | binding.root.findNavController().navigate(action) | ||
| 150 | } | ||
| 151 | ) | ||
| 152 | add( | ||
| 153 | SubmenuProperty( | ||
| 154 | R.string.preferences_settings, | ||
| 155 | R.string.per_game_settings_description, | ||
| 156 | R.drawable.ic_settings | ||
| 157 | ) { | ||
| 158 | val action = HomeNavigationDirections.actionGlobalSettingsActivity( | ||
| 159 | args.game, | ||
| 160 | Settings.MenuTag.SECTION_ROOT | ||
| 161 | ) | ||
| 162 | binding.root.findNavController().navigate(action) | ||
| 163 | } | ||
| 164 | ) | ||
| 165 | |||
| 166 | if (GpuDriverHelper.supportsCustomDriverLoading()) { | ||
| 167 | add( | ||
| 168 | SubmenuProperty( | ||
| 169 | R.string.gpu_driver_manager, | ||
| 170 | R.string.install_gpu_driver_description, | ||
| 171 | R.drawable.ic_build, | ||
| 172 | detailsFlow = driverViewModel.selectedDriverTitle | ||
| 173 | ) { | ||
| 174 | val action = GamePropertiesFragmentDirections | ||
| 175 | .actionPerGamePropertiesFragmentToDriverManagerFragment(args.game) | ||
| 176 | binding.root.findNavController().navigate(action) | ||
| 177 | } | ||
| 178 | ) | ||
| 179 | } | ||
| 180 | |||
| 181 | if (!args.game.isHomebrew) { | ||
| 182 | add( | ||
| 183 | SubmenuProperty( | ||
| 184 | R.string.add_ons, | ||
| 185 | R.string.add_ons_description, | ||
| 186 | R.drawable.ic_edit | ||
| 187 | ) { | ||
| 188 | val action = GamePropertiesFragmentDirections | ||
| 189 | .actionPerGamePropertiesFragmentToAddonsFragment(args.game) | ||
| 190 | binding.root.findNavController().navigate(action) | ||
| 191 | } | ||
| 192 | ) | ||
| 193 | add( | ||
| 194 | InstallableProperty( | ||
| 195 | R.string.save_data, | ||
| 196 | R.string.save_data_description, | ||
| 197 | R.drawable.ic_save, | ||
| 198 | { | ||
| 199 | MessageDialogFragment.newInstance( | ||
| 200 | requireActivity(), | ||
| 201 | titleId = R.string.import_save_warning, | ||
| 202 | descriptionId = R.string.import_save_warning_description, | ||
| 203 | positiveAction = { homeViewModel.setOpenImportSaves(true) } | ||
| 204 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 205 | }, | ||
| 206 | if (File(args.game.saveDir).exists()) { | ||
| 207 | { exportSaves.launch(args.game.saveZipName) } | ||
| 208 | } else { | ||
| 209 | null | ||
| 210 | } | ||
| 211 | ) | ||
| 212 | ) | ||
| 213 | |||
| 214 | val saveDirFile = File(args.game.saveDir) | ||
| 215 | if (saveDirFile.exists()) { | ||
| 216 | add( | ||
| 217 | SubmenuProperty( | ||
| 218 | R.string.delete_save_data, | ||
| 219 | R.string.delete_save_data_description, | ||
| 220 | R.drawable.ic_delete, | ||
| 221 | action = { | ||
| 222 | MessageDialogFragment.newInstance( | ||
| 223 | requireActivity(), | ||
| 224 | titleId = R.string.delete_save_data, | ||
| 225 | descriptionId = R.string.delete_save_data_warning_description, | ||
| 226 | positiveAction = { | ||
| 227 | File(args.game.saveDir).deleteRecursively() | ||
| 228 | Toast.makeText( | ||
| 229 | YuzuApplication.appContext, | ||
| 230 | R.string.save_data_deleted_successfully, | ||
| 231 | Toast.LENGTH_SHORT | ||
| 232 | ).show() | ||
| 233 | homeViewModel.reloadPropertiesList(true) | ||
| 234 | } | ||
| 235 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 236 | } | ||
| 237 | ) | ||
| 238 | ) | ||
| 239 | } | ||
| 240 | |||
| 241 | val shaderCacheDir = File( | ||
| 242 | DirectoryInitialization.userDirectory + | ||
| 243 | "/shader/" + args.game.settingsName.lowercase() | ||
| 244 | ) | ||
| 245 | if (shaderCacheDir.exists()) { | ||
| 246 | add( | ||
| 247 | SubmenuProperty( | ||
| 248 | R.string.clear_shader_cache, | ||
| 249 | R.string.clear_shader_cache_description, | ||
| 250 | R.drawable.ic_delete, | ||
| 251 | { | ||
| 252 | if (shaderCacheDir.exists()) { | ||
| 253 | val bytes = shaderCacheDir.walkTopDown().filter { it.isFile } | ||
| 254 | .map { it.length() }.sum() | ||
| 255 | MemoryUtil.bytesToSizeUnit(bytes.toFloat()) | ||
| 256 | } else { | ||
| 257 | MemoryUtil.bytesToSizeUnit(0f) | ||
| 258 | } | ||
| 259 | } | ||
| 260 | ) { | ||
| 261 | MessageDialogFragment.newInstance( | ||
| 262 | requireActivity(), | ||
| 263 | titleId = R.string.clear_shader_cache, | ||
| 264 | descriptionId = R.string.clear_shader_cache_warning_description, | ||
| 265 | positiveAction = { | ||
| 266 | shaderCacheDir.deleteRecursively() | ||
| 267 | Toast.makeText( | ||
| 268 | YuzuApplication.appContext, | ||
| 269 | R.string.cleared_shaders_successfully, | ||
| 270 | Toast.LENGTH_SHORT | ||
| 271 | ).show() | ||
| 272 | homeViewModel.reloadPropertiesList(true) | ||
| 273 | } | ||
| 274 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 275 | } | ||
| 276 | ) | ||
| 277 | } | ||
| 278 | } | ||
| 279 | } | ||
| 280 | binding.listProperties.apply { | ||
| 281 | layoutManager = | ||
| 282 | GridLayoutManager(requireContext(), resources.getInteger(R.integer.grid_columns)) | ||
| 283 | adapter = GamePropertiesAdapter(viewLifecycleOwner, properties) | ||
| 284 | } | ||
| 285 | } | ||
| 286 | |||
| 287 | override fun onResume() { | ||
| 288 | super.onResume() | ||
| 289 | driverViewModel.updateDriverNameForGame(args.game) | ||
| 290 | } | ||
| 291 | |||
| 292 | private fun setInsets() = | ||
| 293 | ViewCompat.setOnApplyWindowInsetsListener( | ||
| 294 | binding.root | ||
| 295 | ) { _: View, windowInsets: WindowInsetsCompat -> | ||
| 296 | val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) | ||
| 297 | val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) | ||
| 298 | |||
| 299 | val leftInsets = barInsets.left + cutoutInsets.left | ||
| 300 | val rightInsets = barInsets.right + cutoutInsets.right | ||
| 301 | |||
| 302 | val smallLayout = resources.getBoolean(R.bool.small_layout) | ||
| 303 | if (smallLayout) { | ||
| 304 | val mlpListAll = | ||
| 305 | binding.listAll.layoutParams as ViewGroup.MarginLayoutParams | ||
| 306 | mlpListAll.leftMargin = leftInsets | ||
| 307 | mlpListAll.rightMargin = rightInsets | ||
| 308 | binding.listAll.layoutParams = mlpListAll | ||
| 309 | } else { | ||
| 310 | if (ViewCompat.getLayoutDirection(binding.root) == | ||
| 311 | ViewCompat.LAYOUT_DIRECTION_LTR | ||
| 312 | ) { | ||
| 313 | val mlpListAll = | ||
| 314 | binding.listAll.layoutParams as ViewGroup.MarginLayoutParams | ||
| 315 | mlpListAll.rightMargin = rightInsets | ||
| 316 | binding.listAll.layoutParams = mlpListAll | ||
| 317 | |||
| 318 | val mlpIconLayout = | ||
| 319 | binding.iconLayout!!.layoutParams as ViewGroup.MarginLayoutParams | ||
| 320 | mlpIconLayout.topMargin = barInsets.top | ||
| 321 | mlpIconLayout.leftMargin = leftInsets | ||
| 322 | binding.iconLayout!!.layoutParams = mlpIconLayout | ||
| 323 | } else { | ||
| 324 | val mlpListAll = | ||
| 325 | binding.listAll.layoutParams as ViewGroup.MarginLayoutParams | ||
| 326 | mlpListAll.leftMargin = leftInsets | ||
| 327 | binding.listAll.layoutParams = mlpListAll | ||
| 328 | |||
| 329 | val mlpIconLayout = | ||
| 330 | binding.iconLayout!!.layoutParams as ViewGroup.MarginLayoutParams | ||
| 331 | mlpIconLayout.topMargin = barInsets.top | ||
| 332 | mlpIconLayout.rightMargin = rightInsets | ||
| 333 | binding.iconLayout!!.layoutParams = mlpIconLayout | ||
| 334 | } | ||
| 335 | } | ||
| 336 | |||
| 337 | val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab) | ||
| 338 | val mlpFab = | ||
| 339 | binding.buttonStart.layoutParams as ViewGroup.MarginLayoutParams | ||
| 340 | mlpFab.leftMargin = leftInsets + fabSpacing | ||
| 341 | mlpFab.rightMargin = rightInsets + fabSpacing | ||
| 342 | mlpFab.bottomMargin = barInsets.bottom + fabSpacing | ||
| 343 | binding.buttonStart.layoutParams = mlpFab | ||
| 344 | |||
| 345 | binding.layoutAll.updatePadding( | ||
| 346 | top = barInsets.top, | ||
| 347 | bottom = barInsets.bottom + | ||
| 348 | resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab) | ||
| 349 | ) | ||
| 350 | |||
| 351 | windowInsets | ||
| 352 | } | ||
| 353 | |||
| 354 | private val importSaves = | ||
| 355 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> | ||
| 356 | if (result == null) { | ||
| 357 | return@registerForActivityResult | ||
| 358 | } | ||
| 359 | |||
| 360 | val inputZip = requireContext().contentResolver.openInputStream(result) | ||
| 361 | val savesFolder = File(args.game.saveDir) | ||
| 362 | val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") | ||
| 363 | cacheSaveDir.mkdir() | ||
| 364 | |||
| 365 | if (inputZip == null) { | ||
| 366 | Toast.makeText( | ||
| 367 | YuzuApplication.appContext, | ||
| 368 | getString(R.string.fatal_error), | ||
| 369 | Toast.LENGTH_LONG | ||
| 370 | ).show() | ||
| 371 | return@registerForActivityResult | ||
| 372 | } | ||
| 373 | |||
| 374 | IndeterminateProgressDialogFragment.newInstance( | ||
| 375 | requireActivity(), | ||
| 376 | R.string.save_files_importing, | ||
| 377 | false | ||
| 378 | ) { | ||
| 379 | try { | ||
| 380 | FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) | ||
| 381 | val files = cacheSaveDir.listFiles() | ||
| 382 | var savesFolderFile: File? = null | ||
| 383 | if (files != null) { | ||
| 384 | val savesFolderName = args.game.programIdHex | ||
| 385 | for (file in files) { | ||
| 386 | if (file.isDirectory && file.name == savesFolderName) { | ||
| 387 | savesFolderFile = file | ||
| 388 | break | ||
| 389 | } | ||
| 390 | } | ||
| 391 | } | ||
| 392 | |||
| 393 | if (savesFolderFile != null) { | ||
| 394 | savesFolder.deleteRecursively() | ||
| 395 | savesFolder.mkdir() | ||
| 396 | savesFolderFile.copyRecursively(savesFolder) | ||
| 397 | savesFolderFile.deleteRecursively() | ||
| 398 | } | ||
| 399 | |||
| 400 | withContext(Dispatchers.Main) { | ||
| 401 | if (savesFolderFile == null) { | ||
| 402 | MessageDialogFragment.newInstance( | ||
| 403 | requireActivity(), | ||
| 404 | titleId = R.string.save_file_invalid_zip_structure, | ||
| 405 | descriptionId = R.string.save_file_invalid_zip_structure_description | ||
| 406 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 407 | return@withContext | ||
| 408 | } | ||
| 409 | Toast.makeText( | ||
| 410 | YuzuApplication.appContext, | ||
| 411 | getString(R.string.save_file_imported_success), | ||
| 412 | Toast.LENGTH_LONG | ||
| 413 | ).show() | ||
| 414 | homeViewModel.reloadPropertiesList(true) | ||
| 415 | } | ||
| 416 | |||
| 417 | cacheSaveDir.deleteRecursively() | ||
| 418 | } catch (e: Exception) { | ||
| 419 | Toast.makeText( | ||
| 420 | YuzuApplication.appContext, | ||
| 421 | getString(R.string.fatal_error), | ||
| 422 | Toast.LENGTH_LONG | ||
| 423 | ).show() | ||
| 424 | } | ||
| 425 | }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) | ||
| 426 | } | ||
| 427 | |||
| 428 | /** | ||
| 429 | * Exports the save file located in the given folder path by creating a zip file and opening a | ||
| 430 | * file picker to save. | ||
| 431 | */ | ||
| 432 | private val exportSaves = registerForActivityResult( | ||
| 433 | ActivityResultContracts.CreateDocument("application/zip") | ||
| 434 | ) { result -> | ||
| 435 | if (result == null) { | ||
| 436 | return@registerForActivityResult | ||
| 437 | } | ||
| 438 | |||
| 439 | IndeterminateProgressDialogFragment.newInstance( | ||
| 440 | requireActivity(), | ||
| 441 | R.string.save_files_exporting, | ||
| 442 | false | ||
| 443 | ) { | ||
| 444 | val saveLocation = args.game.saveDir | ||
| 445 | val zipResult = FileUtil.zipFromInternalStorage( | ||
| 446 | File(saveLocation), | ||
| 447 | saveLocation.replaceAfterLast("/", ""), | ||
| 448 | BufferedOutputStream(requireContext().contentResolver.openOutputStream(result)) | ||
| 449 | ) | ||
| 450 | return@newInstance when (zipResult) { | ||
| 451 | TaskState.Completed -> getString(R.string.export_success) | ||
| 452 | TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) | ||
| 453 | } | ||
| 454 | }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) | ||
| 455 | } | ||
| 456 | } | ||
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 3addc2e63..6ddd758e6 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 | |||
| @@ -68,6 +68,9 @@ class HomeSettingsFragment : Fragment() { | |||
| 68 | } | 68 | } |
| 69 | 69 | ||
| 70 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 70 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 71 | super.onViewCreated(view, savedInstanceState) | ||
| 72 | homeViewModel.setNavigationVisibility(visible = true, animated = true) | ||
| 73 | homeViewModel.setStatusBarShadeVisibility(visible = true) | ||
| 71 | mainActivity = requireActivity() as MainActivity | 74 | mainActivity = requireActivity() as MainActivity |
| 72 | 75 | ||
| 73 | val optionsList: MutableList<HomeSetting> = mutableListOf<HomeSetting>().apply { | 76 | val optionsList: MutableList<HomeSetting> = mutableListOf<HomeSetting>().apply { |
| @@ -91,13 +94,14 @@ class HomeSettingsFragment : Fragment() { | |||
| 91 | R.string.install_gpu_driver_description, | 94 | R.string.install_gpu_driver_description, |
| 92 | R.drawable.ic_build, | 95 | R.drawable.ic_build, |
| 93 | { | 96 | { |
| 94 | binding.root.findNavController() | 97 | val action = HomeSettingsFragmentDirections |
| 95 | .navigate(R.id.action_homeSettingsFragment_to_driverManagerFragment) | 98 | .actionHomeSettingsFragmentToDriverManagerFragment(null) |
| 99 | binding.root.findNavController().navigate(action) | ||
| 96 | }, | 100 | }, |
| 97 | { GpuDriverHelper.supportsCustomDriverLoading() }, | 101 | { GpuDriverHelper.supportsCustomDriverLoading() }, |
| 98 | R.string.custom_driver_not_supported, | 102 | R.string.custom_driver_not_supported, |
| 99 | R.string.custom_driver_not_supported_description, | 103 | R.string.custom_driver_not_supported_description, |
| 100 | driverViewModel.selectedDriverMetadata | 104 | driverViewModel.selectedDriverTitle |
| 101 | ) | 105 | ) |
| 102 | ) | 106 | ) |
| 103 | add( | 107 | add( |
| @@ -212,8 +216,11 @@ class HomeSettingsFragment : Fragment() { | |||
| 212 | override fun onStart() { | 216 | override fun onStart() { |
| 213 | super.onStart() | 217 | super.onStart() |
| 214 | exitTransition = null | 218 | exitTransition = null |
| 215 | homeViewModel.setNavigationVisibility(visible = true, animated = true) | 219 | } |
| 216 | homeViewModel.setStatusBarShadeVisibility(visible = true) | 220 | |
| 221 | override fun onResume() { | ||
| 222 | super.onResume() | ||
| 223 | driverViewModel.updateDriverNameForGame(null) | ||
| 217 | } | 224 | } |
| 218 | 225 | ||
| 219 | override fun onDestroyView() { | 226 | override fun onDestroyView() { |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt index 7e467814d..8847e5531 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt | |||
| @@ -122,7 +122,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() { | |||
| 122 | activity: FragmentActivity, | 122 | activity: FragmentActivity, |
| 123 | titleId: Int, | 123 | titleId: Int, |
| 124 | cancellable: Boolean = false, | 124 | cancellable: Boolean = false, |
| 125 | task: () -> Any | 125 | task: suspend () -> Any |
| 126 | ): IndeterminateProgressDialogFragment { | 126 | ): IndeterminateProgressDialogFragment { |
| 127 | val dialog = IndeterminateProgressDialogFragment() | 127 | val dialog = IndeterminateProgressDialogFragment() |
| 128 | val args = Bundle() | 128 | val args = Bundle() |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt index 6940fc757..569727b90 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt | |||
| @@ -21,8 +21,6 @@ import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding | |||
| 21 | import org.yuzu.yuzu_emu.model.HomeViewModel | 21 | import org.yuzu.yuzu_emu.model.HomeViewModel |
| 22 | import org.yuzu.yuzu_emu.model.Installable | 22 | import org.yuzu.yuzu_emu.model.Installable |
| 23 | import org.yuzu.yuzu_emu.ui.main.MainActivity | 23 | import org.yuzu.yuzu_emu.ui.main.MainActivity |
| 24 | import java.time.LocalDateTime | ||
| 25 | import java.time.format.DateTimeFormatter | ||
| 26 | 24 | ||
| 27 | class InstallableFragment : Fragment() { | 25 | class InstallableFragment : Fragment() { |
| 28 | private var _binding: FragmentInstallablesBinding? = null | 26 | private var _binding: FragmentInstallablesBinding? = null |
| @@ -75,28 +73,6 @@ class InstallableFragment : Fragment() { | |||
| 75 | R.string.install_firmware_description, | 73 | R.string.install_firmware_description, |
| 76 | install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) } | 74 | install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) } |
| 77 | ), | 75 | ), |
| 78 | if (mainActivity.savesFolderRoot != "") { | ||
| 79 | Installable( | ||
| 80 | R.string.manage_save_data, | ||
| 81 | R.string.import_export_saves_description, | ||
| 82 | install = { mainActivity.importSaves.launch(arrayOf("application/zip")) }, | ||
| 83 | export = { | ||
| 84 | mainActivity.exportSaves.launch( | ||
| 85 | "yuzu saves - ${ | ||
| 86 | LocalDateTime.now().format( | ||
| 87 | DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") | ||
| 88 | ) | ||
| 89 | }.zip" | ||
| 90 | ) | ||
| 91 | } | ||
| 92 | ) | ||
| 93 | } else { | ||
| 94 | Installable( | ||
| 95 | R.string.manage_save_data, | ||
| 96 | R.string.import_export_saves_description, | ||
| 97 | install = { mainActivity.importSaves.launch(arrayOf("application/zip")) } | ||
| 98 | ) | ||
| 99 | }, | ||
| 100 | Installable( | 76 | Installable( |
| 101 | R.string.install_prod_keys, | 77 | R.string.install_prod_keys, |
| 102 | R.string.install_prod_keys_description, | 78 | R.string.install_prod_keys_description, |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt new file mode 100644 index 000000000..e1ac46c48 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt | |||
| @@ -0,0 +1,61 @@ | |||
| 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.navigation.fragment.findNavController | ||
| 11 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||
| 12 | import org.yuzu.yuzu_emu.HomeNavigationDirections | ||
| 13 | import org.yuzu.yuzu_emu.R | ||
| 14 | import org.yuzu.yuzu_emu.model.Game | ||
| 15 | import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable | ||
| 16 | |||
| 17 | class LaunchGameDialogFragment : DialogFragment() { | ||
| 18 | private var selectedItem = 1 | ||
| 19 | |||
| 20 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | ||
| 21 | val game = requireArguments().parcelable<Game>(GAME) | ||
| 22 | val launchOptions = arrayOf(getString(R.string.global), getString(R.string.custom)) | ||
| 23 | |||
| 24 | if (savedInstanceState != null) { | ||
| 25 | selectedItem = savedInstanceState.getInt(SELECTED_ITEM) | ||
| 26 | } | ||
| 27 | |||
| 28 | return MaterialAlertDialogBuilder(requireContext()) | ||
| 29 | .setTitle(R.string.launch_options) | ||
| 30 | .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> | ||
| 31 | val action = HomeNavigationDirections | ||
| 32 | .actionGlobalEmulationActivity(game, selectedItem != 0) | ||
| 33 | requireParentFragment().findNavController().navigate(action) | ||
| 34 | } | ||
| 35 | .setSingleChoiceItems(launchOptions, 1) { _: DialogInterface, i: Int -> | ||
| 36 | selectedItem = i | ||
| 37 | } | ||
| 38 | .setNegativeButton(android.R.string.cancel, null) | ||
| 39 | .show() | ||
| 40 | } | ||
| 41 | |||
| 42 | override fun onSaveInstanceState(outState: Bundle) { | ||
| 43 | super.onSaveInstanceState(outState) | ||
| 44 | outState.putInt(SELECTED_ITEM, selectedItem) | ||
| 45 | } | ||
| 46 | |||
| 47 | companion object { | ||
| 48 | const val TAG = "LaunchGameDialogFragment" | ||
| 49 | |||
| 50 | const val GAME = "Game" | ||
| 51 | const val SELECTED_ITEM = "SelectedItem" | ||
| 52 | |||
| 53 | fun newInstance(game: Game): LaunchGameDialogFragment { | ||
| 54 | val args = Bundle() | ||
| 55 | args.putParcelable(GAME, game) | ||
| 56 | val fragment = LaunchGameDialogFragment() | ||
| 57 | fragment.arguments = args | ||
| 58 | return fragment | ||
| 59 | } | ||
| 60 | } | ||
| 61 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt index a6183d19e..32062b6fe 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt | |||
| @@ -27,30 +27,31 @@ class MessageDialogFragment : DialogFragment() { | |||
| 27 | val descriptionString = requireArguments().getString(DESCRIPTION_STRING)!! | 27 | val descriptionString = requireArguments().getString(DESCRIPTION_STRING)!! |
| 28 | val helpLinkId = requireArguments().getInt(HELP_LINK) | 28 | val helpLinkId = requireArguments().getInt(HELP_LINK) |
| 29 | 29 | ||
| 30 | val dialog = MaterialAlertDialogBuilder(requireContext()) | 30 | val builder = MaterialAlertDialogBuilder(requireContext()) |
| 31 | .setPositiveButton(R.string.close, null) | ||
| 32 | 31 | ||
| 33 | if (titleId != 0) dialog.setTitle(titleId) | 32 | if (messageDialogViewModel.positiveAction == null) { |
| 34 | if (titleString.isNotEmpty()) dialog.setTitle(titleString) | 33 | builder.setPositiveButton(R.string.close, null) |
| 34 | } else { | ||
| 35 | builder.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> | ||
| 36 | messageDialogViewModel.positiveAction?.invoke() | ||
| 37 | }.setNegativeButton(android.R.string.cancel, null) | ||
| 38 | } | ||
| 39 | |||
| 40 | if (titleId != 0) builder.setTitle(titleId) | ||
| 41 | if (titleString.isNotEmpty()) builder.setTitle(titleString) | ||
| 35 | 42 | ||
| 36 | if (descriptionId != 0) { | 43 | if (descriptionId != 0) { |
| 37 | dialog.setMessage(Html.fromHtml(getString(descriptionId), Html.FROM_HTML_MODE_LEGACY)) | 44 | builder.setMessage(Html.fromHtml(getString(descriptionId), Html.FROM_HTML_MODE_LEGACY)) |
| 38 | } | 45 | } |
| 39 | if (descriptionString.isNotEmpty()) dialog.setMessage(descriptionString) | 46 | if (descriptionString.isNotEmpty()) builder.setMessage(descriptionString) |
| 40 | 47 | ||
| 41 | if (helpLinkId != 0) { | 48 | if (helpLinkId != 0) { |
| 42 | dialog.setNeutralButton(R.string.learn_more) { _, _ -> | 49 | builder.setNeutralButton(R.string.learn_more) { _, _ -> |
| 43 | openLink(getString(helpLinkId)) | 50 | openLink(getString(helpLinkId)) |
| 44 | } | 51 | } |
| 45 | } | 52 | } |
| 46 | 53 | ||
| 47 | return dialog.show() | 54 | return builder.show() |
| 48 | } | ||
| 49 | |||
| 50 | override fun onDismiss(dialog: DialogInterface) { | ||
| 51 | super.onDismiss(dialog) | ||
| 52 | messageDialogViewModel.dismissAction.invoke() | ||
| 53 | messageDialogViewModel.clear() | ||
| 54 | } | 55 | } |
| 55 | 56 | ||
| 56 | private fun openLink(link: String) { | 57 | private fun openLink(link: String) { |
| @@ -74,7 +75,7 @@ class MessageDialogFragment : DialogFragment() { | |||
| 74 | descriptionId: Int = 0, | 75 | descriptionId: Int = 0, |
| 75 | descriptionString: String = "", | 76 | descriptionString: String = "", |
| 76 | helpLinkId: Int = 0, | 77 | helpLinkId: Int = 0, |
| 77 | dismissAction: () -> Unit = {} | 78 | positiveAction: (() -> Unit)? = null |
| 78 | ): MessageDialogFragment { | 79 | ): MessageDialogFragment { |
| 79 | val dialog = MessageDialogFragment() | 80 | val dialog = MessageDialogFragment() |
| 80 | val bundle = Bundle() | 81 | val bundle = Bundle() |
| @@ -85,8 +86,10 @@ class MessageDialogFragment : DialogFragment() { | |||
| 85 | putString(DESCRIPTION_STRING, descriptionString) | 86 | putString(DESCRIPTION_STRING, descriptionString) |
| 86 | putInt(HELP_LINK, helpLinkId) | 87 | putInt(HELP_LINK, helpLinkId) |
| 87 | } | 88 | } |
| 88 | ViewModelProvider(activity)[MessageDialogViewModel::class.java].dismissAction = | 89 | ViewModelProvider(activity)[MessageDialogViewModel::class.java].apply { |
| 89 | dismissAction | 90 | clear() |
| 91 | this.positiveAction = positiveAction | ||
| 92 | } | ||
| 90 | dialog.arguments = bundle | 93 | dialog.arguments = bundle |
| 91 | return dialog | 94 | return dialog |
| 92 | } | 95 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt index 2dbca76a5..64b295fbd 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt | |||
| @@ -24,6 +24,7 @@ import androidx.lifecycle.repeatOnLifecycle | |||
| 24 | import androidx.preference.PreferenceManager | 24 | import androidx.preference.PreferenceManager |
| 25 | import info.debatty.java.stringsimilarity.Jaccard | 25 | import info.debatty.java.stringsimilarity.Jaccard |
| 26 | import info.debatty.java.stringsimilarity.JaroWinkler | 26 | import info.debatty.java.stringsimilarity.JaroWinkler |
| 27 | import kotlinx.coroutines.flow.collectLatest | ||
| 27 | import kotlinx.coroutines.launch | 28 | import kotlinx.coroutines.launch |
| 28 | import java.util.Locale | 29 | import java.util.Locale |
| 29 | import org.yuzu.yuzu_emu.R | 30 | import org.yuzu.yuzu_emu.R |
| @@ -60,7 +61,9 @@ class SearchFragment : Fragment() { | |||
| 60 | // This is using the correct scope, lint is just acting up | 61 | // This is using the correct scope, lint is just acting up |
| 61 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") | 62 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") |
| 62 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 63 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 63 | homeViewModel.setNavigationVisibility(visible = true, animated = false) | 64 | super.onViewCreated(view, savedInstanceState) |
| 65 | homeViewModel.setNavigationVisibility(visible = true, animated = true) | ||
| 66 | homeViewModel.setStatusBarShadeVisibility(true) | ||
| 64 | preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) | 67 | preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) |
| 65 | 68 | ||
| 66 | if (savedInstanceState != null) { | 69 | if (savedInstanceState != null) { |
| @@ -99,7 +102,7 @@ class SearchFragment : Fragment() { | |||
| 99 | } | 102 | } |
| 100 | launch { | 103 | launch { |
| 101 | repeatOnLifecycle(Lifecycle.State.CREATED) { | 104 | repeatOnLifecycle(Lifecycle.State.CREATED) { |
| 102 | gamesViewModel.games.collect { filterAndSearch() } | 105 | gamesViewModel.games.collectLatest { filterAndSearch() } |
| 103 | } | 106 | } |
| 104 | } | 107 | } |
| 105 | launch { | 108 | launch { |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt index b88d2c038..60e029f34 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt | |||
| @@ -70,7 +70,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener | |||
| 70 | sliderBinding = DialogSliderBinding.inflate(layoutInflater) | 70 | sliderBinding = DialogSliderBinding.inflate(layoutInflater) |
| 71 | val item = settingsViewModel.clickedItem as SliderSetting | 71 | val item = settingsViewModel.clickedItem as SliderSetting |
| 72 | 72 | ||
| 73 | settingsViewModel.setSliderTextValue(item.selectedValue.toFloat(), item.units) | 73 | settingsViewModel.setSliderTextValue(item.getSelectedValue().toFloat(), item.units) |
| 74 | sliderBinding.slider.apply { | 74 | sliderBinding.slider.apply { |
| 75 | valueFrom = item.min.toFloat() | 75 | valueFrom = item.min.toFloat() |
| 76 | valueTo = item.max.toFloat() | 76 | valueTo = item.max.toFloat() |
| @@ -136,18 +136,18 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener | |||
| 136 | is SingleChoiceSetting -> { | 136 | is SingleChoiceSetting -> { |
| 137 | val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting | 137 | val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting |
| 138 | val value = getValueForSingleChoiceSelection(scSetting, which) | 138 | val value = getValueForSingleChoiceSelection(scSetting, which) |
| 139 | scSetting.selectedValue = value | 139 | scSetting.setSelectedValue(value) |
| 140 | } | 140 | } |
| 141 | 141 | ||
| 142 | is StringSingleChoiceSetting -> { | 142 | is StringSingleChoiceSetting -> { |
| 143 | val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting | 143 | val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting |
| 144 | val value = scSetting.getValueAt(which) | 144 | val value = scSetting.getValueAt(which) |
| 145 | scSetting.selectedValue = value | 145 | scSetting.setSelectedValue(value) |
| 146 | } | 146 | } |
| 147 | 147 | ||
| 148 | is SliderSetting -> { | 148 | is SliderSetting -> { |
| 149 | val sliderSetting = settingsViewModel.clickedItem as SliderSetting | 149 | val sliderSetting = settingsViewModel.clickedItem as SliderSetting |
| 150 | sliderSetting.selectedValue = settingsViewModel.sliderProgress.value | 150 | sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value) |
| 151 | } | 151 | } |
| 152 | } | 152 | } |
| 153 | closeDialog() | 153 | closeDialog() |
| @@ -171,7 +171,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener | |||
| 171 | } | 171 | } |
| 172 | 172 | ||
| 173 | private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int { | 173 | private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int { |
| 174 | val value = item.selectedValue | 174 | val value = item.getSelectedValue() |
| 175 | val valuesId = item.valuesId | 175 | val valuesId = item.valuesId |
| 176 | if (valuesId > 0) { | 176 | if (valuesId > 0) { |
| 177 | val valuesArray = requireContext().resources.getIntArray(valuesId) | 177 | val valuesArray = requireContext().resources.getIntArray(valuesId) |
| @@ -211,7 +211,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener | |||
| 211 | throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!") | 211 | throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!") |
| 212 | 212 | ||
| 213 | SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress( | 213 | SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress( |
| 214 | (clickedItem as SliderSetting).selectedValue.toFloat() | 214 | (clickedItem as SliderSetting).getSelectedValue().toFloat() |
| 215 | ) | 215 | ) |
| 216 | } | 216 | } |
| 217 | settingsViewModel.clickedItem = clickedItem | 217 | settingsViewModel.clickedItem = clickedItem |
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 eb5edaa10..064342cdd 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 | |||
| @@ -304,6 +304,11 @@ class SetupFragment : Fragment() { | |||
| 304 | setInsets() | 304 | setInsets() |
| 305 | } | 305 | } |
| 306 | 306 | ||
| 307 | override fun onStop() { | ||
| 308 | super.onStop() | ||
| 309 | NativeConfig.saveGlobalConfig() | ||
| 310 | } | ||
| 311 | |||
| 307 | override fun onSaveInstanceState(outState: Bundle) { | 312 | override fun onSaveInstanceState(outState: Bundle) { |
| 308 | super.onSaveInstanceState(outState) | 313 | super.onSaveInstanceState(outState) |
| 309 | if (_binding != null) { | 314 | if (_binding != null) { |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt new file mode 100644 index 000000000..ed79a8b02 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt | |||
| @@ -0,0 +1,10 @@ | |||
| 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 | data class Addon( | ||
| 7 | var enabled: Boolean, | ||
| 8 | val title: String, | ||
| 9 | val version: String | ||
| 10 | ) | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt new file mode 100644 index 000000000..075252f5b --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt | |||
| @@ -0,0 +1,83 @@ | |||
| 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 androidx.lifecycle.ViewModel | ||
| 7 | import androidx.lifecycle.viewModelScope | ||
| 8 | import kotlinx.coroutines.Dispatchers | ||
| 9 | import kotlinx.coroutines.flow.MutableStateFlow | ||
| 10 | import kotlinx.coroutines.flow.asStateFlow | ||
| 11 | import kotlinx.coroutines.launch | ||
| 12 | import kotlinx.coroutines.withContext | ||
| 13 | import org.yuzu.yuzu_emu.NativeLibrary | ||
| 14 | import org.yuzu.yuzu_emu.utils.NativeConfig | ||
| 15 | import java.util.concurrent.atomic.AtomicBoolean | ||
| 16 | |||
| 17 | class AddonViewModel : ViewModel() { | ||
| 18 | private val _addonList = MutableStateFlow(mutableListOf<Addon>()) | ||
| 19 | val addonList get() = _addonList.asStateFlow() | ||
| 20 | |||
| 21 | private val _showModInstallPicker = MutableStateFlow(false) | ||
| 22 | val showModInstallPicker get() = _showModInstallPicker.asStateFlow() | ||
| 23 | |||
| 24 | private val _showModNoticeDialog = MutableStateFlow(false) | ||
| 25 | val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow() | ||
| 26 | |||
| 27 | var game: Game? = null | ||
| 28 | |||
| 29 | private val isRefreshing = AtomicBoolean(false) | ||
| 30 | |||
| 31 | fun onOpenAddons(game: Game) { | ||
| 32 | this.game = game | ||
| 33 | refreshAddons() | ||
| 34 | } | ||
| 35 | |||
| 36 | fun refreshAddons() { | ||
| 37 | if (isRefreshing.get() || game == null) { | ||
| 38 | return | ||
| 39 | } | ||
| 40 | isRefreshing.set(true) | ||
| 41 | viewModelScope.launch { | ||
| 42 | withContext(Dispatchers.IO) { | ||
| 43 | val addonList = mutableListOf<Addon>() | ||
| 44 | val disabledAddons = NativeConfig.getDisabledAddons(game!!.programId) | ||
| 45 | NativeLibrary.getAddonsForFile(game!!.path, game!!.programId)?.forEach { | ||
| 46 | val name = it.first.replace("[D] ", "") | ||
| 47 | addonList.add(Addon(!disabledAddons.contains(name), name, it.second)) | ||
| 48 | } | ||
| 49 | addonList.sortBy { it.title } | ||
| 50 | _addonList.value = addonList | ||
| 51 | isRefreshing.set(false) | ||
| 52 | } | ||
| 53 | } | ||
| 54 | } | ||
| 55 | |||
| 56 | fun onCloseAddons() { | ||
| 57 | if (_addonList.value.isEmpty()) { | ||
| 58 | return | ||
| 59 | } | ||
| 60 | |||
| 61 | NativeConfig.setDisabledAddons( | ||
| 62 | game!!.programId, | ||
| 63 | _addonList.value.mapNotNull { | ||
| 64 | if (it.enabled) { | ||
| 65 | null | ||
| 66 | } else { | ||
| 67 | it.title | ||
| 68 | } | ||
| 69 | }.toTypedArray() | ||
| 70 | ) | ||
| 71 | NativeConfig.saveGlobalConfig() | ||
| 72 | _addonList.value.clear() | ||
| 73 | game = null | ||
| 74 | } | ||
| 75 | |||
| 76 | fun showModInstallPicker(install: Boolean) { | ||
| 77 | _showModInstallPicker.value = install | ||
| 78 | } | ||
| 79 | |||
| 80 | fun showModNoticeDialog(show: Boolean) { | ||
| 81 | _showModNoticeDialog.value = show | ||
| 82 | } | ||
| 83 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt index 62945ad65..76accf8f3 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt | |||
| @@ -7,81 +7,83 @@ import androidx.lifecycle.ViewModel | |||
| 7 | import androidx.lifecycle.viewModelScope | 7 | import androidx.lifecycle.viewModelScope |
| 8 | import kotlinx.coroutines.Dispatchers | 8 | import kotlinx.coroutines.Dispatchers |
| 9 | import kotlinx.coroutines.flow.MutableStateFlow | 9 | import kotlinx.coroutines.flow.MutableStateFlow |
| 10 | import kotlinx.coroutines.flow.SharingStarted | ||
| 10 | import kotlinx.coroutines.flow.StateFlow | 11 | import kotlinx.coroutines.flow.StateFlow |
| 12 | import kotlinx.coroutines.flow.combine | ||
| 13 | import kotlinx.coroutines.flow.stateIn | ||
| 11 | import kotlinx.coroutines.launch | 14 | import kotlinx.coroutines.launch |
| 12 | import kotlinx.coroutines.withContext | 15 | import kotlinx.coroutines.withContext |
| 13 | import org.yuzu.yuzu_emu.R | 16 | import org.yuzu.yuzu_emu.R |
| 14 | import org.yuzu.yuzu_emu.YuzuApplication | 17 | import org.yuzu.yuzu_emu.YuzuApplication |
| 18 | import org.yuzu.yuzu_emu.features.settings.model.StringSetting | ||
| 19 | import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile | ||
| 15 | import org.yuzu.yuzu_emu.utils.FileUtil | 20 | import org.yuzu.yuzu_emu.utils.FileUtil |
| 16 | import org.yuzu.yuzu_emu.utils.GpuDriverHelper | 21 | import org.yuzu.yuzu_emu.utils.GpuDriverHelper |
| 17 | import org.yuzu.yuzu_emu.utils.GpuDriverMetadata | 22 | import org.yuzu.yuzu_emu.utils.GpuDriverMetadata |
| 23 | import org.yuzu.yuzu_emu.utils.NativeConfig | ||
| 18 | import java.io.BufferedOutputStream | 24 | import java.io.BufferedOutputStream |
| 19 | import java.io.File | 25 | import java.io.File |
| 20 | 26 | ||
| 21 | class DriverViewModel : ViewModel() { | 27 | class DriverViewModel : ViewModel() { |
| 22 | private val _areDriversLoading = MutableStateFlow(false) | 28 | private val _areDriversLoading = MutableStateFlow(false) |
| 23 | val areDriversLoading: StateFlow<Boolean> get() = _areDriversLoading | ||
| 24 | |||
| 25 | private val _isDriverReady = MutableStateFlow(true) | 29 | private val _isDriverReady = MutableStateFlow(true) |
| 26 | val isDriverReady: StateFlow<Boolean> get() = _isDriverReady | ||
| 27 | |||
| 28 | private val _isDeletingDrivers = MutableStateFlow(false) | 30 | private val _isDeletingDrivers = MutableStateFlow(false) |
| 29 | val isDeletingDrivers: StateFlow<Boolean> get() = _isDeletingDrivers | ||
| 30 | 31 | ||
| 31 | private val _driverList = MutableStateFlow(mutableListOf<Pair<String, GpuDriverMetadata>>()) | 32 | val isInteractionAllowed: StateFlow<Boolean> = |
| 33 | combine( | ||
| 34 | _areDriversLoading, | ||
| 35 | _isDriverReady, | ||
| 36 | _isDeletingDrivers | ||
| 37 | ) { loading, ready, deleting -> | ||
| 38 | !loading && ready && !deleting | ||
| 39 | }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = false) | ||
| 40 | |||
| 41 | private val _driverList = MutableStateFlow(GpuDriverHelper.getDrivers()) | ||
| 32 | val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList | 42 | val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList |
| 33 | 43 | ||
| 34 | var previouslySelectedDriver = 0 | 44 | var previouslySelectedDriver = 0 |
| 35 | var selectedDriver = -1 | 45 | var selectedDriver = -1 |
| 36 | 46 | ||
| 37 | private val _selectedDriverMetadata = | 47 | // Used for showing which driver is currently installed within the driver manager card |
| 38 | MutableStateFlow( | 48 | private val _selectedDriverTitle = MutableStateFlow("") |
| 39 | GpuDriverHelper.customDriverData.name | 49 | val selectedDriverTitle: StateFlow<String> get() = _selectedDriverTitle |
| 40 | ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) | ||
| 41 | ) | ||
| 42 | val selectedDriverMetadata: StateFlow<String> get() = _selectedDriverMetadata | ||
| 43 | 50 | ||
| 44 | private val _newDriverInstalled = MutableStateFlow(false) | 51 | private val _newDriverInstalled = MutableStateFlow(false) |
| 45 | val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled | 52 | val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled |
| 46 | 53 | ||
| 47 | val driversToDelete = mutableListOf<String>() | 54 | val driversToDelete = mutableListOf<String>() |
| 48 | 55 | ||
| 49 | val isInteractionAllowed | ||
| 50 | get() = !areDriversLoading.value && isDriverReady.value && !isDeletingDrivers.value | ||
| 51 | |||
| 52 | init { | 56 | init { |
| 53 | _areDriversLoading.value = true | 57 | val currentDriverMetadata = GpuDriverHelper.installedCustomDriverData |
| 54 | viewModelScope.launch { | 58 | findSelectedDriver(currentDriverMetadata) |
| 55 | withContext(Dispatchers.IO) { | 59 | |
| 56 | val drivers = GpuDriverHelper.getDrivers() | 60 | // If a user had installed a driver before the manager was implemented, this zips |
| 57 | val currentDriverMetadata = GpuDriverHelper.customDriverData | 61 | // the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can |
| 58 | for (i in drivers.indices) { | 62 | // be indexed and exported as expected. |
| 59 | if (drivers[i].second == currentDriverMetadata) { | 63 | if (selectedDriver == -1) { |
| 60 | setSelectedDriverIndex(i) | 64 | val driverToSave = |
| 61 | break | 65 | File(GpuDriverHelper.driverStoragePath, "CustomDriver.zip") |
| 62 | } | 66 | driverToSave.createNewFile() |
| 63 | } | 67 | FileUtil.zipFromInternalStorage( |
| 64 | 68 | File(GpuDriverHelper.driverInstallationPath!!), | |
| 65 | // If a user had installed a driver before the manager was implemented, this zips | 69 | GpuDriverHelper.driverInstallationPath!!, |
| 66 | // the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can | 70 | BufferedOutputStream(driverToSave.outputStream()) |
| 67 | // be indexed and exported as expected. | 71 | ) |
| 68 | if (selectedDriver == -1) { | 72 | _driverList.value.add(Pair(driverToSave.path, currentDriverMetadata)) |
| 69 | val driverToSave = | 73 | setSelectedDriverIndex(_driverList.value.size - 1) |
| 70 | File(GpuDriverHelper.driverStoragePath, "CustomDriver.zip") | 74 | } |
| 71 | driverToSave.createNewFile() | ||
| 72 | FileUtil.zipFromInternalStorage( | ||
| 73 | File(GpuDriverHelper.driverInstallationPath!!), | ||
| 74 | GpuDriverHelper.driverInstallationPath!!, | ||
| 75 | BufferedOutputStream(driverToSave.outputStream()) | ||
| 76 | ) | ||
| 77 | drivers.add(Pair(driverToSave.path, currentDriverMetadata)) | ||
| 78 | setSelectedDriverIndex(drivers.size - 1) | ||
| 79 | } | ||
| 80 | 75 | ||
| 81 | _driverList.value = drivers | 76 | // If a user had installed a driver before the config was reworked to be multiplatform, |
| 82 | _areDriversLoading.value = false | 77 | // we have save the path of the previously selected driver to the new setting. |
| 83 | } | 78 | if (StringSetting.DRIVER_PATH.getString(true).isEmpty() && selectedDriver > 0 && |
| 79 | StringSetting.DRIVER_PATH.global | ||
| 80 | ) { | ||
| 81 | StringSetting.DRIVER_PATH.setString(_driverList.value[selectedDriver].first) | ||
| 82 | NativeConfig.saveGlobalConfig() | ||
| 83 | } else { | ||
| 84 | findSelectedDriver(GpuDriverHelper.customDriverSettingData) | ||
| 84 | } | 85 | } |
| 86 | updateDriverNameForGame(null) | ||
| 85 | } | 87 | } |
| 86 | 88 | ||
| 87 | fun setSelectedDriverIndex(value: Int) { | 89 | fun setSelectedDriverIndex(value: Int) { |
| @@ -98,9 +100,9 @@ class DriverViewModel : ViewModel() { | |||
| 98 | fun addDriver(driverData: Pair<String, GpuDriverMetadata>) { | 100 | fun addDriver(driverData: Pair<String, GpuDriverMetadata>) { |
| 99 | val driverIndex = _driverList.value.indexOfFirst { it == driverData } | 101 | val driverIndex = _driverList.value.indexOfFirst { it == driverData } |
| 100 | if (driverIndex == -1) { | 102 | if (driverIndex == -1) { |
| 101 | setSelectedDriverIndex(_driverList.value.size) | ||
| 102 | _driverList.value.add(driverData) | 103 | _driverList.value.add(driverData) |
| 103 | _selectedDriverMetadata.value = driverData.second.name | 104 | setSelectedDriverIndex(_driverList.value.size - 1) |
| 105 | _selectedDriverTitle.value = driverData.second.name | ||
| 104 | ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) | 106 | ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) |
| 105 | } else { | 107 | } else { |
| 106 | setSelectedDriverIndex(driverIndex) | 108 | setSelectedDriverIndex(driverIndex) |
| @@ -111,8 +113,31 @@ class DriverViewModel : ViewModel() { | |||
| 111 | _driverList.value.remove(driverData) | 113 | _driverList.value.remove(driverData) |
| 112 | } | 114 | } |
| 113 | 115 | ||
| 114 | fun onCloseDriverManager() { | 116 | fun onOpenDriverManager(game: Game?) { |
| 117 | if (game != null) { | ||
| 118 | SettingsFile.loadCustomConfig(game) | ||
| 119 | } | ||
| 120 | |||
| 121 | val driverPath = StringSetting.DRIVER_PATH.getString() | ||
| 122 | if (driverPath.isEmpty()) { | ||
| 123 | setSelectedDriverIndex(0) | ||
| 124 | } else { | ||
| 125 | findSelectedDriver(GpuDriverHelper.getMetadataFromZip(File(driverPath))) | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | fun onCloseDriverManager(game: Game?) { | ||
| 115 | _isDeletingDrivers.value = true | 130 | _isDeletingDrivers.value = true |
| 131 | StringSetting.DRIVER_PATH.setString(driverList.value[selectedDriver].first) | ||
| 132 | updateDriverNameForGame(game) | ||
| 133 | if (game == null) { | ||
| 134 | NativeConfig.saveGlobalConfig() | ||
| 135 | } else { | ||
| 136 | NativeConfig.savePerGameConfig() | ||
| 137 | NativeConfig.unloadPerGameConfig() | ||
| 138 | NativeConfig.reloadGlobalConfig() | ||
| 139 | } | ||
| 140 | |||
| 116 | viewModelScope.launch { | 141 | viewModelScope.launch { |
| 117 | withContext(Dispatchers.IO) { | 142 | withContext(Dispatchers.IO) { |
| 118 | driversToDelete.forEach { | 143 | driversToDelete.forEach { |
| @@ -125,23 +150,29 @@ class DriverViewModel : ViewModel() { | |||
| 125 | _isDeletingDrivers.value = false | 150 | _isDeletingDrivers.value = false |
| 126 | } | 151 | } |
| 127 | } | 152 | } |
| 153 | } | ||
| 154 | |||
| 155 | // It is the Emulation Fragment's responsibility to load per-game settings so that this function | ||
| 156 | // knows what driver to load. | ||
| 157 | fun onLaunchGame() { | ||
| 158 | _isDriverReady.value = false | ||
| 128 | 159 | ||
| 129 | if (GpuDriverHelper.customDriverData == driverList.value[selectedDriver].second) { | 160 | val selectedDriverFile = File(StringSetting.DRIVER_PATH.getString()) |
| 161 | val selectedDriverMetadata = GpuDriverHelper.customDriverSettingData | ||
| 162 | if (GpuDriverHelper.installedCustomDriverData == selectedDriverMetadata) { | ||
| 130 | return | 163 | return |
| 131 | } | 164 | } |
| 132 | 165 | ||
| 133 | _isDriverReady.value = false | ||
| 134 | viewModelScope.launch { | 166 | viewModelScope.launch { |
| 135 | withContext(Dispatchers.IO) { | 167 | withContext(Dispatchers.IO) { |
| 136 | if (selectedDriver == 0) { | 168 | if (selectedDriverMetadata.name == null) { |
| 137 | GpuDriverHelper.installDefaultDriver() | 169 | GpuDriverHelper.installDefaultDriver() |
| 138 | setDriverReady() | 170 | setDriverReady() |
| 139 | return@withContext | 171 | return@withContext |
| 140 | } | 172 | } |
| 141 | 173 | ||
| 142 | val driverToInstall = File(driverList.value[selectedDriver].first) | 174 | if (selectedDriverFile.exists()) { |
| 143 | if (driverToInstall.exists()) { | 175 | GpuDriverHelper.installCustomDriver(selectedDriverFile) |
| 144 | GpuDriverHelper.installCustomDriver(driverToInstall) | ||
| 145 | } else { | 176 | } else { |
| 146 | GpuDriverHelper.installDefaultDriver() | 177 | GpuDriverHelper.installDefaultDriver() |
| 147 | } | 178 | } |
| @@ -150,9 +181,43 @@ class DriverViewModel : ViewModel() { | |||
| 150 | } | 181 | } |
| 151 | } | 182 | } |
| 152 | 183 | ||
| 184 | private fun findSelectedDriver(currentDriverMetadata: GpuDriverMetadata) { | ||
| 185 | if (driverList.value.size == 1) { | ||
| 186 | setSelectedDriverIndex(0) | ||
| 187 | return | ||
| 188 | } | ||
| 189 | |||
| 190 | driverList.value.forEachIndexed { i: Int, driver: Pair<String, GpuDriverMetadata> -> | ||
| 191 | if (driver.second == currentDriverMetadata) { | ||
| 192 | setSelectedDriverIndex(i) | ||
| 193 | return | ||
| 194 | } | ||
| 195 | } | ||
| 196 | } | ||
| 197 | |||
| 198 | fun updateDriverNameForGame(game: Game?) { | ||
| 199 | if (!GpuDriverHelper.supportsCustomDriverLoading()) { | ||
| 200 | return | ||
| 201 | } | ||
| 202 | |||
| 203 | if (game == null || NativeConfig.isPerGameConfigLoaded()) { | ||
| 204 | updateName() | ||
| 205 | } else { | ||
| 206 | SettingsFile.loadCustomConfig(game) | ||
| 207 | updateName() | ||
| 208 | NativeConfig.unloadPerGameConfig() | ||
| 209 | NativeConfig.reloadGlobalConfig() | ||
| 210 | } | ||
| 211 | } | ||
| 212 | |||
| 213 | private fun updateName() { | ||
| 214 | _selectedDriverTitle.value = GpuDriverHelper.customDriverSettingData.name | ||
| 215 | ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) | ||
| 216 | } | ||
| 217 | |||
| 153 | private fun setDriverReady() { | 218 | private fun setDriverReady() { |
| 154 | _isDriverReady.value = true | 219 | _isDriverReady.value = true |
| 155 | _selectedDriverMetadata.value = GpuDriverHelper.customDriverData.name | 220 | _selectedDriverTitle.value = GpuDriverHelper.customDriverSettingData.name |
| 156 | ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) | 221 | ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) |
| 157 | } | 222 | } |
| 158 | } | 223 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt index 2fa3ab31b..f1ea1e20f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt | |||
| @@ -3,10 +3,18 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.model | 4 | package org.yuzu.yuzu_emu.model |
| 5 | 5 | ||
| 6 | import android.net.Uri | ||
| 6 | import android.os.Parcelable | 7 | import android.os.Parcelable |
| 7 | import java.util.HashSet | 8 | import java.util.HashSet |
| 8 | import kotlinx.parcelize.Parcelize | 9 | import kotlinx.parcelize.Parcelize |
| 9 | import kotlinx.serialization.Serializable | 10 | import kotlinx.serialization.Serializable |
| 11 | import org.yuzu.yuzu_emu.NativeLibrary | ||
| 12 | import org.yuzu.yuzu_emu.R | ||
| 13 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 14 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization | ||
| 15 | import org.yuzu.yuzu_emu.utils.FileUtil | ||
| 16 | import java.time.LocalDateTime | ||
| 17 | import java.time.format.DateTimeFormatter | ||
| 10 | 18 | ||
| 11 | @Parcelize | 19 | @Parcelize |
| 12 | @Serializable | 20 | @Serializable |
| @@ -15,12 +23,44 @@ class Game( | |||
| 15 | val path: String, | 23 | val path: String, |
| 16 | val programId: String = "", | 24 | val programId: String = "", |
| 17 | val developer: String = "", | 25 | val developer: String = "", |
| 18 | val version: String = "", | 26 | var version: String = "", |
| 19 | val isHomebrew: Boolean = false | 27 | val isHomebrew: Boolean = false |
| 20 | ) : Parcelable { | 28 | ) : Parcelable { |
| 21 | val keyAddedToLibraryTime get() = "${path}_AddedToLibraryTime" | 29 | val keyAddedToLibraryTime get() = "${path}_AddedToLibraryTime" |
| 22 | val keyLastPlayedTime get() = "${path}_LastPlayed" | 30 | val keyLastPlayedTime get() = "${path}_LastPlayed" |
| 23 | 31 | ||
| 32 | val settingsName: String | ||
| 33 | get() { | ||
| 34 | val programIdLong = programId.toLong() | ||
| 35 | return if (programIdLong == 0L) { | ||
| 36 | FileUtil.getFilename(Uri.parse(path)) | ||
| 37 | } else { | ||
| 38 | "0" + programIdLong.toString(16).uppercase() | ||
| 39 | } | ||
| 40 | } | ||
| 41 | |||
| 42 | val programIdHex: String | ||
| 43 | get() { | ||
| 44 | val programIdLong = programId.toLong() | ||
| 45 | return if (programIdLong == 0L) { | ||
| 46 | "0" | ||
| 47 | } else { | ||
| 48 | "0" + programIdLong.toString(16).uppercase() | ||
| 49 | } | ||
| 50 | } | ||
| 51 | |||
| 52 | val saveZipName: String | ||
| 53 | get() = "$title ${YuzuApplication.appContext.getString(R.string.save_data).lowercase()} - ${ | ||
| 54 | LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) | ||
| 55 | }.zip" | ||
| 56 | |||
| 57 | val saveDir: String | ||
| 58 | get() = DirectoryInitialization.userDirectory + "/nand" + | ||
| 59 | NativeLibrary.getSavePath(programId) | ||
| 60 | |||
| 61 | val addonDir: String | ||
| 62 | get() = DirectoryInitialization.userDirectory + "/load/" + programIdHex + "/" | ||
| 63 | |||
| 24 | override fun equals(other: Any?): Boolean { | 64 | override fun equals(other: Any?): Boolean { |
| 25 | if (other !is Game) { | 65 | if (other !is Game) { |
| 26 | return false | 66 | return false |
| @@ -34,6 +74,7 @@ class Game( | |||
| 34 | result = 31 * result + path.hashCode() | 74 | result = 31 * result + path.hashCode() |
| 35 | result = 31 * result + programId.hashCode() | 75 | result = 31 * result + programId.hashCode() |
| 36 | result = 31 * result + developer.hashCode() | 76 | result = 31 * result + developer.hashCode() |
| 77 | result = 31 * result + version.hashCode() | ||
| 37 | result = 31 * result + isHomebrew.hashCode() | 78 | result = 31 * result + isHomebrew.hashCode() |
| 38 | return result | 79 | return result |
| 39 | } | 80 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProperties.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProperties.kt new file mode 100644 index 000000000..0135a95be --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProperties.kt | |||
| @@ -0,0 +1,36 @@ | |||
| 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 androidx.annotation.DrawableRes | ||
| 7 | import androidx.annotation.StringRes | ||
| 8 | import kotlinx.coroutines.flow.StateFlow | ||
| 9 | |||
| 10 | interface GameProperty { | ||
| 11 | @get:StringRes | ||
| 12 | val titleId: Int | ||
| 13 | |||
| 14 | @get:StringRes | ||
| 15 | val descriptionId: Int | ||
| 16 | |||
| 17 | @get:DrawableRes | ||
| 18 | val iconId: Int | ||
| 19 | } | ||
| 20 | |||
| 21 | data class SubmenuProperty( | ||
| 22 | override val titleId: Int, | ||
| 23 | override val descriptionId: Int, | ||
| 24 | override val iconId: Int, | ||
| 25 | val details: (() -> String)? = null, | ||
| 26 | val detailsFlow: StateFlow<String>? = null, | ||
| 27 | val action: () -> Unit | ||
| 28 | ) : GameProperty | ||
| 29 | |||
| 30 | data class InstallableProperty( | ||
| 31 | override val titleId: Int, | ||
| 32 | override val descriptionId: Int, | ||
| 33 | override val iconId: Int, | ||
| 34 | val install: (() -> Unit)? = null, | ||
| 35 | val export: (() -> Unit)? = null | ||
| 36 | ) : GameProperty | ||
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 fd925235b..d19f20dc2 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 | |||
| @@ -20,8 +20,8 @@ import kotlinx.serialization.json.Json | |||
| 20 | import org.yuzu.yuzu_emu.NativeLibrary | 20 | import org.yuzu.yuzu_emu.NativeLibrary |
| 21 | import org.yuzu.yuzu_emu.YuzuApplication | 21 | import org.yuzu.yuzu_emu.YuzuApplication |
| 22 | import org.yuzu.yuzu_emu.utils.GameHelper | 22 | import org.yuzu.yuzu_emu.utils.GameHelper |
| 23 | import org.yuzu.yuzu_emu.utils.GameMetadata | ||
| 24 | import org.yuzu.yuzu_emu.utils.NativeConfig | 23 | import org.yuzu.yuzu_emu.utils.NativeConfig |
| 24 | import java.util.concurrent.atomic.AtomicBoolean | ||
| 25 | 25 | ||
| 26 | class GamesViewModel : ViewModel() { | 26 | class GamesViewModel : ViewModel() { |
| 27 | val games: StateFlow<List<Game>> get() = _games | 27 | val games: StateFlow<List<Game>> get() = _games |
| @@ -33,6 +33,8 @@ class GamesViewModel : ViewModel() { | |||
| 33 | val isReloading: StateFlow<Boolean> get() = _isReloading | 33 | val isReloading: StateFlow<Boolean> get() = _isReloading |
| 34 | private val _isReloading = MutableStateFlow(false) | 34 | private val _isReloading = MutableStateFlow(false) |
| 35 | 35 | ||
| 36 | private val reloading = AtomicBoolean(false) | ||
| 37 | |||
| 36 | val shouldSwapData: StateFlow<Boolean> get() = _shouldSwapData | 38 | val shouldSwapData: StateFlow<Boolean> get() = _shouldSwapData |
| 37 | private val _shouldSwapData = MutableStateFlow(false) | 39 | private val _shouldSwapData = MutableStateFlow(false) |
| 38 | 40 | ||
| @@ -49,38 +51,8 @@ class GamesViewModel : ViewModel() { | |||
| 49 | // Ensure keys are loaded so that ROM metadata can be decrypted. | 51 | // Ensure keys are loaded so that ROM metadata can be decrypted. |
| 50 | NativeLibrary.reloadKeys() | 52 | NativeLibrary.reloadKeys() |
| 51 | 53 | ||
| 52 | // Retrieve list of cached games | 54 | getGameDirs() |
| 53 | val storedGames = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) | 55 | reloadGames(directoriesChanged = false, firstStartup = true) |
| 54 | .getStringSet(GameHelper.KEY_GAMES, emptySet()) | ||
| 55 | |||
| 56 | viewModelScope.launch { | ||
| 57 | withContext(Dispatchers.IO) { | ||
| 58 | getGameDirs() | ||
| 59 | if (storedGames!!.isNotEmpty()) { | ||
| 60 | val deserializedGames = mutableSetOf<Game>() | ||
| 61 | storedGames.forEach { | ||
| 62 | val game: Game | ||
| 63 | try { | ||
| 64 | game = Json.decodeFromString(it) | ||
| 65 | } catch (e: Exception) { | ||
| 66 | // We don't care about any errors related to parsing the game cache | ||
| 67 | return@forEach | ||
| 68 | } | ||
| 69 | |||
| 70 | val gameExists = | ||
| 71 | DocumentFile.fromSingleUri( | ||
| 72 | YuzuApplication.appContext, | ||
| 73 | Uri.parse(game.path) | ||
| 74 | )?.exists() | ||
| 75 | if (gameExists == true) { | ||
| 76 | deserializedGames.add(game) | ||
| 77 | } | ||
| 78 | } | ||
| 79 | setGames(deserializedGames.toList()) | ||
| 80 | } | ||
| 81 | reloadGames(false) | ||
| 82 | } | ||
| 83 | } | ||
| 84 | } | 56 | } |
| 85 | 57 | ||
| 86 | fun setGames(games: List<Game>) { | 58 | fun setGames(games: List<Game>) { |
| @@ -110,16 +82,46 @@ class GamesViewModel : ViewModel() { | |||
| 110 | _searchFocused.value = searchFocused | 82 | _searchFocused.value = searchFocused |
| 111 | } | 83 | } |
| 112 | 84 | ||
| 113 | fun reloadGames(directoriesChanged: Boolean) { | 85 | fun reloadGames(directoriesChanged: Boolean, firstStartup: Boolean = false) { |
| 114 | if (isReloading.value) { | 86 | if (reloading.get()) { |
| 115 | return | 87 | return |
| 116 | } | 88 | } |
| 89 | reloading.set(true) | ||
| 117 | _isReloading.value = true | 90 | _isReloading.value = true |
| 118 | 91 | ||
| 119 | viewModelScope.launch { | 92 | viewModelScope.launch { |
| 120 | withContext(Dispatchers.IO) { | 93 | withContext(Dispatchers.IO) { |
| 121 | GameMetadata.resetMetadata() | 94 | if (firstStartup) { |
| 95 | // Retrieve list of cached games | ||
| 96 | val storedGames = | ||
| 97 | PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) | ||
| 98 | .getStringSet(GameHelper.KEY_GAMES, emptySet()) | ||
| 99 | if (storedGames!!.isNotEmpty()) { | ||
| 100 | val deserializedGames = mutableSetOf<Game>() | ||
| 101 | storedGames.forEach { | ||
| 102 | val game: Game | ||
| 103 | try { | ||
| 104 | game = Json.decodeFromString(it) | ||
| 105 | } catch (e: Exception) { | ||
| 106 | // We don't care about any errors related to parsing the game cache | ||
| 107 | return@forEach | ||
| 108 | } | ||
| 109 | |||
| 110 | val gameExists = | ||
| 111 | DocumentFile.fromSingleUri( | ||
| 112 | YuzuApplication.appContext, | ||
| 113 | Uri.parse(game.path) | ||
| 114 | )?.exists() | ||
| 115 | if (gameExists == true) { | ||
| 116 | deserializedGames.add(game) | ||
| 117 | } | ||
| 118 | } | ||
| 119 | setGames(deserializedGames.toList()) | ||
| 120 | } | ||
| 121 | } | ||
| 122 | |||
| 122 | setGames(GameHelper.getGames()) | 123 | setGames(GameHelper.getGames()) |
| 124 | reloading.set(false) | ||
| 123 | _isReloading.value = false | 125 | _isReloading.value = false |
| 124 | 126 | ||
| 125 | if (directoriesChanged) { | 127 | if (directoriesChanged) { |
| @@ -168,6 +170,7 @@ class GamesViewModel : ViewModel() { | |||
| 168 | fun onCloseGameFoldersFragment() = | 170 | fun onCloseGameFoldersFragment() = |
| 169 | viewModelScope.launch { | 171 | viewModelScope.launch { |
| 170 | withContext(Dispatchers.IO) { | 172 | withContext(Dispatchers.IO) { |
| 173 | NativeConfig.saveGlobalConfig() | ||
| 171 | getGameDirs(true) | 174 | getGameDirs(true) |
| 172 | } | 175 | } |
| 173 | } | 176 | } |
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 07e65b028..513ac2fc5 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,6 +3,7 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.model | 4 | package org.yuzu.yuzu_emu.model |
| 5 | 5 | ||
| 6 | import android.net.Uri | ||
| 6 | import androidx.lifecycle.ViewModel | 7 | import androidx.lifecycle.ViewModel |
| 7 | import kotlinx.coroutines.flow.MutableStateFlow | 8 | import kotlinx.coroutines.flow.MutableStateFlow |
| 8 | import kotlinx.coroutines.flow.StateFlow | 9 | import kotlinx.coroutines.flow.StateFlow |
| @@ -21,6 +22,15 @@ class HomeViewModel : ViewModel() { | |||
| 21 | private val _gamesDirSelected = MutableStateFlow(false) | 22 | private val _gamesDirSelected = MutableStateFlow(false) |
| 22 | val gamesDirSelected get() = _gamesDirSelected.asStateFlow() | 23 | val gamesDirSelected get() = _gamesDirSelected.asStateFlow() |
| 23 | 24 | ||
| 25 | private val _openImportSaves = MutableStateFlow(false) | ||
| 26 | val openImportSaves get() = _openImportSaves.asStateFlow() | ||
| 27 | |||
| 28 | private val _contentToInstall = MutableStateFlow<List<Uri>?>(null) | ||
| 29 | val contentToInstall get() = _contentToInstall.asStateFlow() | ||
| 30 | |||
| 31 | private val _reloadPropertiesList = MutableStateFlow(false) | ||
| 32 | val reloadPropertiesList get() = _reloadPropertiesList.asStateFlow() | ||
| 33 | |||
| 24 | var navigatedToSetup = false | 34 | var navigatedToSetup = false |
| 25 | 35 | ||
| 26 | fun setNavigationVisibility(visible: Boolean, animated: Boolean) { | 36 | fun setNavigationVisibility(visible: Boolean, animated: Boolean) { |
| @@ -44,4 +54,16 @@ class HomeViewModel : ViewModel() { | |||
| 44 | fun setGamesDirSelected(selected: Boolean) { | 54 | fun setGamesDirSelected(selected: Boolean) { |
| 45 | _gamesDirSelected.value = selected | 55 | _gamesDirSelected.value = selected |
| 46 | } | 56 | } |
| 57 | |||
| 58 | fun setOpenImportSaves(import: Boolean) { | ||
| 59 | _openImportSaves.value = import | ||
| 60 | } | ||
| 61 | |||
| 62 | fun setContentToInstall(documents: List<Uri>?) { | ||
| 63 | _contentToInstall.value = documents | ||
| 64 | } | ||
| 65 | |||
| 66 | fun reloadPropertiesList(reload: Boolean) { | ||
| 67 | _reloadPropertiesList.value = reload | ||
| 68 | } | ||
| 47 | } | 69 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt index 36ffd08d2..641c5cb17 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt | |||
| @@ -6,9 +6,9 @@ package org.yuzu.yuzu_emu.model | |||
| 6 | import androidx.lifecycle.ViewModel | 6 | import androidx.lifecycle.ViewModel |
| 7 | 7 | ||
| 8 | class MessageDialogViewModel : ViewModel() { | 8 | class MessageDialogViewModel : ViewModel() { |
| 9 | var dismissAction: () -> Unit = {} | 9 | var positiveAction: (() -> Unit)? = null |
| 10 | 10 | ||
| 11 | fun clear() { | 11 | fun clear() { |
| 12 | dismissAction = {} | 12 | positiveAction = null |
| 13 | } | 13 | } |
| 14 | } | 14 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt index ccc981e95..5cb6a5d57 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt | |||
| @@ -68,8 +68,4 @@ class SettingsViewModel : ViewModel() { | |||
| 68 | fun setAdapterItemChanged(value: Int) { | 68 | fun setAdapterItemChanged(value: Int) { |
| 69 | _adapterItemChanged.value = value | 69 | _adapterItemChanged.value = value |
| 70 | } | 70 | } |
| 71 | |||
| 72 | fun clear() { | ||
| 73 | game = null | ||
| 74 | } | ||
| 75 | } | 71 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt index 16a794dee..e59c95733 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt | |||
| @@ -23,7 +23,7 @@ class TaskViewModel : ViewModel() { | |||
| 23 | val cancelled: StateFlow<Boolean> get() = _cancelled | 23 | val cancelled: StateFlow<Boolean> get() = _cancelled |
| 24 | private val _cancelled = MutableStateFlow(false) | 24 | private val _cancelled = MutableStateFlow(false) |
| 25 | 25 | ||
| 26 | lateinit var task: () -> Any | 26 | lateinit var task: suspend () -> Any |
| 27 | 27 | ||
| 28 | fun clear() { | 28 | fun clear() { |
| 29 | _result.value = Any() | 29 | _result.value = Any() |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt index 805b89b31..fc0eeb9ad 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt | |||
| @@ -19,7 +19,7 @@ import androidx.lifecycle.Lifecycle | |||
| 19 | import androidx.lifecycle.lifecycleScope | 19 | import androidx.lifecycle.lifecycleScope |
| 20 | import androidx.lifecycle.repeatOnLifecycle | 20 | import androidx.lifecycle.repeatOnLifecycle |
| 21 | import com.google.android.material.color.MaterialColors | 21 | import com.google.android.material.color.MaterialColors |
| 22 | import com.google.android.material.transition.MaterialFadeThrough | 22 | import kotlinx.coroutines.flow.collectLatest |
| 23 | import kotlinx.coroutines.launch | 23 | import kotlinx.coroutines.launch |
| 24 | import org.yuzu.yuzu_emu.R | 24 | import org.yuzu.yuzu_emu.R |
| 25 | import org.yuzu.yuzu_emu.adapters.GameAdapter | 25 | import org.yuzu.yuzu_emu.adapters.GameAdapter |
| @@ -35,11 +35,6 @@ class GamesFragment : Fragment() { | |||
| 35 | private val gamesViewModel: GamesViewModel by activityViewModels() | 35 | private val gamesViewModel: GamesViewModel by activityViewModels() |
| 36 | private val homeViewModel: HomeViewModel by activityViewModels() | 36 | private val homeViewModel: HomeViewModel by activityViewModels() |
| 37 | 37 | ||
| 38 | override fun onCreate(savedInstanceState: Bundle?) { | ||
| 39 | super.onCreate(savedInstanceState) | ||
| 40 | enterTransition = MaterialFadeThrough() | ||
| 41 | } | ||
| 42 | |||
| 43 | override fun onCreateView( | 38 | override fun onCreateView( |
| 44 | inflater: LayoutInflater, | 39 | inflater: LayoutInflater, |
| 45 | container: ViewGroup?, | 40 | container: ViewGroup?, |
| @@ -52,7 +47,9 @@ class GamesFragment : Fragment() { | |||
| 52 | // This is using the correct scope, lint is just acting up | 47 | // This is using the correct scope, lint is just acting up |
| 53 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") | 48 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") |
| 54 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 49 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 55 | homeViewModel.setNavigationVisibility(visible = true, animated = false) | 50 | super.onViewCreated(view, savedInstanceState) |
| 51 | homeViewModel.setNavigationVisibility(visible = true, animated = true) | ||
| 52 | homeViewModel.setStatusBarShadeVisibility(true) | ||
| 56 | 53 | ||
| 57 | binding.gridGames.apply { | 54 | binding.gridGames.apply { |
| 58 | layoutManager = AutofitGridLayoutManager( | 55 | layoutManager = AutofitGridLayoutManager( |
| @@ -99,7 +96,7 @@ class GamesFragment : Fragment() { | |||
| 99 | } | 96 | } |
| 100 | launch { | 97 | launch { |
| 101 | repeatOnLifecycle(Lifecycle.State.RESUMED) { | 98 | repeatOnLifecycle(Lifecycle.State.RESUMED) { |
| 102 | gamesViewModel.games.collect { | 99 | gamesViewModel.games.collectLatest { |
| 103 | (binding.gridGames.adapter as GameAdapter).submitList(it) | 100 | (binding.gridGames.adapter as GameAdapter).submitList(it) |
| 104 | if (it.isEmpty()) { | 101 | if (it.isEmpty()) { |
| 105 | binding.noticeText.visibility = View.VISIBLE | 102 | binding.noticeText.visibility = View.VISIBLE |
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 16323a316..b4117d761 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 | |||
| @@ -28,12 +28,9 @@ import androidx.navigation.ui.setupWithNavController | |||
| 28 | import androidx.preference.PreferenceManager | 28 | import androidx.preference.PreferenceManager |
| 29 | import com.google.android.material.color.MaterialColors | 29 | import com.google.android.material.color.MaterialColors |
| 30 | import com.google.android.material.navigation.NavigationBarView | 30 | import com.google.android.material.navigation.NavigationBarView |
| 31 | import kotlinx.coroutines.CoroutineScope | ||
| 32 | import java.io.File | 31 | import java.io.File |
| 33 | import java.io.FilenameFilter | 32 | import java.io.FilenameFilter |
| 34 | import kotlinx.coroutines.Dispatchers | ||
| 35 | import kotlinx.coroutines.launch | 33 | import kotlinx.coroutines.launch |
| 36 | import kotlinx.coroutines.withContext | ||
| 37 | import org.yuzu.yuzu_emu.HomeNavigationDirections | 34 | import org.yuzu.yuzu_emu.HomeNavigationDirections |
| 38 | import org.yuzu.yuzu_emu.NativeLibrary | 35 | import org.yuzu.yuzu_emu.NativeLibrary |
| 39 | import org.yuzu.yuzu_emu.R | 36 | import org.yuzu.yuzu_emu.R |
| @@ -43,7 +40,7 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings | |||
| 43 | import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment | 40 | import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment |
| 44 | import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment | 41 | import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment |
| 45 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment | 42 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment |
| 46 | import org.yuzu.yuzu_emu.getPublicFilesDir | 43 | import org.yuzu.yuzu_emu.model.AddonViewModel |
| 47 | import org.yuzu.yuzu_emu.model.GamesViewModel | 44 | import org.yuzu.yuzu_emu.model.GamesViewModel |
| 48 | import org.yuzu.yuzu_emu.model.HomeViewModel | 45 | import org.yuzu.yuzu_emu.model.HomeViewModel |
| 49 | import org.yuzu.yuzu_emu.model.TaskState | 46 | import org.yuzu.yuzu_emu.model.TaskState |
| @@ -60,15 +57,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 60 | private val homeViewModel: HomeViewModel by viewModels() | 57 | private val homeViewModel: HomeViewModel by viewModels() |
| 61 | private val gamesViewModel: GamesViewModel by viewModels() | 58 | private val gamesViewModel: GamesViewModel by viewModels() |
| 62 | private val taskViewModel: TaskViewModel by viewModels() | 59 | private val taskViewModel: TaskViewModel by viewModels() |
| 60 | private val addonViewModel: AddonViewModel by viewModels() | ||
| 63 | 61 | ||
| 64 | override var themeId: Int = 0 | 62 | override var themeId: Int = 0 |
| 65 | 63 | ||
| 66 | private val savesFolder | ||
| 67 | get() = "${getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000" | ||
| 68 | |||
| 69 | // Get first subfolder in saves folder (should be the user folder) | ||
| 70 | val savesFolderRoot get() = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: "" | ||
| 71 | |||
| 72 | override fun onCreate(savedInstanceState: Bundle?) { | 64 | override fun onCreate(savedInstanceState: Bundle?) { |
| 73 | val splashScreen = installSplashScreen() | 65 | val splashScreen = installSplashScreen() |
| 74 | splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } | 66 | splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } |
| @@ -145,6 +137,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 145 | homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) } | 137 | homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) } |
| 146 | } | 138 | } |
| 147 | } | 139 | } |
| 140 | launch { | ||
| 141 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 142 | homeViewModel.contentToInstall.collect { | ||
| 143 | if (it != null) { | ||
| 144 | installContent(it) | ||
| 145 | homeViewModel.setContentToInstall(null) | ||
| 146 | } | ||
| 147 | } | ||
| 148 | } | ||
| 149 | } | ||
| 148 | } | 150 | } |
| 149 | 151 | ||
| 150 | // Dismiss previous notifications (should not happen unless a crash occurred) | 152 | // Dismiss previous notifications (should not happen unless a crash occurred) |
| @@ -253,13 +255,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 253 | super.onResume() | 255 | super.onResume() |
| 254 | } | 256 | } |
| 255 | 257 | ||
| 256 | override fun onStop() { | ||
| 257 | super.onStop() | ||
| 258 | CoroutineScope(Dispatchers.IO).launch { | ||
| 259 | NativeConfig.saveSettings() | ||
| 260 | } | ||
| 261 | } | ||
| 262 | |||
| 263 | override fun onDestroy() { | 258 | override fun onDestroy() { |
| 264 | EmulationActivity.stopForegroundService(this) | 259 | EmulationActivity.stopForegroundService(this) |
| 265 | super.onDestroy() | 260 | super.onDestroy() |
| @@ -468,110 +463,150 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 468 | val installGameUpdate = registerForActivityResult( | 463 | val installGameUpdate = registerForActivityResult( |
| 469 | ActivityResultContracts.OpenMultipleDocuments() | 464 | ActivityResultContracts.OpenMultipleDocuments() |
| 470 | ) { documents: List<Uri> -> | 465 | ) { documents: List<Uri> -> |
| 471 | if (documents.isNotEmpty()) { | 466 | if (documents.isEmpty()) { |
| 472 | IndeterminateProgressDialogFragment.newInstance( | 467 | return@registerForActivityResult |
| 473 | this@MainActivity, | 468 | } |
| 474 | R.string.installing_game_content | ||
| 475 | ) { | ||
| 476 | var installSuccess = 0 | ||
| 477 | var installOverwrite = 0 | ||
| 478 | var errorBaseGame = 0 | ||
| 479 | var errorExtension = 0 | ||
| 480 | var errorOther = 0 | ||
| 481 | documents.forEach { | ||
| 482 | when ( | ||
| 483 | NativeLibrary.installFileToNand( | ||
| 484 | it.toString(), | ||
| 485 | FileUtil.getExtension(it) | ||
| 486 | ) | ||
| 487 | ) { | ||
| 488 | NativeLibrary.InstallFileToNandResult.Success -> { | ||
| 489 | installSuccess += 1 | ||
| 490 | } | ||
| 491 | 469 | ||
| 492 | NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> { | 470 | if (addonViewModel.game == null) { |
| 493 | installOverwrite += 1 | 471 | installContent(documents) |
| 494 | } | 472 | return@registerForActivityResult |
| 473 | } | ||
| 495 | 474 | ||
| 496 | NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> { | 475 | IndeterminateProgressDialogFragment.newInstance( |
| 497 | errorBaseGame += 1 | 476 | this@MainActivity, |
| 498 | } | 477 | R.string.verifying_content, |
| 478 | false | ||
| 479 | ) { | ||
| 480 | var updatesMatchProgram = true | ||
| 481 | for (document in documents) { | ||
| 482 | val valid = NativeLibrary.doesUpdateMatchProgram( | ||
| 483 | addonViewModel.game!!.programId, | ||
| 484 | document.toString() | ||
| 485 | ) | ||
| 486 | if (!valid) { | ||
| 487 | updatesMatchProgram = false | ||
| 488 | break | ||
| 489 | } | ||
| 490 | } | ||
| 499 | 491 | ||
| 500 | NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> { | 492 | if (updatesMatchProgram) { |
| 501 | errorExtension += 1 | 493 | homeViewModel.setContentToInstall(documents) |
| 502 | } | 494 | } else { |
| 495 | MessageDialogFragment.newInstance( | ||
| 496 | this@MainActivity, | ||
| 497 | titleId = R.string.content_install_notice, | ||
| 498 | descriptionId = R.string.content_install_notice_description, | ||
| 499 | positiveAction = { homeViewModel.setContentToInstall(documents) } | ||
| 500 | ) | ||
| 501 | } | ||
| 502 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) | ||
| 503 | } | ||
| 503 | 504 | ||
| 504 | else -> { | 505 | private fun installContent(documents: List<Uri>) { |
| 505 | errorOther += 1 | 506 | IndeterminateProgressDialogFragment.newInstance( |
| 506 | } | 507 | this@MainActivity, |
| 508 | R.string.installing_game_content | ||
| 509 | ) { | ||
| 510 | var installSuccess = 0 | ||
| 511 | var installOverwrite = 0 | ||
| 512 | var errorBaseGame = 0 | ||
| 513 | var errorExtension = 0 | ||
| 514 | var errorOther = 0 | ||
| 515 | documents.forEach { | ||
| 516 | when ( | ||
| 517 | NativeLibrary.installFileToNand( | ||
| 518 | it.toString(), | ||
| 519 | FileUtil.getExtension(it) | ||
| 520 | ) | ||
| 521 | ) { | ||
| 522 | NativeLibrary.InstallFileToNandResult.Success -> { | ||
| 523 | installSuccess += 1 | ||
| 524 | } | ||
| 525 | |||
| 526 | NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> { | ||
| 527 | installOverwrite += 1 | ||
| 528 | } | ||
| 529 | |||
| 530 | NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> { | ||
| 531 | errorBaseGame += 1 | ||
| 532 | } | ||
| 533 | |||
| 534 | NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> { | ||
| 535 | errorExtension += 1 | ||
| 536 | } | ||
| 537 | |||
| 538 | else -> { | ||
| 539 | errorOther += 1 | ||
| 507 | } | 540 | } |
| 508 | } | 541 | } |
| 542 | } | ||
| 509 | 543 | ||
| 510 | val separator = System.getProperty("line.separator") ?: "\n" | 544 | addonViewModel.refreshAddons() |
| 511 | val installResult = StringBuilder() | 545 | |
| 512 | if (installSuccess > 0) { | 546 | val separator = System.getProperty("line.separator") ?: "\n" |
| 513 | installResult.append( | 547 | val installResult = StringBuilder() |
| 514 | getString( | 548 | if (installSuccess > 0) { |
| 515 | R.string.install_game_content_success_install, | 549 | installResult.append( |
| 516 | installSuccess | 550 | getString( |
| 517 | ) | 551 | R.string.install_game_content_success_install, |
| 552 | installSuccess | ||
| 518 | ) | 553 | ) |
| 554 | ) | ||
| 555 | installResult.append(separator) | ||
| 556 | } | ||
| 557 | if (installOverwrite > 0) { | ||
| 558 | installResult.append( | ||
| 559 | getString( | ||
| 560 | R.string.install_game_content_success_overwrite, | ||
| 561 | installOverwrite | ||
| 562 | ) | ||
| 563 | ) | ||
| 564 | installResult.append(separator) | ||
| 565 | } | ||
| 566 | val errorTotal: Int = errorBaseGame + errorExtension + errorOther | ||
| 567 | if (errorTotal > 0) { | ||
| 568 | installResult.append(separator) | ||
| 569 | installResult.append( | ||
| 570 | getString( | ||
| 571 | R.string.install_game_content_failed_count, | ||
| 572 | errorTotal | ||
| 573 | ) | ||
| 574 | ) | ||
| 575 | installResult.append(separator) | ||
| 576 | if (errorBaseGame > 0) { | ||
| 519 | installResult.append(separator) | 577 | installResult.append(separator) |
| 520 | } | ||
| 521 | if (installOverwrite > 0) { | ||
| 522 | installResult.append( | 578 | installResult.append( |
| 523 | getString( | 579 | getString(R.string.install_game_content_failure_base) |
| 524 | R.string.install_game_content_success_overwrite, | ||
| 525 | installOverwrite | ||
| 526 | ) | ||
| 527 | ) | 580 | ) |
| 528 | installResult.append(separator) | 581 | installResult.append(separator) |
| 529 | } | 582 | } |
| 530 | val errorTotal: Int = errorBaseGame + errorExtension + errorOther | 583 | if (errorExtension > 0) { |
| 531 | if (errorTotal > 0) { | ||
| 532 | installResult.append(separator) | 584 | installResult.append(separator) |
| 533 | installResult.append( | 585 | installResult.append( |
| 534 | getString( | 586 | getString(R.string.install_game_content_failure_file_extension) |
| 535 | R.string.install_game_content_failed_count, | ||
| 536 | errorTotal | ||
| 537 | ) | ||
| 538 | ) | 587 | ) |
| 539 | installResult.append(separator) | 588 | installResult.append(separator) |
| 540 | if (errorBaseGame > 0) { | 589 | } |
| 541 | installResult.append(separator) | 590 | if (errorOther > 0) { |
| 542 | installResult.append( | 591 | installResult.append( |
| 543 | getString(R.string.install_game_content_failure_base) | 592 | getString(R.string.install_game_content_failure_description) |
| 544 | ) | ||
| 545 | installResult.append(separator) | ||
| 546 | } | ||
| 547 | if (errorExtension > 0) { | ||
| 548 | installResult.append(separator) | ||
| 549 | installResult.append( | ||
| 550 | getString(R.string.install_game_content_failure_file_extension) | ||
| 551 | ) | ||
| 552 | installResult.append(separator) | ||
| 553 | } | ||
| 554 | if (errorOther > 0) { | ||
| 555 | installResult.append( | ||
| 556 | getString(R.string.install_game_content_failure_description) | ||
| 557 | ) | ||
| 558 | installResult.append(separator) | ||
| 559 | } | ||
| 560 | return@newInstance MessageDialogFragment.newInstance( | ||
| 561 | this, | ||
| 562 | titleId = R.string.install_game_content_failure, | ||
| 563 | descriptionString = installResult.toString().trim(), | ||
| 564 | helpLinkId = R.string.install_game_content_help_link | ||
| 565 | ) | ||
| 566 | } else { | ||
| 567 | return@newInstance MessageDialogFragment.newInstance( | ||
| 568 | this, | ||
| 569 | titleId = R.string.install_game_content_success, | ||
| 570 | descriptionString = installResult.toString().trim() | ||
| 571 | ) | 593 | ) |
| 594 | installResult.append(separator) | ||
| 572 | } | 595 | } |
| 573 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) | 596 | return@newInstance MessageDialogFragment.newInstance( |
| 574 | } | 597 | this, |
| 598 | titleId = R.string.install_game_content_failure, | ||
| 599 | descriptionString = installResult.toString().trim(), | ||
| 600 | helpLinkId = R.string.install_game_content_help_link | ||
| 601 | ) | ||
| 602 | } else { | ||
| 603 | return@newInstance MessageDialogFragment.newInstance( | ||
| 604 | this, | ||
| 605 | titleId = R.string.install_game_content_success, | ||
| 606 | descriptionString = installResult.toString().trim() | ||
| 607 | ) | ||
| 608 | } | ||
| 609 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) | ||
| 575 | } | 610 | } |
| 576 | 611 | ||
| 577 | val exportUserData = registerForActivityResult( | 612 | val exportUserData = registerForActivityResult( |
| @@ -632,7 +667,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 632 | } | 667 | } |
| 633 | 668 | ||
| 634 | // Clear existing user data | 669 | // Clear existing user data |
| 635 | NativeConfig.unloadConfig() | 670 | NativeConfig.unloadGlobalConfig() |
| 636 | File(DirectoryInitialization.userDirectory!!).deleteRecursively() | 671 | File(DirectoryInitialization.userDirectory!!).deleteRecursively() |
| 637 | 672 | ||
| 638 | // Copy archive to internal storage | 673 | // Copy archive to internal storage |
| @@ -651,108 +686,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 651 | 686 | ||
| 652 | // Reinitialize relevant data | 687 | // Reinitialize relevant data |
| 653 | NativeLibrary.initializeSystem(true) | 688 | NativeLibrary.initializeSystem(true) |
| 654 | NativeConfig.initializeConfig() | 689 | NativeConfig.initializeGlobalConfig() |
| 655 | gamesViewModel.reloadGames(false) | 690 | gamesViewModel.reloadGames(false) |
| 656 | 691 | ||
| 657 | return@newInstance getString(R.string.user_data_import_success) | 692 | return@newInstance getString(R.string.user_data_import_success) |
| 658 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) | 693 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) |
| 659 | } | 694 | } |
| 660 | |||
| 661 | /** | ||
| 662 | * Exports the save file located in the given folder path by creating a zip file and sharing it via intent. | ||
| 663 | */ | ||
| 664 | val exportSaves = registerForActivityResult( | ||
| 665 | ActivityResultContracts.CreateDocument("application/zip") | ||
| 666 | ) { result -> | ||
| 667 | if (result == null) { | ||
| 668 | return@registerForActivityResult | ||
| 669 | } | ||
| 670 | |||
| 671 | IndeterminateProgressDialogFragment.newInstance( | ||
| 672 | this, | ||
| 673 | R.string.save_files_exporting, | ||
| 674 | false | ||
| 675 | ) { | ||
| 676 | val zipResult = FileUtil.zipFromInternalStorage( | ||
| 677 | File(savesFolderRoot), | ||
| 678 | savesFolderRoot, | ||
| 679 | BufferedOutputStream(contentResolver.openOutputStream(result)) | ||
| 680 | ) | ||
| 681 | return@newInstance when (zipResult) { | ||
| 682 | TaskState.Completed -> getString(R.string.export_success) | ||
| 683 | TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) | ||
| 684 | } | ||
| 685 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) | ||
| 686 | } | ||
| 687 | |||
| 688 | private val startForResultExportSave = | ||
| 689 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ -> | ||
| 690 | File(getPublicFilesDir().canonicalPath, "temp").deleteRecursively() | ||
| 691 | } | ||
| 692 | |||
| 693 | val importSaves = | ||
| 694 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> | ||
| 695 | if (result == null) { | ||
| 696 | return@registerForActivityResult | ||
| 697 | } | ||
| 698 | |||
| 699 | NativeLibrary.initializeEmptyUserDirectory() | ||
| 700 | |||
| 701 | val inputZip = contentResolver.openInputStream(result) | ||
| 702 | // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid. | ||
| 703 | var validZip = false | ||
| 704 | val savesFolder = File(savesFolderRoot) | ||
| 705 | val cacheSaveDir = File("${applicationContext.cacheDir.path}/saves/") | ||
| 706 | cacheSaveDir.mkdir() | ||
| 707 | |||
| 708 | if (inputZip == null) { | ||
| 709 | Toast.makeText( | ||
| 710 | applicationContext, | ||
| 711 | getString(R.string.fatal_error), | ||
| 712 | Toast.LENGTH_LONG | ||
| 713 | ).show() | ||
| 714 | return@registerForActivityResult | ||
| 715 | } | ||
| 716 | |||
| 717 | val filterTitleId = | ||
| 718 | FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) } | ||
| 719 | |||
| 720 | try { | ||
| 721 | CoroutineScope(Dispatchers.IO).launch { | ||
| 722 | FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) | ||
| 723 | cacheSaveDir.list(filterTitleId)?.forEach { savePath -> | ||
| 724 | File(savesFolder, savePath).deleteRecursively() | ||
| 725 | File(cacheSaveDir, savePath).copyRecursively( | ||
| 726 | File(savesFolder, savePath), | ||
| 727 | true | ||
| 728 | ) | ||
| 729 | validZip = true | ||
| 730 | } | ||
| 731 | |||
| 732 | withContext(Dispatchers.Main) { | ||
| 733 | if (!validZip) { | ||
| 734 | MessageDialogFragment.newInstance( | ||
| 735 | this@MainActivity, | ||
| 736 | titleId = R.string.save_file_invalid_zip_structure, | ||
| 737 | descriptionId = R.string.save_file_invalid_zip_structure_description | ||
| 738 | ).show(supportFragmentManager, MessageDialogFragment.TAG) | ||
| 739 | return@withContext | ||
| 740 | } | ||
| 741 | Toast.makeText( | ||
| 742 | applicationContext, | ||
| 743 | getString(R.string.save_file_imported_success), | ||
| 744 | Toast.LENGTH_LONG | ||
| 745 | ).show() | ||
| 746 | } | ||
| 747 | |||
| 748 | cacheSaveDir.deleteRecursively() | ||
| 749 | } | ||
| 750 | } catch (e: Exception) { | ||
| 751 | Toast.makeText( | ||
| 752 | applicationContext, | ||
| 753 | getString(R.string.fatal_error), | ||
| 754 | Toast.LENGTH_LONG | ||
| 755 | ).show() | ||
| 756 | } | ||
| 757 | } | ||
| 758 | } | 695 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddonUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddonUtil.kt new file mode 100644 index 000000000..8cc5ea71f --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddonUtil.kt | |||
| @@ -0,0 +1,8 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.utils | ||
| 5 | |||
| 6 | object AddonUtil { | ||
| 7 | val validAddonDirectories = listOf("cheats", "exefs", "romfs") | ||
| 8 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt index 21270fc84..0197fd712 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt | |||
| @@ -16,7 +16,7 @@ object DirectoryInitialization { | |||
| 16 | if (!areDirectoriesReady) { | 16 | if (!areDirectoriesReady) { |
| 17 | initializeInternalStorage() | 17 | initializeInternalStorage() |
| 18 | NativeLibrary.initializeSystem(false) | 18 | NativeLibrary.initializeSystem(false) |
| 19 | NativeConfig.initializeConfig() | 19 | NativeConfig.initializeGlobalConfig() |
| 20 | areDirectoriesReady = true | 20 | areDirectoriesReady = true |
| 21 | } | 21 | } |
| 22 | } | 22 | } |
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 bbe7bfa92..00c6bf90e 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 | |||
| @@ -22,6 +22,7 @@ import java.io.BufferedOutputStream | |||
| 22 | import java.lang.NullPointerException | 22 | import java.lang.NullPointerException |
| 23 | import java.nio.charset.StandardCharsets | 23 | import java.nio.charset.StandardCharsets |
| 24 | import java.util.zip.ZipOutputStream | 24 | import java.util.zip.ZipOutputStream |
| 25 | import kotlin.IllegalStateException | ||
| 25 | 26 | ||
| 26 | object FileUtil { | 27 | object FileUtil { |
| 27 | const val PATH_TREE = "tree" | 28 | const val PATH_TREE = "tree" |
| @@ -342,6 +343,37 @@ object FileUtil { | |||
| 342 | return TaskState.Completed | 343 | return TaskState.Completed |
| 343 | } | 344 | } |
| 344 | 345 | ||
| 346 | /** | ||
| 347 | * Helper function that copies the contents of a DocumentFile folder into a [File] | ||
| 348 | * @param file [File] representation of the folder to copy into | ||
| 349 | * @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa | ||
| 350 | */ | ||
| 351 | fun DocumentFile.copyFilesTo(file: File) { | ||
| 352 | file.mkdirs() | ||
| 353 | if (!this.isDirectory || !file.isDirectory) { | ||
| 354 | throw IllegalStateException( | ||
| 355 | "[FileUtil] Tried to copy a folder into a file or vice versa" | ||
| 356 | ) | ||
| 357 | } | ||
| 358 | |||
| 359 | this.listFiles().forEach { | ||
| 360 | val newFile = File(file, it.name!!) | ||
| 361 | if (it.isDirectory) { | ||
| 362 | newFile.mkdirs() | ||
| 363 | DocumentFile.fromTreeUri(YuzuApplication.appContext, it.uri)?.copyFilesTo(newFile) | ||
| 364 | } else { | ||
| 365 | val inputStream = | ||
| 366 | YuzuApplication.appContext.contentResolver.openInputStream(it.uri) | ||
| 367 | BufferedInputStream(inputStream).use { bos -> | ||
| 368 | if (!newFile.exists()) { | ||
| 369 | newFile.createNewFile() | ||
| 370 | } | ||
| 371 | newFile.outputStream().use { os -> bos.copyTo(os) } | ||
| 372 | } | ||
| 373 | } | ||
| 374 | } | ||
| 375 | } | ||
| 376 | |||
| 345 | fun isRootTreeUri(uri: Uri): Boolean { | 377 | fun isRootTreeUri(uri: Uri): Boolean { |
| 346 | val paths = uri.pathSegments | 378 | val paths = uri.pathSegments |
| 347 | return paths.size == 2 && PATH_TREE == paths[0] | 379 | return paths.size == 2 && PATH_TREE == paths[0] |
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 55010dc59..579b600f1 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 | |||
| @@ -36,6 +36,12 @@ object GameHelper { | |||
| 36 | // Ensure keys are loaded so that ROM metadata can be decrypted. | 36 | // Ensure keys are loaded so that ROM metadata can be decrypted. |
| 37 | NativeLibrary.reloadKeys() | 37 | NativeLibrary.reloadKeys() |
| 38 | 38 | ||
| 39 | // Reset metadata so we don't use stale information | ||
| 40 | GameMetadata.resetMetadata() | ||
| 41 | |||
| 42 | // Remove previous filesystem provider information so we can get up to date version info | ||
| 43 | NativeLibrary.clearFilesystemProvider() | ||
| 44 | |||
| 39 | val badDirs = mutableListOf<Int>() | 45 | val badDirs = mutableListOf<Int>() |
| 40 | gameDirs.forEachIndexed { index: Int, gameDir: GameDir -> | 46 | gameDirs.forEachIndexed { index: Int, gameDir: GameDir -> |
| 41 | val gameDirUri = Uri.parse(gameDir.uriString) | 47 | val gameDirUri = Uri.parse(gameDir.uriString) |
| @@ -92,14 +98,24 @@ object GameHelper { | |||
| 92 | ) | 98 | ) |
| 93 | } else { | 99 | } else { |
| 94 | if (Game.extensions.contains(FileUtil.getExtension(it.uri))) { | 100 | if (Game.extensions.contains(FileUtil.getExtension(it.uri))) { |
| 95 | games.add(getGame(it.uri, true)) | 101 | val game = getGame(it.uri, true) |
| 102 | if (game != null) { | ||
| 103 | games.add(game) | ||
| 104 | } | ||
| 96 | } | 105 | } |
| 97 | } | 106 | } |
| 98 | } | 107 | } |
| 99 | } | 108 | } |
| 100 | 109 | ||
| 101 | fun getGame(uri: Uri, addedToLibrary: Boolean): Game { | 110 | fun getGame(uri: Uri, addedToLibrary: Boolean): Game? { |
| 102 | val filePath = uri.toString() | 111 | val filePath = uri.toString() |
| 112 | if (!GameMetadata.getIsValid(filePath)) { | ||
| 113 | return null | ||
| 114 | } | ||
| 115 | |||
| 116 | // Needed to update installed content information | ||
| 117 | NativeLibrary.addFileToFilesystemProvider(filePath) | ||
| 118 | |||
| 103 | var name = GameMetadata.getTitle(filePath) | 119 | var name = GameMetadata.getTitle(filePath) |
| 104 | 120 | ||
| 105 | // If the game's title field is empty, use the filename. | 121 | // If the game's title field is empty, use the filename. |
| @@ -118,7 +134,7 @@ object GameHelper { | |||
| 118 | filePath, | 134 | filePath, |
| 119 | programId, | 135 | programId, |
| 120 | GameMetadata.getDeveloper(filePath), | 136 | GameMetadata.getDeveloper(filePath), |
| 121 | GameMetadata.getVersion(filePath), | 137 | GameMetadata.getVersion(filePath, false), |
| 122 | GameMetadata.getIsHomebrew(filePath) | 138 | GameMetadata.getIsHomebrew(filePath) |
| 123 | ) | 139 | ) |
| 124 | 140 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameMetadata.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameMetadata.kt index 0f3542ac6..8e412482a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameMetadata.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameMetadata.kt | |||
| @@ -4,13 +4,15 @@ | |||
| 4 | package org.yuzu.yuzu_emu.utils | 4 | package org.yuzu.yuzu_emu.utils |
| 5 | 5 | ||
| 6 | object GameMetadata { | 6 | object GameMetadata { |
| 7 | external fun getIsValid(path: String): Boolean | ||
| 8 | |||
| 7 | external fun getTitle(path: String): String | 9 | external fun getTitle(path: String): String |
| 8 | 10 | ||
| 9 | external fun getProgramId(path: String): String | 11 | external fun getProgramId(path: String): String |
| 10 | 12 | ||
| 11 | external fun getDeveloper(path: String): String | 13 | external fun getDeveloper(path: String): String |
| 12 | 14 | ||
| 13 | external fun getVersion(path: String): String | 15 | external fun getVersion(path: String, reload: Boolean): String |
| 14 | 16 | ||
| 15 | external fun getIcon(path: String): ByteArray | 17 | external fun getIcon(path: String): ByteArray |
| 16 | 18 | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt index f6882ce6c..685272288 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt | |||
| @@ -10,6 +10,8 @@ import java.io.File | |||
| 10 | import java.io.IOException | 10 | import java.io.IOException |
| 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.features.settings.model.StringSetting | ||
| 14 | import java.io.FileNotFoundException | ||
| 13 | import java.util.zip.ZipException | 15 | import java.util.zip.ZipException |
| 14 | import java.util.zip.ZipFile | 16 | import java.util.zip.ZipFile |
| 15 | 17 | ||
| @@ -44,7 +46,7 @@ object GpuDriverHelper { | |||
| 44 | NativeLibrary.initializeGpuDriver( | 46 | NativeLibrary.initializeGpuDriver( |
| 45 | hookLibPath, | 47 | hookLibPath, |
| 46 | driverInstallationPath, | 48 | driverInstallationPath, |
| 47 | customDriverData.libraryName, | 49 | installedCustomDriverData.libraryName, |
| 48 | fileRedirectionPath | 50 | fileRedirectionPath |
| 49 | ) | 51 | ) |
| 50 | } | 52 | } |
| @@ -190,6 +192,7 @@ object GpuDriverHelper { | |||
| 190 | } | 192 | } |
| 191 | } | 193 | } |
| 192 | } catch (_: ZipException) { | 194 | } catch (_: ZipException) { |
| 195 | } catch (_: FileNotFoundException) { | ||
| 193 | } | 196 | } |
| 194 | return GpuDriverMetadata() | 197 | return GpuDriverMetadata() |
| 195 | } | 198 | } |
| @@ -197,9 +200,12 @@ object GpuDriverHelper { | |||
| 197 | external fun supportsCustomDriverLoading(): Boolean | 200 | external fun supportsCustomDriverLoading(): Boolean |
| 198 | 201 | ||
| 199 | // Parse the custom driver metadata to retrieve the name. | 202 | // Parse the custom driver metadata to retrieve the name. |
| 200 | val customDriverData: GpuDriverMetadata | 203 | val installedCustomDriverData: GpuDriverMetadata |
| 201 | get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME)) | 204 | get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME)) |
| 202 | 205 | ||
| 206 | val customDriverSettingData: GpuDriverMetadata | ||
| 207 | get() = getMetadataFromZip(File(StringSetting.DRIVER_PATH.getString())) | ||
| 208 | |||
| 203 | fun initializeDirectories() { | 209 | fun initializeDirectories() { |
| 204 | // Ensure the file redirection directory exists. | 210 | // Ensure the file redirection directory exists. |
| 205 | val fileRedirectionDir = File(fileRedirectionPath!!) | 211 | val fileRedirectionDir = File(fileRedirectionPath!!) |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt index 9076a86c4..0b94c73e5 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt | |||
| @@ -27,13 +27,13 @@ object MemoryUtil { | |||
| 27 | const val Pb = Tb * 1024 | 27 | const val Pb = Tb * 1024 |
| 28 | const val Eb = Pb * 1024 | 28 | const val Eb = Pb * 1024 |
| 29 | 29 | ||
| 30 | private fun bytesToSizeUnit(size: Float, roundUp: Boolean = false): String = | 30 | fun bytesToSizeUnit(size: Float, roundUp: Boolean = false): String = |
| 31 | when { | 31 | when { |
| 32 | size < Kb -> { | 32 | size < Kb -> { |
| 33 | context.getString( | 33 | context.getString( |
| 34 | R.string.memory_formatted, | 34 | R.string.memory_formatted, |
| 35 | size.hundredths, | 35 | size.hundredths, |
| 36 | context.getString(R.string.memory_byte) | 36 | context.getString(R.string.memory_byte_shorthand) |
| 37 | ) | 37 | ) |
| 38 | } | 38 | } |
| 39 | size < Mb -> { | 39 | size < Mb -> { |
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 f4e1bb13f..7512d5eed 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 | |||
| @@ -7,56 +7,113 @@ import org.yuzu.yuzu_emu.model.GameDir | |||
| 7 | 7 | ||
| 8 | object NativeConfig { | 8 | object NativeConfig { |
| 9 | /** | 9 | /** |
| 10 | * Creates a Config object and opens the emulation config. | 10 | * Loads global config. |
| 11 | */ | 11 | */ |
| 12 | @Synchronized | 12 | @Synchronized |
| 13 | external fun initializeConfig() | 13 | external fun initializeGlobalConfig() |
| 14 | 14 | ||
| 15 | /** | 15 | /** |
| 16 | * Destroys the stored config object. This automatically saves the existing config. | 16 | * Destroys the stored global config object. This does not save the existing config. |
| 17 | */ | 17 | */ |
| 18 | @Synchronized | 18 | @Synchronized |
| 19 | external fun unloadConfig() | 19 | external fun unloadGlobalConfig() |
| 20 | 20 | ||
| 21 | /** | 21 | /** |
| 22 | * Reads values saved to the config file and saves them. | 22 | * Reads values in the global config file and saves them. |
| 23 | */ | 23 | */ |
| 24 | @Synchronized | 24 | @Synchronized |
| 25 | external fun reloadSettings() | 25 | external fun reloadGlobalConfig() |
| 26 | 26 | ||
| 27 | /** | 27 | /** |
| 28 | * Saves settings values in memory to disk. | 28 | * Saves global settings values in memory to disk. |
| 29 | */ | 29 | */ |
| 30 | @Synchronized | 30 | @Synchronized |
| 31 | external fun saveSettings() | 31 | external fun saveGlobalConfig() |
| 32 | 32 | ||
| 33 | external fun getBoolean(key: String, getDefault: Boolean): Boolean | 33 | /** |
| 34 | * Creates per-game config for the specified parameters. Must be unloaded once per-game config | ||
| 35 | * is closed with [unloadPerGameConfig]. All switchable values that [NativeConfig] gets/sets | ||
| 36 | * will follow the per-game config until the global config is reloaded. | ||
| 37 | * | ||
| 38 | * @param programId String representation of the u64 programId | ||
| 39 | * @param fileName Filename of the game, including its extension | ||
| 40 | */ | ||
| 41 | @Synchronized | ||
| 42 | external fun initializePerGameConfig(programId: String, fileName: String) | ||
| 43 | |||
| 44 | @Synchronized | ||
| 45 | external fun isPerGameConfigLoaded(): Boolean | ||
| 46 | |||
| 47 | /** | ||
| 48 | * Saves per-game settings values in memory to disk. | ||
| 49 | */ | ||
| 50 | @Synchronized | ||
| 51 | external fun savePerGameConfig() | ||
| 52 | |||
| 53 | /** | ||
| 54 | * Destroys the stored per-game config object. This does not save the config. | ||
| 55 | */ | ||
| 56 | @Synchronized | ||
| 57 | external fun unloadPerGameConfig() | ||
| 58 | |||
| 59 | @Synchronized | ||
| 60 | external fun getBoolean(key: String, needsGlobal: Boolean): Boolean | ||
| 61 | |||
| 62 | @Synchronized | ||
| 34 | external fun setBoolean(key: String, value: Boolean) | 63 | external fun setBoolean(key: String, value: Boolean) |
| 35 | 64 | ||
| 36 | external fun getByte(key: String, getDefault: Boolean): Byte | 65 | @Synchronized |
| 66 | external fun getByte(key: String, needsGlobal: Boolean): Byte | ||
| 67 | |||
| 68 | @Synchronized | ||
| 37 | external fun setByte(key: String, value: Byte) | 69 | external fun setByte(key: String, value: Byte) |
| 38 | 70 | ||
| 39 | external fun getShort(key: String, getDefault: Boolean): Short | 71 | @Synchronized |
| 72 | external fun getShort(key: String, needsGlobal: Boolean): Short | ||
| 73 | |||
| 74 | @Synchronized | ||
| 40 | external fun setShort(key: String, value: Short) | 75 | external fun setShort(key: String, value: Short) |
| 41 | 76 | ||
| 42 | external fun getInt(key: String, getDefault: Boolean): Int | 77 | @Synchronized |
| 78 | external fun getInt(key: String, needsGlobal: Boolean): Int | ||
| 79 | |||
| 80 | @Synchronized | ||
| 43 | external fun setInt(key: String, value: Int) | 81 | external fun setInt(key: String, value: Int) |
| 44 | 82 | ||
| 45 | external fun getFloat(key: String, getDefault: Boolean): Float | 83 | @Synchronized |
| 84 | external fun getFloat(key: String, needsGlobal: Boolean): Float | ||
| 85 | |||
| 86 | @Synchronized | ||
| 46 | external fun setFloat(key: String, value: Float) | 87 | external fun setFloat(key: String, value: Float) |
| 47 | 88 | ||
| 48 | external fun getLong(key: String, getDefault: Boolean): Long | 89 | @Synchronized |
| 90 | external fun getLong(key: String, needsGlobal: Boolean): Long | ||
| 91 | |||
| 92 | @Synchronized | ||
| 49 | external fun setLong(key: String, value: Long) | 93 | external fun setLong(key: String, value: Long) |
| 50 | 94 | ||
| 51 | external fun getString(key: String, getDefault: Boolean): String | 95 | @Synchronized |
| 96 | external fun getString(key: String, needsGlobal: Boolean): String | ||
| 97 | |||
| 98 | @Synchronized | ||
| 52 | external fun setString(key: String, value: String) | 99 | external fun setString(key: String, value: String) |
| 53 | 100 | ||
| 54 | external fun getIsRuntimeModifiable(key: String): Boolean | 101 | external fun getIsRuntimeModifiable(key: String): Boolean |
| 55 | 102 | ||
| 56 | external fun getConfigHeader(category: Int): String | ||
| 57 | |||
| 58 | external fun getPairedSettingKey(key: String): String | 103 | external fun getPairedSettingKey(key: String): String |
| 59 | 104 | ||
| 105 | external fun getIsSwitchable(key: String): Boolean | ||
| 106 | |||
| 107 | @Synchronized | ||
| 108 | external fun usingGlobal(key: String): Boolean | ||
| 109 | |||
| 110 | @Synchronized | ||
| 111 | external fun setGlobal(key: String, global: Boolean) | ||
| 112 | |||
| 113 | external fun getIsSaveable(key: String): Boolean | ||
| 114 | |||
| 115 | external fun getDefaultToString(key: String): String | ||
| 116 | |||
| 60 | /** | 117 | /** |
| 61 | * Gets every [GameDir] in AndroidSettings::values.game_dirs | 118 | * Gets every [GameDir] in AndroidSettings::values.game_dirs |
| 62 | */ | 119 | */ |
| @@ -74,4 +131,23 @@ object NativeConfig { | |||
| 74 | */ | 131 | */ |
| 75 | @Synchronized | 132 | @Synchronized |
| 76 | external fun addGameDir(dir: GameDir) | 133 | external fun addGameDir(dir: GameDir) |
| 134 | |||
| 135 | /** | ||
| 136 | * Gets an array of the addons that are disabled for a given game | ||
| 137 | * | ||
| 138 | * @param programId String representation of a game's program ID | ||
| 139 | * @return An array of disabled addons | ||
| 140 | */ | ||
| 141 | @Synchronized | ||
| 142 | external fun getDisabledAddons(programId: String): Array<String> | ||
| 143 | |||
| 144 | /** | ||
| 145 | * Clears the disabled addons array corresponding to [programId] and replaces them | ||
| 146 | * with [disabledAddons] | ||
| 147 | * | ||
| 148 | * @param programId String representation of a game's program ID | ||
| 149 | * @param disabledAddons Replacement array of disabled addons | ||
| 150 | */ | ||
| 151 | @Synchronized | ||
| 152 | external fun setDisabledAddons(programId: String, disabledAddons: Array<String>) | ||
| 77 | } | 153 | } |
diff --git a/src/android/app/src/main/jni/android_config.cpp b/src/android/app/src/main/jni/android_config.cpp index 767d8ea83..9c3a5a9b2 100644 --- a/src/android/app/src/main/jni/android_config.cpp +++ b/src/android/app/src/main/jni/android_config.cpp | |||
| @@ -36,6 +36,7 @@ void AndroidConfig::ReadAndroidValues() { | |||
| 36 | ReadAndroidUIValues(); | 36 | ReadAndroidUIValues(); |
| 37 | ReadUIValues(); | 37 | ReadUIValues(); |
| 38 | } | 38 | } |
| 39 | ReadDriverValues(); | ||
| 39 | } | 40 | } |
| 40 | 41 | ||
| 41 | void AndroidConfig::ReadAndroidUIValues() { | 42 | void AndroidConfig::ReadAndroidUIValues() { |
| @@ -57,6 +58,7 @@ void AndroidConfig::ReadUIValues() { | |||
| 57 | void AndroidConfig::ReadPathValues() { | 58 | void AndroidConfig::ReadPathValues() { |
| 58 | BeginGroup(Settings::TranslateCategory(Settings::Category::Paths)); | 59 | BeginGroup(Settings::TranslateCategory(Settings::Category::Paths)); |
| 59 | 60 | ||
| 61 | AndroidSettings::values.game_dirs.clear(); | ||
| 60 | const int gamedirs_size = BeginArray(std::string("gamedirs")); | 62 | const int gamedirs_size = BeginArray(std::string("gamedirs")); |
| 61 | for (int i = 0; i < gamedirs_size; ++i) { | 63 | for (int i = 0; i < gamedirs_size; ++i) { |
| 62 | SetArrayIndex(i); | 64 | SetArrayIndex(i); |
| @@ -71,11 +73,20 @@ void AndroidConfig::ReadPathValues() { | |||
| 71 | EndGroup(); | 73 | EndGroup(); |
| 72 | } | 74 | } |
| 73 | 75 | ||
| 76 | void AndroidConfig::ReadDriverValues() { | ||
| 77 | BeginGroup(Settings::TranslateCategory(Settings::Category::GpuDriver)); | ||
| 78 | |||
| 79 | ReadCategory(Settings::Category::GpuDriver); | ||
| 80 | |||
| 81 | EndGroup(); | ||
| 82 | } | ||
| 83 | |||
| 74 | void AndroidConfig::SaveAndroidValues() { | 84 | void AndroidConfig::SaveAndroidValues() { |
| 75 | if (global) { | 85 | if (global) { |
| 76 | SaveAndroidUIValues(); | 86 | SaveAndroidUIValues(); |
| 77 | SaveUIValues(); | 87 | SaveUIValues(); |
| 78 | } | 88 | } |
| 89 | SaveDriverValues(); | ||
| 79 | 90 | ||
| 80 | WriteToIni(); | 91 | WriteToIni(); |
| 81 | } | 92 | } |
| @@ -111,6 +122,14 @@ void AndroidConfig::SavePathValues() { | |||
| 111 | EndGroup(); | 122 | EndGroup(); |
| 112 | } | 123 | } |
| 113 | 124 | ||
| 125 | void AndroidConfig::SaveDriverValues() { | ||
| 126 | BeginGroup(Settings::TranslateCategory(Settings::Category::GpuDriver)); | ||
| 127 | |||
| 128 | WriteCategory(Settings::Category::GpuDriver); | ||
| 129 | |||
| 130 | EndGroup(); | ||
| 131 | } | ||
| 132 | |||
| 114 | std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) { | 133 | std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) { |
| 115 | auto& map = Settings::values.linkage.by_category; | 134 | auto& map = Settings::values.linkage.by_category; |
| 116 | if (map.contains(category)) { | 135 | 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 f490be016..2c12874e1 100644 --- a/src/android/app/src/main/jni/android_config.h +++ b/src/android/app/src/main/jni/android_config.h | |||
| @@ -17,6 +17,7 @@ public: | |||
| 17 | protected: | 17 | protected: |
| 18 | void ReadAndroidValues(); | 18 | void ReadAndroidValues(); |
| 19 | void ReadAndroidUIValues(); | 19 | void ReadAndroidUIValues(); |
| 20 | void ReadDriverValues(); | ||
| 20 | void ReadHidbusValues() override {} | 21 | void ReadHidbusValues() override {} |
| 21 | void ReadDebugControlValues() override {} | 22 | void ReadDebugControlValues() override {} |
| 22 | void ReadPathValues() override; | 23 | void ReadPathValues() override; |
| @@ -28,6 +29,7 @@ protected: | |||
| 28 | 29 | ||
| 29 | void SaveAndroidValues(); | 30 | void SaveAndroidValues(); |
| 30 | void SaveAndroidUIValues(); | 31 | void SaveAndroidUIValues(); |
| 32 | void SaveDriverValues(); | ||
| 31 | void SaveHidbusValues() override {} | 33 | void SaveHidbusValues() override {} |
| 32 | void SaveDebugControlValues() override {} | 34 | void SaveDebugControlValues() override {} |
| 33 | void SavePathValues() override; | 35 | void SavePathValues() override; |
diff --git a/src/android/app/src/main/jni/android_settings.h b/src/android/app/src/main/jni/android_settings.h index fc0523206..3733f5a3c 100644 --- a/src/android/app/src/main/jni/android_settings.h +++ b/src/android/app/src/main/jni/android_settings.h | |||
| @@ -30,6 +30,9 @@ struct Values { | |||
| 30 | Settings::Specialization::Default, | 30 | Settings::Specialization::Default, |
| 31 | true, | 31 | true, |
| 32 | true}; | 32 | true}; |
| 33 | |||
| 34 | Settings::SwitchableSetting<std::string, false> driver_path{linkage, "", "driver_path", | ||
| 35 | Settings::Category::GpuDriver}; | ||
| 33 | }; | 36 | }; |
| 34 | 37 | ||
| 35 | extern Values values; | 38 | extern Values values; |
diff --git a/src/android/app/src/main/jni/game_metadata.cpp b/src/android/app/src/main/jni/game_metadata.cpp index 24d9df702..78f604c70 100644 --- a/src/android/app/src/main/jni/game_metadata.cpp +++ b/src/android/app/src/main/jni/game_metadata.cpp | |||
| @@ -2,6 +2,7 @@ | |||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | 2 | // SPDX-License-Identifier: GPL-2.0-or-later |
| 3 | 3 | ||
| 4 | #include <core/core.h> | 4 | #include <core/core.h> |
| 5 | #include <core/file_sys/mode.h> | ||
| 5 | #include <core/file_sys/patch_manager.h> | 6 | #include <core/file_sys/patch_manager.h> |
| 6 | #include <core/loader/nro.h> | 7 | #include <core/loader/nro.h> |
| 7 | #include <jni.h> | 8 | #include <jni.h> |
| @@ -61,7 +62,11 @@ RomMetadata CacheRomMetadata(const std::string& path) { | |||
| 61 | return entry; | 62 | return entry; |
| 62 | } | 63 | } |
| 63 | 64 | ||
| 64 | RomMetadata GetRomMetadata(const std::string& path) { | 65 | RomMetadata GetRomMetadata(const std::string& path, bool reload = false) { |
| 66 | if (reload) { | ||
| 67 | return CacheRomMetadata(path); | ||
| 68 | } | ||
| 69 | |||
| 65 | if (auto search = m_rom_metadata_cache.find(path); search != m_rom_metadata_cache.end()) { | 70 | if (auto search = m_rom_metadata_cache.find(path); search != m_rom_metadata_cache.end()) { |
| 66 | return search->second; | 71 | return search->second; |
| 67 | } | 72 | } |
| @@ -71,6 +76,32 @@ RomMetadata GetRomMetadata(const std::string& path) { | |||
| 71 | 76 | ||
| 72 | extern "C" { | 77 | extern "C" { |
| 73 | 78 | ||
| 79 | jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsValid(JNIEnv* env, jobject obj, | ||
| 80 | jstring jpath) { | ||
| 81 | const auto file = EmulationSession::GetInstance().System().GetFilesystem()->OpenFile( | ||
| 82 | GetJString(env, jpath), FileSys::Mode::Read); | ||
| 83 | if (!file) { | ||
| 84 | return false; | ||
| 85 | } | ||
| 86 | |||
| 87 | auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file); | ||
| 88 | if (!loader) { | ||
| 89 | return false; | ||
| 90 | } | ||
| 91 | |||
| 92 | const auto file_type = loader->GetFileType(); | ||
| 93 | if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) { | ||
| 94 | return false; | ||
| 95 | } | ||
| 96 | |||
| 97 | u64 program_id = 0; | ||
| 98 | Loader::ResultStatus res = loader->ReadProgramId(program_id); | ||
| 99 | if (res != Loader::ResultStatus::Success) { | ||
| 100 | return false; | ||
| 101 | } | ||
| 102 | return true; | ||
| 103 | } | ||
| 104 | |||
| 74 | jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getTitle(JNIEnv* env, jobject obj, | 105 | jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getTitle(JNIEnv* env, jobject obj, |
| 75 | jstring jpath) { | 106 | jstring jpath) { |
| 76 | return ToJString(env, GetRomMetadata(GetJString(env, jpath)).title); | 107 | return ToJString(env, GetRomMetadata(GetJString(env, jpath)).title); |
| @@ -87,8 +118,8 @@ jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getDeveloper(JNIEnv* env, job | |||
| 87 | } | 118 | } |
| 88 | 119 | ||
| 89 | jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getVersion(JNIEnv* env, jobject obj, | 120 | jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getVersion(JNIEnv* env, jobject obj, |
| 90 | jstring jpath) { | 121 | jstring jpath, jboolean jreload) { |
| 91 | return ToJString(env, GetRomMetadata(GetJString(env, jpath)).version); | 122 | return ToJString(env, GetRomMetadata(GetJString(env, jpath), jreload).version); |
| 92 | } | 123 | } |
| 93 | 124 | ||
| 94 | jbyteArray Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIcon(JNIEnv* env, jobject obj, | 125 | jbyteArray Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIcon(JNIEnv* env, jobject obj, |
| @@ -106,7 +137,7 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsHomebrew(JNIEnv* env, j | |||
| 106 | } | 137 | } |
| 107 | 138 | ||
| 108 | void Java_org_yuzu_yuzu_1emu_utils_GameMetadata_resetMetadata(JNIEnv* env, jobject obj) { | 139 | void Java_org_yuzu_yuzu_1emu_utils_GameMetadata_resetMetadata(JNIEnv* env, jobject obj) { |
| 109 | return m_rom_metadata_cache.clear(); | 140 | m_rom_metadata_cache.clear(); |
| 110 | } | 141 | } |
| 111 | 142 | ||
| 112 | } // extern "C" | 143 | } // extern "C" |
diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp index a56ed5662..e7a86d3fd 100644 --- a/src/android/app/src/main/jni/id_cache.cpp +++ b/src/android/app/src/main/jni/id_cache.cpp | |||
| @@ -20,6 +20,21 @@ static jmethodID s_disk_cache_load_progress; | |||
| 20 | static jmethodID s_on_emulation_started; | 20 | static jmethodID s_on_emulation_started; |
| 21 | static jmethodID s_on_emulation_stopped; | 21 | static jmethodID s_on_emulation_stopped; |
| 22 | 22 | ||
| 23 | static jclass s_game_class; | ||
| 24 | static jmethodID s_game_constructor; | ||
| 25 | static jfieldID s_game_title_field; | ||
| 26 | static jfieldID s_game_path_field; | ||
| 27 | static jfieldID s_game_program_id_field; | ||
| 28 | static jfieldID s_game_developer_field; | ||
| 29 | static jfieldID s_game_version_field; | ||
| 30 | static jfieldID s_game_is_homebrew_field; | ||
| 31 | |||
| 32 | static jclass s_string_class; | ||
| 33 | static jclass s_pair_class; | ||
| 34 | static jmethodID s_pair_constructor; | ||
| 35 | static jfieldID s_pair_first_field; | ||
| 36 | static jfieldID s_pair_second_field; | ||
| 37 | |||
| 23 | static constexpr jint JNI_VERSION = JNI_VERSION_1_6; | 38 | static constexpr jint JNI_VERSION = JNI_VERSION_1_6; |
| 24 | 39 | ||
| 25 | namespace IDCache { | 40 | namespace IDCache { |
| @@ -79,6 +94,58 @@ jmethodID GetOnEmulationStopped() { | |||
| 79 | return s_on_emulation_stopped; | 94 | return s_on_emulation_stopped; |
| 80 | } | 95 | } |
| 81 | 96 | ||
| 97 | jclass GetGameClass() { | ||
| 98 | return s_game_class; | ||
| 99 | } | ||
| 100 | |||
| 101 | jmethodID GetGameConstructor() { | ||
| 102 | return s_game_constructor; | ||
| 103 | } | ||
| 104 | |||
| 105 | jfieldID GetGameTitleField() { | ||
| 106 | return s_game_title_field; | ||
| 107 | } | ||
| 108 | |||
| 109 | jfieldID GetGamePathField() { | ||
| 110 | return s_game_path_field; | ||
| 111 | } | ||
| 112 | |||
| 113 | jfieldID GetGameProgramIdField() { | ||
| 114 | return s_game_program_id_field; | ||
| 115 | } | ||
| 116 | |||
| 117 | jfieldID GetGameDeveloperField() { | ||
| 118 | return s_game_developer_field; | ||
| 119 | } | ||
| 120 | |||
| 121 | jfieldID GetGameVersionField() { | ||
| 122 | return s_game_version_field; | ||
| 123 | } | ||
| 124 | |||
| 125 | jfieldID GetGameIsHomebrewField() { | ||
| 126 | return s_game_is_homebrew_field; | ||
| 127 | } | ||
| 128 | |||
| 129 | jclass GetStringClass() { | ||
| 130 | return s_string_class; | ||
| 131 | } | ||
| 132 | |||
| 133 | jclass GetPairClass() { | ||
| 134 | return s_pair_class; | ||
| 135 | } | ||
| 136 | |||
| 137 | jmethodID GetPairConstructor() { | ||
| 138 | return s_pair_constructor; | ||
| 139 | } | ||
| 140 | |||
| 141 | jfieldID GetPairFirstField() { | ||
| 142 | return s_pair_first_field; | ||
| 143 | } | ||
| 144 | |||
| 145 | jfieldID GetPairSecondField() { | ||
| 146 | return s_pair_second_field; | ||
| 147 | } | ||
| 148 | |||
| 82 | } // namespace IDCache | 149 | } // namespace IDCache |
| 83 | 150 | ||
| 84 | #ifdef __cplusplus | 151 | #ifdef __cplusplus |
| @@ -115,6 +182,31 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { | |||
| 115 | s_on_emulation_stopped = | 182 | s_on_emulation_stopped = |
| 116 | env->GetStaticMethodID(s_native_library_class, "onEmulationStopped", "(I)V"); | 183 | env->GetStaticMethodID(s_native_library_class, "onEmulationStopped", "(I)V"); |
| 117 | 184 | ||
| 185 | const jclass game_class = env->FindClass("org/yuzu/yuzu_emu/model/Game"); | ||
| 186 | s_game_class = reinterpret_cast<jclass>(env->NewGlobalRef(game_class)); | ||
| 187 | s_game_constructor = env->GetMethodID(game_class, "<init>", | ||
| 188 | "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/" | ||
| 189 | "String;Ljava/lang/String;Ljava/lang/String;Z)V"); | ||
| 190 | s_game_title_field = env->GetFieldID(game_class, "title", "Ljava/lang/String;"); | ||
| 191 | s_game_path_field = env->GetFieldID(game_class, "path", "Ljava/lang/String;"); | ||
| 192 | s_game_program_id_field = env->GetFieldID(game_class, "programId", "Ljava/lang/String;"); | ||
| 193 | s_game_developer_field = env->GetFieldID(game_class, "developer", "Ljava/lang/String;"); | ||
| 194 | s_game_version_field = env->GetFieldID(game_class, "version", "Ljava/lang/String;"); | ||
| 195 | s_game_is_homebrew_field = env->GetFieldID(game_class, "isHomebrew", "Z"); | ||
| 196 | env->DeleteLocalRef(game_class); | ||
| 197 | |||
| 198 | const jclass string_class = env->FindClass("java/lang/String"); | ||
| 199 | s_string_class = reinterpret_cast<jclass>(env->NewGlobalRef(string_class)); | ||
| 200 | env->DeleteLocalRef(string_class); | ||
| 201 | |||
| 202 | const jclass pair_class = env->FindClass("kotlin/Pair"); | ||
| 203 | s_pair_class = reinterpret_cast<jclass>(env->NewGlobalRef(pair_class)); | ||
| 204 | s_pair_constructor = | ||
| 205 | env->GetMethodID(pair_class, "<init>", "(Ljava/lang/Object;Ljava/lang/Object;)V"); | ||
| 206 | s_pair_first_field = env->GetFieldID(pair_class, "first", "Ljava/lang/Object;"); | ||
| 207 | s_pair_second_field = env->GetFieldID(pair_class, "second", "Ljava/lang/Object;"); | ||
| 208 | env->DeleteLocalRef(pair_class); | ||
| 209 | |||
| 118 | // Initialize Android Storage | 210 | // Initialize Android Storage |
| 119 | Common::FS::Android::RegisterCallbacks(env, s_native_library_class); | 211 | Common::FS::Android::RegisterCallbacks(env, s_native_library_class); |
| 120 | 212 | ||
| @@ -136,6 +228,9 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { | |||
| 136 | env->DeleteGlobalRef(s_disk_cache_progress_class); | 228 | env->DeleteGlobalRef(s_disk_cache_progress_class); |
| 137 | env->DeleteGlobalRef(s_load_callback_stage_class); | 229 | env->DeleteGlobalRef(s_load_callback_stage_class); |
| 138 | env->DeleteGlobalRef(s_game_dir_class); | 230 | env->DeleteGlobalRef(s_game_dir_class); |
| 231 | env->DeleteGlobalRef(s_game_class); | ||
| 232 | env->DeleteGlobalRef(s_string_class); | ||
| 233 | env->DeleteGlobalRef(s_pair_class); | ||
| 139 | 234 | ||
| 140 | // UnInitialize applets | 235 | // UnInitialize applets |
| 141 | SoftwareKeyboard::CleanupJNI(env); | 236 | 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 855649efa..24030be42 100644 --- a/src/android/app/src/main/jni/id_cache.h +++ b/src/android/app/src/main/jni/id_cache.h | |||
| @@ -20,4 +20,19 @@ jmethodID GetDiskCacheLoadProgress(); | |||
| 20 | jmethodID GetOnEmulationStarted(); | 20 | jmethodID GetOnEmulationStarted(); |
| 21 | jmethodID GetOnEmulationStopped(); | 21 | jmethodID GetOnEmulationStopped(); |
| 22 | 22 | ||
| 23 | jclass GetGameClass(); | ||
| 24 | jmethodID GetGameConstructor(); | ||
| 25 | jfieldID GetGameTitleField(); | ||
| 26 | jfieldID GetGamePathField(); | ||
| 27 | jfieldID GetGameProgramIdField(); | ||
| 28 | jfieldID GetGameDeveloperField(); | ||
| 29 | jfieldID GetGameVersionField(); | ||
| 30 | jfieldID GetGameIsHomebrewField(); | ||
| 31 | |||
| 32 | jclass GetStringClass(); | ||
| 33 | jclass GetPairClass(); | ||
| 34 | jmethodID GetPairConstructor(); | ||
| 35 | jfieldID GetPairFirstField(); | ||
| 36 | jfieldID GetPairSecondField(); | ||
| 37 | |||
| 23 | } // namespace IDCache | 38 | } // namespace IDCache |
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index e5d3158c8..0c1db7d46 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp | |||
| @@ -14,6 +14,7 @@ | |||
| 14 | #include <android/api-level.h> | 14 | #include <android/api-level.h> |
| 15 | #include <android/native_window_jni.h> | 15 | #include <android/native_window_jni.h> |
| 16 | #include <common/fs/fs.h> | 16 | #include <common/fs/fs.h> |
| 17 | #include <core/file_sys/patch_manager.h> | ||
| 17 | #include <core/file_sys/savedata_factory.h> | 18 | #include <core/file_sys/savedata_factory.h> |
| 18 | #include <core/loader/nro.h> | 19 | #include <core/loader/nro.h> |
| 19 | #include <jni.h> | 20 | #include <jni.h> |
| @@ -79,6 +80,10 @@ Core::System& EmulationSession::System() { | |||
| 79 | return m_system; | 80 | return m_system; |
| 80 | } | 81 | } |
| 81 | 82 | ||
| 83 | FileSys::ManualContentProvider* EmulationSession::GetContentProvider() { | ||
| 84 | return m_manual_provider.get(); | ||
| 85 | } | ||
| 86 | |||
| 82 | const EmuWindow_Android& EmulationSession::Window() const { | 87 | const EmuWindow_Android& EmulationSession::Window() const { |
| 83 | return *m_window; | 88 | return *m_window; |
| 84 | } | 89 | } |
| @@ -455,6 +460,15 @@ void EmulationSession::OnEmulationStopped(Core::SystemResultStatus result) { | |||
| 455 | static_cast<jint>(result)); | 460 | static_cast<jint>(result)); |
| 456 | } | 461 | } |
| 457 | 462 | ||
| 463 | u64 EmulationSession::GetProgramId(JNIEnv* env, jstring jprogramId) { | ||
| 464 | auto program_id_string = GetJString(env, jprogramId); | ||
| 465 | try { | ||
| 466 | return std::stoull(program_id_string); | ||
| 467 | } catch (...) { | ||
| 468 | return 0; | ||
| 469 | } | ||
| 470 | } | ||
| 471 | |||
| 458 | static Core::SystemResultStatus RunEmulation(const std::string& filepath) { | 472 | static Core::SystemResultStatus RunEmulation(const std::string& filepath) { |
| 459 | MicroProfileOnThreadCreate("EmuThread"); | 473 | MicroProfileOnThreadCreate("EmuThread"); |
| 460 | SCOPE_EXIT({ MicroProfileShutdown(); }); | 474 | SCOPE_EXIT({ MicroProfileShutdown(); }); |
| @@ -504,6 +518,27 @@ int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject | |||
| 504 | GetJString(env, j_file_extension)); | 518 | GetJString(env, j_file_extension)); |
| 505 | } | 519 | } |
| 506 | 520 | ||
| 521 | jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* env, jobject jobj, | ||
| 522 | jstring jprogramId, | ||
| 523 | jstring jupdatePath) { | ||
| 524 | u64 program_id = EmulationSession::GetProgramId(env, jprogramId); | ||
| 525 | std::string updatePath = GetJString(env, jupdatePath); | ||
| 526 | std::shared_ptr<FileSys::NSP> nsp = std::make_shared<FileSys::NSP>( | ||
| 527 | EmulationSession::GetInstance().System().GetFilesystem()->OpenFile(updatePath, | ||
| 528 | FileSys::Mode::Read)); | ||
| 529 | for (const auto& item : nsp->GetNCAs()) { | ||
| 530 | for (const auto& nca_details : item.second) { | ||
| 531 | if (nca_details.second->GetName().ends_with(".cnmt.nca")) { | ||
| 532 | auto update_id = nca_details.second->GetTitleId() & ~0xFFFULL; | ||
| 533 | if (update_id == program_id) { | ||
| 534 | return true; | ||
| 535 | } | ||
| 536 | } | ||
| 537 | } | ||
| 538 | } | ||
| 539 | return false; | ||
| 540 | } | ||
| 541 | |||
| 507 | void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env, jclass clazz, | 542 | void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env, jclass clazz, |
| 508 | jstring hook_lib_dir, | 543 | jstring hook_lib_dir, |
| 509 | jstring custom_driver_dir, | 544 | jstring custom_driver_dir, |
| @@ -665,13 +700,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass | |||
| 665 | EmulationSession::GetInstance().InitializeSystem(reload); | 700 | EmulationSession::GetInstance().InitializeSystem(reload); |
| 666 | } | 701 | } |
| 667 | 702 | ||
| 668 | jint Java_org_yuzu_yuzu_1emu_NativeLibrary_defaultCPUCore(JNIEnv* env, jclass clazz) { | ||
| 669 | return {}; | ||
| 670 | } | ||
| 671 | |||
| 672 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2Ljava_lang_String_2Z( | ||
| 673 | JNIEnv* env, jclass clazz, jstring j_file, jstring j_savestate, jboolean j_delete_savestate) {} | ||
| 674 | |||
| 675 | jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats(JNIEnv* env, jclass clazz) { | 703 | jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats(JNIEnv* env, jclass clazz) { |
| 676 | jdoubleArray j_stats = env->NewDoubleArray(4); | 704 | jdoubleArray j_stats = env->NewDoubleArray(4); |
| 677 | 705 | ||
| @@ -696,9 +724,13 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuBackend(JNIEnv* env, jclass | |||
| 696 | return ToJString(env, "JIT"); | 724 | return ToJString(env, "JIT"); |
| 697 | } | 725 | } |
| 698 | 726 | ||
| 699 | void Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_setSysDirectory(JNIEnv* env, | 727 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) { |
| 700 | jclass clazz, | 728 | EmulationSession::GetInstance().System().ApplySettings(); |
| 701 | jstring j_path) {} | 729 | } |
| 730 | |||
| 731 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_logSettings(JNIEnv* env, jobject jobj) { | ||
| 732 | Settings::LogSettings(); | ||
| 733 | } | ||
| 702 | 734 | ||
| 703 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2(JNIEnv* env, jclass clazz, | 735 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2(JNIEnv* env, jclass clazz, |
| 704 | jstring j_path) { | 736 | jstring j_path) { |
| @@ -792,4 +824,69 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env, | |||
| 792 | return true; | 824 | return true; |
| 793 | } | 825 | } |
| 794 | 826 | ||
| 827 | jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, jobject jobj, | ||
| 828 | jstring jpath, | ||
| 829 | jstring jprogramId) { | ||
| 830 | const auto path = GetJString(env, jpath); | ||
| 831 | const auto vFile = | ||
| 832 | Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path); | ||
| 833 | if (vFile == nullptr) { | ||
| 834 | return nullptr; | ||
| 835 | } | ||
| 836 | |||
| 837 | auto& system = EmulationSession::GetInstance().System(); | ||
| 838 | auto program_id = EmulationSession::GetProgramId(env, jprogramId); | ||
| 839 | const FileSys::PatchManager pm{program_id, system.GetFileSystemController(), | ||
| 840 | system.GetContentProvider()}; | ||
| 841 | const auto loader = Loader::GetLoader(system, vFile); | ||
| 842 | |||
| 843 | FileSys::VirtualFile update_raw; | ||
| 844 | loader->ReadUpdateRaw(update_raw); | ||
| 845 | |||
| 846 | auto addons = pm.GetPatchVersionNames(update_raw); | ||
| 847 | auto jemptyString = ToJString(env, ""); | ||
| 848 | auto jemptyStringPair = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(), | ||
| 849 | jemptyString, jemptyString); | ||
| 850 | jobjectArray jaddonsArray = | ||
| 851 | env->NewObjectArray(addons.size(), IDCache::GetPairClass(), jemptyStringPair); | ||
| 852 | int i = 0; | ||
| 853 | for (const auto& addon : addons) { | ||
| 854 | jobject jaddon = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(), | ||
| 855 | ToJString(env, addon.first), ToJString(env, addon.second)); | ||
| 856 | env->SetObjectArrayElement(jaddonsArray, i, jaddon); | ||
| 857 | ++i; | ||
| 858 | } | ||
| 859 | return jaddonsArray; | ||
| 860 | } | ||
| 861 | |||
| 862 | jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj, | ||
| 863 | jstring jprogramId) { | ||
| 864 | auto program_id = EmulationSession::GetProgramId(env, jprogramId); | ||
| 865 | |||
| 866 | auto& system = EmulationSession::GetInstance().System(); | ||
| 867 | |||
| 868 | Service::Account::ProfileManager manager; | ||
| 869 | // TODO: Pass in a selected user once we get the relevant UI working | ||
| 870 | const auto user_id = manager.GetUser(static_cast<std::size_t>(0)); | ||
| 871 | ASSERT(user_id); | ||
| 872 | |||
| 873 | const auto nandDir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir); | ||
| 874 | auto vfsNandDir = system.GetFilesystem()->OpenDirectory(Common::FS::PathToUTF8String(nandDir), | ||
| 875 | FileSys::Mode::Read); | ||
| 876 | |||
| 877 | const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath( | ||
| 878 | system, vfsNandDir, FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData, | ||
| 879 | program_id, user_id->AsU128(), 0); | ||
| 880 | return ToJString(env, user_save_data_path); | ||
| 881 | } | ||
| 882 | |||
| 883 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_addFileToFilesystemProvider(JNIEnv* env, jobject jobj, | ||
| 884 | jstring jpath) { | ||
| 885 | EmulationSession::GetInstance().ConfigureFilesystemProvider(GetJString(env, jpath)); | ||
| 886 | } | ||
| 887 | |||
| 888 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_clearFilesystemProvider(JNIEnv* env, jobject jobj) { | ||
| 889 | EmulationSession::GetInstance().GetContentProvider()->ClearAllEntries(); | ||
| 890 | } | ||
| 891 | |||
| 795 | } // extern "C" | 892 | } // extern "C" |
diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h index f1457bd1f..4a8049578 100644 --- a/src/android/app/src/main/jni/native.h +++ b/src/android/app/src/main/jni/native.h | |||
| @@ -21,6 +21,7 @@ public: | |||
| 21 | static EmulationSession& GetInstance(); | 21 | static EmulationSession& GetInstance(); |
| 22 | const Core::System& System() const; | 22 | const Core::System& System() const; |
| 23 | Core::System& System(); | 23 | Core::System& System(); |
| 24 | FileSys::ManualContentProvider* GetContentProvider(); | ||
| 24 | 25 | ||
| 25 | const EmuWindow_Android& Window() const; | 26 | const EmuWindow_Android& Window() const; |
| 26 | EmuWindow_Android& Window(); | 27 | EmuWindow_Android& Window(); |
| @@ -54,6 +55,8 @@ public: | |||
| 54 | 55 | ||
| 55 | static void OnEmulationStarted(); | 56 | static void OnEmulationStarted(); |
| 56 | 57 | ||
| 58 | static u64 GetProgramId(JNIEnv* env, jstring jprogramId); | ||
| 59 | |||
| 57 | private: | 60 | private: |
| 58 | static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max); | 61 | static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max); |
| 59 | static void OnEmulationStopped(Core::SystemResultStatus result); | 62 | static void OnEmulationStopped(Core::SystemResultStatus result); |
diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp index 763b2164c..324d9e9cd 100644 --- a/src/android/app/src/main/jni/native_config.cpp +++ b/src/android/app/src/main/jni/native_config.cpp | |||
| @@ -3,6 +3,7 @@ | |||
| 3 | 3 | ||
| 4 | #include <string> | 4 | #include <string> |
| 5 | 5 | ||
| 6 | #include <common/fs/fs_util.h> | ||
| 6 | #include <jni.h> | 7 | #include <jni.h> |
| 7 | 8 | ||
| 8 | #include "android_config.h" | 9 | #include "android_config.h" |
| @@ -12,19 +13,21 @@ | |||
| 12 | #include "frontend_common/config.h" | 13 | #include "frontend_common/config.h" |
| 13 | #include "jni/android_common/android_common.h" | 14 | #include "jni/android_common/android_common.h" |
| 14 | #include "jni/id_cache.h" | 15 | #include "jni/id_cache.h" |
| 16 | #include "native.h" | ||
| 15 | 17 | ||
| 16 | std::unique_ptr<AndroidConfig> config; | 18 | std::unique_ptr<AndroidConfig> global_config; |
| 19 | std::unique_ptr<AndroidConfig> per_game_config; | ||
| 17 | 20 | ||
| 18 | template <typename T> | 21 | template <typename T> |
| 19 | Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) { | 22 | Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) { |
| 20 | auto key = GetJString(env, jkey); | 23 | auto key = GetJString(env, jkey); |
| 21 | auto basicSetting = Settings::values.linkage.by_key[key]; | 24 | auto basic_setting = Settings::values.linkage.by_key[key]; |
| 22 | auto basicAndroidSetting = AndroidSettings::values.linkage.by_key[key]; | 25 | if (basic_setting != 0) { |
| 23 | if (basicSetting != 0) { | 26 | return static_cast<Settings::Setting<T>*>(basic_setting); |
| 24 | return static_cast<Settings::Setting<T>*>(basicSetting); | ||
| 25 | } | 27 | } |
| 26 | if (basicAndroidSetting != 0) { | 28 | auto basic_android_setting = AndroidSettings::values.linkage.by_key[key]; |
| 27 | return static_cast<Settings::Setting<T>*>(basicAndroidSetting); | 29 | if (basic_android_setting != 0) { |
| 30 | return static_cast<Settings::Setting<T>*>(basic_android_setting); | ||
| 28 | } | 31 | } |
| 29 | LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key); | 32 | LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key); |
| 30 | return nullptr; | 33 | return nullptr; |
| @@ -32,35 +35,52 @@ Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) { | |||
| 32 | 35 | ||
| 33 | extern "C" { | 36 | extern "C" { |
| 34 | 37 | ||
| 35 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeConfig(JNIEnv* env, jobject obj) { | 38 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeGlobalConfig(JNIEnv* env, jobject obj) { |
| 36 | config = std::make_unique<AndroidConfig>(); | 39 | global_config = std::make_unique<AndroidConfig>(); |
| 37 | } | 40 | } |
| 38 | 41 | ||
| 39 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadConfig(JNIEnv* env, jobject obj) { | 42 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadGlobalConfig(JNIEnv* env, jobject obj) { |
| 40 | config.reset(); | 43 | global_config.reset(); |
| 41 | } | 44 | } |
| 42 | 45 | ||
| 43 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_reloadSettings(JNIEnv* env, jobject obj) { | 46 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_reloadGlobalConfig(JNIEnv* env, jobject obj) { |
| 44 | config->AndroidConfig::ReloadAllValues(); | 47 | global_config->AndroidConfig::ReloadAllValues(); |
| 45 | } | 48 | } |
| 46 | 49 | ||
| 47 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveSettings(JNIEnv* env, jobject obj) { | 50 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveGlobalConfig(JNIEnv* env, jobject obj) { |
| 48 | config->AndroidConfig::SaveAllValues(); | 51 | global_config->AndroidConfig::SaveAllValues(); |
| 52 | } | ||
| 53 | |||
| 54 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializePerGameConfig(JNIEnv* env, jobject obj, | ||
| 55 | jstring jprogramId, | ||
| 56 | jstring jfileName) { | ||
| 57 | auto program_id = EmulationSession::GetProgramId(env, jprogramId); | ||
| 58 | auto file_name = GetJString(env, jfileName); | ||
| 59 | const auto config_file_name = program_id == 0 ? file_name : fmt::format("{:016X}", program_id); | ||
| 60 | per_game_config = | ||
| 61 | std::make_unique<AndroidConfig>(config_file_name, Config::ConfigType::PerGameConfig); | ||
| 62 | } | ||
| 63 | |||
| 64 | jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_isPerGameConfigLoaded(JNIEnv* env, | ||
| 65 | jobject obj) { | ||
| 66 | return per_game_config != nullptr; | ||
| 67 | } | ||
| 68 | |||
| 69 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_savePerGameConfig(JNIEnv* env, jobject obj) { | ||
| 70 | per_game_config->AndroidConfig::SaveAllValues(); | ||
| 71 | } | ||
| 72 | |||
| 73 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadPerGameConfig(JNIEnv* env, jobject obj) { | ||
| 74 | per_game_config.reset(); | ||
| 49 | } | 75 | } |
| 50 | 76 | ||
| 51 | jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getBoolean(JNIEnv* env, jobject obj, | 77 | jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getBoolean(JNIEnv* env, jobject obj, |
| 52 | jstring jkey, jboolean getDefault) { | 78 | jstring jkey, jboolean needGlobal) { |
| 53 | auto setting = getSetting<bool>(env, jkey); | 79 | auto setting = getSetting<bool>(env, jkey); |
| 54 | if (setting == nullptr) { | 80 | if (setting == nullptr) { |
| 55 | return false; | 81 | return false; |
| 56 | } | 82 | } |
| 57 | setting->SetGlobal(true); | 83 | return setting->GetValue(static_cast<bool>(needGlobal)); |
| 58 | |||
| 59 | if (static_cast<bool>(getDefault)) { | ||
| 60 | return setting->GetDefault(); | ||
| 61 | } | ||
| 62 | |||
| 63 | return setting->GetValue(); | ||
| 64 | } | 84 | } |
| 65 | 85 | ||
| 66 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setBoolean(JNIEnv* env, jobject obj, jstring jkey, | 86 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setBoolean(JNIEnv* env, jobject obj, jstring jkey, |
| @@ -69,23 +89,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setBoolean(JNIEnv* env, jobject | |||
| 69 | if (setting == nullptr) { | 89 | if (setting == nullptr) { |
| 70 | return; | 90 | return; |
| 71 | } | 91 | } |
| 72 | setting->SetGlobal(true); | ||
| 73 | setting->SetValue(static_cast<bool>(value)); | 92 | setting->SetValue(static_cast<bool>(value)); |
| 74 | } | 93 | } |
| 75 | 94 | ||
| 76 | jbyte Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getByte(JNIEnv* env, jobject obj, jstring jkey, | 95 | jbyte Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getByte(JNIEnv* env, jobject obj, jstring jkey, |
| 77 | jboolean getDefault) { | 96 | jboolean needGlobal) { |
| 78 | auto setting = getSetting<u8>(env, jkey); | 97 | auto setting = getSetting<u8>(env, jkey); |
| 79 | if (setting == nullptr) { | 98 | if (setting == nullptr) { |
| 80 | return -1; | 99 | return -1; |
| 81 | } | 100 | } |
| 82 | setting->SetGlobal(true); | 101 | return setting->GetValue(static_cast<bool>(needGlobal)); |
| 83 | |||
| 84 | if (static_cast<bool>(getDefault)) { | ||
| 85 | return setting->GetDefault(); | ||
| 86 | } | ||
| 87 | |||
| 88 | return setting->GetValue(); | ||
| 89 | } | 102 | } |
| 90 | 103 | ||
| 91 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setByte(JNIEnv* env, jobject obj, jstring jkey, | 104 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setByte(JNIEnv* env, jobject obj, jstring jkey, |
| @@ -94,23 +107,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setByte(JNIEnv* env, jobject obj | |||
| 94 | if (setting == nullptr) { | 107 | if (setting == nullptr) { |
| 95 | return; | 108 | return; |
| 96 | } | 109 | } |
| 97 | setting->SetGlobal(true); | ||
| 98 | setting->SetValue(value); | 110 | setting->SetValue(value); |
| 99 | } | 111 | } |
| 100 | 112 | ||
| 101 | jshort Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getShort(JNIEnv* env, jobject obj, jstring jkey, | 113 | jshort Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getShort(JNIEnv* env, jobject obj, jstring jkey, |
| 102 | jboolean getDefault) { | 114 | jboolean needGlobal) { |
| 103 | auto setting = getSetting<u16>(env, jkey); | 115 | auto setting = getSetting<u16>(env, jkey); |
| 104 | if (setting == nullptr) { | 116 | if (setting == nullptr) { |
| 105 | return -1; | 117 | return -1; |
| 106 | } | 118 | } |
| 107 | setting->SetGlobal(true); | 119 | return setting->GetValue(static_cast<bool>(needGlobal)); |
| 108 | |||
| 109 | if (static_cast<bool>(getDefault)) { | ||
| 110 | return setting->GetDefault(); | ||
| 111 | } | ||
| 112 | |||
| 113 | return setting->GetValue(); | ||
| 114 | } | 120 | } |
| 115 | 121 | ||
| 116 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setShort(JNIEnv* env, jobject obj, jstring jkey, | 122 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setShort(JNIEnv* env, jobject obj, jstring jkey, |
| @@ -119,23 +125,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setShort(JNIEnv* env, jobject ob | |||
| 119 | if (setting == nullptr) { | 125 | if (setting == nullptr) { |
| 120 | return; | 126 | return; |
| 121 | } | 127 | } |
| 122 | setting->SetGlobal(true); | ||
| 123 | setting->SetValue(value); | 128 | setting->SetValue(value); |
| 124 | } | 129 | } |
| 125 | 130 | ||
| 126 | jint Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInt(JNIEnv* env, jobject obj, jstring jkey, | 131 | jint Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInt(JNIEnv* env, jobject obj, jstring jkey, |
| 127 | jboolean getDefault) { | 132 | jboolean needGlobal) { |
| 128 | auto setting = getSetting<int>(env, jkey); | 133 | auto setting = getSetting<int>(env, jkey); |
| 129 | if (setting == nullptr) { | 134 | if (setting == nullptr) { |
| 130 | return -1; | 135 | return -1; |
| 131 | } | 136 | } |
| 132 | setting->SetGlobal(true); | 137 | return setting->GetValue(needGlobal); |
| 133 | |||
| 134 | if (static_cast<bool>(getDefault)) { | ||
| 135 | return setting->GetDefault(); | ||
| 136 | } | ||
| 137 | |||
| 138 | return setting->GetValue(); | ||
| 139 | } | 138 | } |
| 140 | 139 | ||
| 141 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInt(JNIEnv* env, jobject obj, jstring jkey, | 140 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInt(JNIEnv* env, jobject obj, jstring jkey, |
| @@ -144,23 +143,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInt(JNIEnv* env, jobject obj, | |||
| 144 | if (setting == nullptr) { | 143 | if (setting == nullptr) { |
| 145 | return; | 144 | return; |
| 146 | } | 145 | } |
| 147 | setting->SetGlobal(true); | ||
| 148 | setting->SetValue(value); | 146 | setting->SetValue(value); |
| 149 | } | 147 | } |
| 150 | 148 | ||
| 151 | jfloat Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getFloat(JNIEnv* env, jobject obj, jstring jkey, | 149 | jfloat Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getFloat(JNIEnv* env, jobject obj, jstring jkey, |
| 152 | jboolean getDefault) { | 150 | jboolean needGlobal) { |
| 153 | auto setting = getSetting<float>(env, jkey); | 151 | auto setting = getSetting<float>(env, jkey); |
| 154 | if (setting == nullptr) { | 152 | if (setting == nullptr) { |
| 155 | return -1; | 153 | return -1; |
| 156 | } | 154 | } |
| 157 | setting->SetGlobal(true); | 155 | return setting->GetValue(static_cast<bool>(needGlobal)); |
| 158 | |||
| 159 | if (static_cast<bool>(getDefault)) { | ||
| 160 | return setting->GetDefault(); | ||
| 161 | } | ||
| 162 | |||
| 163 | return setting->GetValue(); | ||
| 164 | } | 156 | } |
| 165 | 157 | ||
| 166 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setFloat(JNIEnv* env, jobject obj, jstring jkey, | 158 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setFloat(JNIEnv* env, jobject obj, jstring jkey, |
| @@ -169,23 +161,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setFloat(JNIEnv* env, jobject ob | |||
| 169 | if (setting == nullptr) { | 161 | if (setting == nullptr) { |
| 170 | return; | 162 | return; |
| 171 | } | 163 | } |
| 172 | setting->SetGlobal(true); | ||
| 173 | setting->SetValue(value); | 164 | setting->SetValue(value); |
| 174 | } | 165 | } |
| 175 | 166 | ||
| 176 | jlong Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getLong(JNIEnv* env, jobject obj, jstring jkey, | 167 | jlong Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getLong(JNIEnv* env, jobject obj, jstring jkey, |
| 177 | jboolean getDefault) { | 168 | jboolean needGlobal) { |
| 178 | auto setting = getSetting<long>(env, jkey); | 169 | auto setting = getSetting<s64>(env, jkey); |
| 179 | if (setting == nullptr) { | 170 | if (setting == nullptr) { |
| 180 | return -1; | 171 | return -1; |
| 181 | } | 172 | } |
| 182 | setting->SetGlobal(true); | 173 | return setting->GetValue(static_cast<bool>(needGlobal)); |
| 183 | |||
| 184 | if (static_cast<bool>(getDefault)) { | ||
| 185 | return setting->GetDefault(); | ||
| 186 | } | ||
| 187 | |||
| 188 | return setting->GetValue(); | ||
| 189 | } | 174 | } |
| 190 | 175 | ||
| 191 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setLong(JNIEnv* env, jobject obj, jstring jkey, | 176 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setLong(JNIEnv* env, jobject obj, jstring jkey, |
| @@ -194,23 +179,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setLong(JNIEnv* env, jobject obj | |||
| 194 | if (setting == nullptr) { | 179 | if (setting == nullptr) { |
| 195 | return; | 180 | return; |
| 196 | } | 181 | } |
| 197 | setting->SetGlobal(true); | ||
| 198 | setting->SetValue(value); | 182 | setting->SetValue(value); |
| 199 | } | 183 | } |
| 200 | 184 | ||
| 201 | jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getString(JNIEnv* env, jobject obj, jstring jkey, | 185 | jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getString(JNIEnv* env, jobject obj, jstring jkey, |
| 202 | jboolean getDefault) { | 186 | jboolean needGlobal) { |
| 203 | auto setting = getSetting<std::string>(env, jkey); | 187 | auto setting = getSetting<std::string>(env, jkey); |
| 204 | if (setting == nullptr) { | 188 | if (setting == nullptr) { |
| 205 | return ToJString(env, ""); | 189 | return ToJString(env, ""); |
| 206 | } | 190 | } |
| 207 | setting->SetGlobal(true); | 191 | return ToJString(env, setting->GetValue(static_cast<bool>(needGlobal))); |
| 208 | |||
| 209 | if (static_cast<bool>(getDefault)) { | ||
| 210 | return ToJString(env, setting->GetDefault()); | ||
| 211 | } | ||
| 212 | |||
| 213 | return ToJString(env, setting->GetValue()); | ||
| 214 | } | 192 | } |
| 215 | 193 | ||
| 216 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setString(JNIEnv* env, jobject obj, jstring jkey, | 194 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setString(JNIEnv* env, jobject obj, jstring jkey, |
| @@ -220,27 +198,18 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setString(JNIEnv* env, jobject o | |||
| 220 | return; | 198 | return; |
| 221 | } | 199 | } |
| 222 | 200 | ||
| 223 | setting->SetGlobal(true); | ||
| 224 | setting->SetValue(GetJString(env, value)); | 201 | setting->SetValue(GetJString(env, value)); |
| 225 | } | 202 | } |
| 226 | 203 | ||
| 227 | jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsRuntimeModifiable(JNIEnv* env, jobject obj, | 204 | jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsRuntimeModifiable(JNIEnv* env, jobject obj, |
| 228 | jstring jkey) { | 205 | jstring jkey) { |
| 229 | auto key = GetJString(env, jkey); | 206 | auto setting = getSetting<std::string>(env, jkey); |
| 230 | auto setting = Settings::values.linkage.by_key[key]; | 207 | if (setting != nullptr) { |
| 231 | if (setting != 0) { | ||
| 232 | return setting->RuntimeModfiable(); | 208 | return setting->RuntimeModfiable(); |
| 233 | } | 209 | } |
| 234 | LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key); | ||
| 235 | return true; | 210 | return true; |
| 236 | } | 211 | } |
| 237 | 212 | ||
| 238 | jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getConfigHeader(JNIEnv* env, jobject obj, | ||
| 239 | jint jcategory) { | ||
| 240 | auto category = static_cast<Settings::Category>(jcategory); | ||
| 241 | return ToJString(env, Settings::TranslateCategory(category)); | ||
| 242 | } | ||
| 243 | |||
| 244 | jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* env, jobject obj, | 213 | jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* env, jobject obj, |
| 245 | jstring jkey) { | 214 | jstring jkey) { |
| 246 | auto setting = getSetting<std::string>(env, jkey); | 215 | auto setting = getSetting<std::string>(env, jkey); |
| @@ -254,6 +223,50 @@ jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* e | |||
| 254 | return ToJString(env, setting->PairedSetting()->GetLabel()); | 223 | return ToJString(env, setting->PairedSetting()->GetLabel()); |
| 255 | } | 224 | } |
| 256 | 225 | ||
| 226 | jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsSwitchable(JNIEnv* env, jobject obj, | ||
| 227 | jstring jkey) { | ||
| 228 | auto setting = getSetting<std::string>(env, jkey); | ||
| 229 | if (setting != nullptr) { | ||
| 230 | return setting->Switchable(); | ||
| 231 | } | ||
| 232 | return false; | ||
| 233 | } | ||
| 234 | |||
| 235 | jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_usingGlobal(JNIEnv* env, jobject obj, | ||
| 236 | jstring jkey) { | ||
| 237 | auto setting = getSetting<std::string>(env, jkey); | ||
| 238 | if (setting != nullptr) { | ||
| 239 | return setting->UsingGlobal(); | ||
| 240 | } | ||
| 241 | return true; | ||
| 242 | } | ||
| 243 | |||
| 244 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setGlobal(JNIEnv* env, jobject obj, jstring jkey, | ||
| 245 | jboolean global) { | ||
| 246 | auto setting = getSetting<std::string>(env, jkey); | ||
| 247 | if (setting != nullptr) { | ||
| 248 | setting->SetGlobal(static_cast<bool>(global)); | ||
| 249 | } | ||
| 250 | } | ||
| 251 | |||
| 252 | jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsSaveable(JNIEnv* env, jobject obj, | ||
| 253 | jstring jkey) { | ||
| 254 | auto setting = getSetting<std::string>(env, jkey); | ||
| 255 | if (setting != nullptr) { | ||
| 256 | return setting->Save(); | ||
| 257 | } | ||
| 258 | return false; | ||
| 259 | } | ||
| 260 | |||
| 261 | jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getDefaultToString(JNIEnv* env, jobject obj, | ||
| 262 | jstring jkey) { | ||
| 263 | auto setting = getSetting<std::string>(env, jkey); | ||
| 264 | if (setting != nullptr) { | ||
| 265 | return ToJString(env, setting->DefaultToString()); | ||
| 266 | } | ||
| 267 | return ToJString(env, ""); | ||
| 268 | } | ||
| 269 | |||
| 257 | jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getGameDirs(JNIEnv* env, jobject obj) { | 270 | jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getGameDirs(JNIEnv* env, jobject obj) { |
| 258 | jclass gameDirClass = IDCache::GetGameDirClass(); | 271 | jclass gameDirClass = IDCache::GetGameDirClass(); |
| 259 | jmethodID gameDirConstructor = IDCache::GetGameDirConstructor(); | 272 | jmethodID gameDirConstructor = IDCache::GetGameDirConstructor(); |
| @@ -305,4 +318,30 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_addGameDir(JNIEnv* env, jobject | |||
| 305 | AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)}); | 318 | AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)}); |
| 306 | } | 319 | } |
| 307 | 320 | ||
| 321 | jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getDisabledAddons(JNIEnv* env, jobject obj, | ||
| 322 | jstring jprogramId) { | ||
| 323 | auto program_id = EmulationSession::GetProgramId(env, jprogramId); | ||
| 324 | auto& disabledAddons = Settings::values.disabled_addons[program_id]; | ||
| 325 | jobjectArray jdisabledAddonsArray = | ||
| 326 | env->NewObjectArray(disabledAddons.size(), IDCache::GetStringClass(), ToJString(env, "")); | ||
| 327 | for (size_t i = 0; i < disabledAddons.size(); ++i) { | ||
| 328 | env->SetObjectArrayElement(jdisabledAddonsArray, i, ToJString(env, disabledAddons[i])); | ||
| 329 | } | ||
| 330 | return jdisabledAddonsArray; | ||
| 331 | } | ||
| 332 | |||
| 333 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setDisabledAddons(JNIEnv* env, jobject obj, | ||
| 334 | jstring jprogramId, | ||
| 335 | jobjectArray jdisabledAddons) { | ||
| 336 | auto program_id = EmulationSession::GetProgramId(env, jprogramId); | ||
| 337 | Settings::values.disabled_addons[program_id].clear(); | ||
| 338 | std::vector<std::string> disabled_addons; | ||
| 339 | const int size = env->GetArrayLength(jdisabledAddons); | ||
| 340 | for (int i = 0; i < size; ++i) { | ||
| 341 | auto jaddon = static_cast<jstring>(env->GetObjectArrayElement(jdisabledAddons, i)); | ||
| 342 | disabled_addons.push_back(GetJString(env, jaddon)); | ||
| 343 | } | ||
| 344 | Settings::values.disabled_addons[program_id] = disabled_addons; | ||
| 345 | } | ||
| 346 | |||
| 308 | } // extern "C" | 347 | } // extern "C" |
diff --git a/src/android/app/src/main/res/drawable/ic_save.xml b/src/android/app/src/main/res/drawable/ic_save.xml index a9af3d9cf..5acc2bbab 100644 --- a/src/android/app/src/main/res/drawable/ic_save.xml +++ b/src/android/app/src/main/res/drawable/ic_save.xml | |||
| @@ -1,10 +1,9 @@ | |||
| 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" | 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" |
| 2 | android:width="24dp" | 2 | android:width="24dp" |
| 3 | android:height="24dp" | 3 | android:height="24dp" |
| 4 | android:viewportWidth="960" | 4 | android:viewportWidth="24" |
| 5 | android:viewportHeight="960" | 5 | android:viewportHeight="24"> |
| 6 | android:tint="?attr/colorControlNormal"> | ||
| 7 | <path | 6 | <path |
| 8 | android:fillColor="@android:color/white" | 7 | android:fillColor="?attr/colorControlNormal" |
| 9 | android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L647,120Q663,120 677.5,126Q692,132 703,143L817,257Q828,268 834,282.5Q840,297 840,313L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM760,314L646,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L760,760Q760,760 760,760Q760,760 760,760L760,314ZM480,720Q530,720 565,685Q600,650 600,600Q600,550 565,515Q530,480 480,480Q430,480 395,515Q360,550 360,600Q360,650 395,685Q430,720 480,720ZM280,400L560,400Q577,400 588.5,388.5Q600,377 600,360L600,280Q600,263 588.5,251.5Q577,240 560,240L280,240Q263,240 251.5,251.5Q240,263 240,280L240,360Q240,377 251.5,388.5Q263,400 280,400ZM200,314L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200L200,200L200,314Z"/> | 8 | android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" /> |
| 10 | </vector> | 9 | </vector> |
diff --git a/src/android/app/src/main/res/layout-w1000dp/card_installable_icon.xml b/src/android/app/src/main/res/layout-w1000dp/card_installable_icon.xml new file mode 100644 index 000000000..59ee1aad3 --- /dev/null +++ b/src/android/app/src/main/res/layout-w1000dp/card_installable_icon.xml | |||
| @@ -0,0 +1,82 @@ | |||
| 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 | |||
| 11 | <LinearLayout | ||
| 12 | android:layout_width="match_parent" | ||
| 13 | android:layout_height="wrap_content" | ||
| 14 | android:layout_gravity="center" | ||
| 15 | android:orientation="horizontal" | ||
| 16 | android:gravity="center_vertical" | ||
| 17 | android:paddingHorizontal="24dp" | ||
| 18 | android:paddingVertical="16dp"> | ||
| 19 | |||
| 20 | <ImageView | ||
| 21 | android:id="@+id/icon" | ||
| 22 | android:layout_width="24dp" | ||
| 23 | android:layout_height="24dp" | ||
| 24 | android:layout_marginEnd="20dp" | ||
| 25 | android:layout_gravity="center_vertical" | ||
| 26 | app:tint="?attr/colorOnSurface" | ||
| 27 | tools:src="@drawable/ic_settings" /> | ||
| 28 | |||
| 29 | <LinearLayout | ||
| 30 | android:layout_width="0dp" | ||
| 31 | android:layout_height="wrap_content" | ||
| 32 | android:layout_marginEnd="16dp" | ||
| 33 | android:layout_weight="1" | ||
| 34 | android:orientation="vertical"> | ||
| 35 | |||
| 36 | <com.google.android.material.textview.MaterialTextView | ||
| 37 | android:id="@+id/title" | ||
| 38 | style="@style/TextAppearance.Material3.TitleMedium" | ||
| 39 | android:layout_width="match_parent" | ||
| 40 | android:layout_height="wrap_content" | ||
| 41 | android:text="@string/user_data" | ||
| 42 | android:textAlignment="viewStart" /> | ||
| 43 | |||
| 44 | <com.google.android.material.textview.MaterialTextView | ||
| 45 | android:id="@+id/description" | ||
| 46 | style="@style/TextAppearance.Material3.BodyMedium" | ||
| 47 | android:layout_width="match_parent" | ||
| 48 | android:layout_height="wrap_content" | ||
| 49 | android:layout_marginTop="6dp" | ||
| 50 | android:text="@string/user_data_description" | ||
| 51 | android:textAlignment="viewStart" /> | ||
| 52 | |||
| 53 | </LinearLayout> | ||
| 54 | |||
| 55 | <Button | ||
| 56 | android:id="@+id/button_export" | ||
| 57 | style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" | ||
| 58 | android:layout_width="wrap_content" | ||
| 59 | android:layout_height="wrap_content" | ||
| 60 | android:layout_gravity="center_vertical" | ||
| 61 | android:contentDescription="@string/export" | ||
| 62 | android:tooltipText="@string/export" | ||
| 63 | android:visibility="gone" | ||
| 64 | app:icon="@drawable/ic_export" | ||
| 65 | tools:visibility="visible" /> | ||
| 66 | |||
| 67 | <Button | ||
| 68 | android:id="@+id/button_install" | ||
| 69 | style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" | ||
| 70 | android:layout_width="wrap_content" | ||
| 71 | android:layout_height="wrap_content" | ||
| 72 | android:layout_gravity="center_vertical" | ||
| 73 | android:layout_marginStart="12dp" | ||
| 74 | android:contentDescription="@string/string_import" | ||
| 75 | android:tooltipText="@string/string_import" | ||
| 76 | android:visibility="gone" | ||
| 77 | app:icon="@drawable/ic_import" | ||
| 78 | tools:visibility="visible" /> | ||
| 79 | |||
| 80 | </LinearLayout> | ||
| 81 | |||
| 82 | </com.google.android.material.card.MaterialCardView> | ||
diff --git a/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml b/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml new file mode 100644 index 000000000..0b9633855 --- /dev/null +++ b/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml | |||
| @@ -0,0 +1,99 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <androidx.constraintlayout.widget.ConstraintLayout | ||
| 3 | xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 4 | xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 5 | xmlns:tools="http://schemas.android.com/tools" | ||
| 6 | android:layout_width="match_parent" | ||
| 7 | android:layout_height="match_parent" | ||
| 8 | android:background="?attr/colorSurface"> | ||
| 9 | |||
| 10 | <androidx.core.widget.NestedScrollView | ||
| 11 | android:id="@+id/list_all" | ||
| 12 | android:layout_width="0dp" | ||
| 13 | android:layout_height="match_parent" | ||
| 14 | android:clipToPadding="false" | ||
| 15 | android:fadeScrollbars="false" | ||
| 16 | android:scrollbars="vertical" | ||
| 17 | app:layout_constraintEnd_toEndOf="parent" | ||
| 18 | app:layout_constraintStart_toEndOf="@+id/icon_layout" | ||
| 19 | app:layout_constraintTop_toTopOf="parent"> | ||
| 20 | |||
| 21 | <LinearLayout | ||
| 22 | android:id="@+id/layout_all" | ||
| 23 | android:layout_width="match_parent" | ||
| 24 | android:layout_height="wrap_content" | ||
| 25 | android:gravity="center_horizontal" | ||
| 26 | android:orientation="horizontal"> | ||
| 27 | |||
| 28 | <androidx.recyclerview.widget.RecyclerView | ||
| 29 | android:id="@+id/list_properties" | ||
| 30 | android:layout_width="match_parent" | ||
| 31 | android:layout_height="match_parent" | ||
| 32 | tools:listitem="@layout/card_simple_outlined" /> | ||
| 33 | |||
| 34 | </LinearLayout> | ||
| 35 | |||
| 36 | </androidx.core.widget.NestedScrollView> | ||
| 37 | |||
| 38 | <LinearLayout | ||
| 39 | android:id="@+id/icon_layout" | ||
| 40 | android:layout_width="wrap_content" | ||
| 41 | android:layout_height="wrap_content" | ||
| 42 | android:orientation="vertical" | ||
| 43 | app:layout_constraintStart_toStartOf="parent" | ||
| 44 | app:layout_constraintTop_toTopOf="parent"> | ||
| 45 | |||
| 46 | <Button | ||
| 47 | android:id="@+id/button_back" | ||
| 48 | style="?attr/materialIconButtonStyle" | ||
| 49 | android:layout_width="wrap_content" | ||
| 50 | android:layout_height="wrap_content" | ||
| 51 | android:layout_gravity="start" | ||
| 52 | android:layout_margin="8dp" | ||
| 53 | app:icon="@drawable/ic_back" | ||
| 54 | app:iconSize="24dp" | ||
| 55 | app:iconTint="?attr/colorOnSurface" /> | ||
| 56 | |||
| 57 | <com.google.android.material.card.MaterialCardView | ||
| 58 | style="?attr/materialCardViewElevatedStyle" | ||
| 59 | android:layout_width="wrap_content" | ||
| 60 | android:layout_height="wrap_content" | ||
| 61 | android:layout_marginHorizontal="16dp" | ||
| 62 | android:layout_marginTop="8dp" | ||
| 63 | app:cardCornerRadius="4dp" | ||
| 64 | app:cardElevation="4dp"> | ||
| 65 | |||
| 66 | <ImageView | ||
| 67 | android:id="@+id/image_game_screen" | ||
| 68 | android:layout_width="175dp" | ||
| 69 | android:layout_height="175dp" | ||
| 70 | tools:src="@drawable/default_icon" /> | ||
| 71 | |||
| 72 | </com.google.android.material.card.MaterialCardView> | ||
| 73 | |||
| 74 | <com.google.android.material.textview.MaterialTextView | ||
| 75 | android:id="@+id/title" | ||
| 76 | style="@style/TextAppearance.Material3.TitleMedium" | ||
| 77 | android:layout_width="match_parent" | ||
| 78 | android:layout_height="wrap_content" | ||
| 79 | android:layout_marginHorizontal="16dp" | ||
| 80 | android:layout_marginTop="12dp" | ||
| 81 | android:ellipsize="none" | ||
| 82 | android:marqueeRepeatLimit="marquee_forever" | ||
| 83 | android:requiresFadingEdge="horizontal" | ||
| 84 | android:singleLine="true" | ||
| 85 | android:textAlignment="center" | ||
| 86 | tools:text="deko_basic" /> | ||
| 87 | |||
| 88 | </LinearLayout> | ||
| 89 | |||
| 90 | <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton | ||
| 91 | android:id="@+id/button_start" | ||
| 92 | android:layout_width="wrap_content" | ||
| 93 | android:layout_height="wrap_content" | ||
| 94 | android:text="@string/start" | ||
| 95 | app:icon="@drawable/ic_play" | ||
| 96 | app:layout_constraintBottom_toBottomOf="parent" | ||
| 97 | app:layout_constraintEnd_toEndOf="parent" /> | ||
| 98 | |||
| 99 | </androidx.constraintlayout.widget.ConstraintLayout> | ||
diff --git a/src/android/app/src/main/res/layout/card_installable.xml b/src/android/app/src/main/res/layout/card_installable.xml index f5b0e3741..ce2402d7a 100644 --- a/src/android/app/src/main/res/layout/card_installable.xml +++ b/src/android/app/src/main/res/layout/card_installable.xml | |||
| @@ -11,7 +11,8 @@ | |||
| 11 | <LinearLayout | 11 | <LinearLayout |
| 12 | android:layout_width="match_parent" | 12 | android:layout_width="match_parent" |
| 13 | android:layout_height="wrap_content" | 13 | android:layout_height="wrap_content" |
| 14 | android:layout_margin="16dp" | 14 | android:paddingVertical="16dp" |
| 15 | android:paddingHorizontal="24dp" | ||
| 15 | android:orientation="horizontal" | 16 | android:orientation="horizontal" |
| 16 | android:layout_gravity="center"> | 17 | android:layout_gravity="center"> |
| 17 | 18 | ||
diff --git a/src/android/app/src/main/res/layout/card_installable_icon.xml b/src/android/app/src/main/res/layout/card_installable_icon.xml new file mode 100644 index 000000000..4ae5423b1 --- /dev/null +++ b/src/android/app/src/main/res/layout/card_installable_icon.xml | |||
| @@ -0,0 +1,89 @@ | |||
| 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 | |||
| 11 | <LinearLayout | ||
| 12 | android:layout_width="match_parent" | ||
| 13 | android:layout_height="wrap_content" | ||
| 14 | android:layout_gravity="center" | ||
| 15 | android:orientation="horizontal" | ||
| 16 | android:gravity="center_vertical" | ||
| 17 | android:paddingHorizontal="24dp" | ||
| 18 | android:paddingVertical="16dp"> | ||
| 19 | |||
| 20 | <ImageView | ||
| 21 | android:id="@+id/icon" | ||
| 22 | android:layout_width="24dp" | ||
| 23 | android:layout_height="24dp" | ||
| 24 | android:layout_marginEnd="20dp" | ||
| 25 | android:layout_gravity="center_vertical" | ||
| 26 | app:tint="?attr/colorOnSurface" | ||
| 27 | tools:src="@drawable/ic_settings" /> | ||
| 28 | |||
| 29 | <LinearLayout | ||
| 30 | android:layout_width="0dp" | ||
| 31 | android:layout_height="wrap_content" | ||
| 32 | android:layout_marginEnd="16dp" | ||
| 33 | android:layout_weight="1" | ||
| 34 | android:orientation="vertical"> | ||
| 35 | |||
| 36 | <com.google.android.material.textview.MaterialTextView | ||
| 37 | android:id="@+id/title" | ||
| 38 | style="@style/TextAppearance.Material3.TitleMedium" | ||
| 39 | android:layout_width="match_parent" | ||
| 40 | android:layout_height="wrap_content" | ||
| 41 | android:text="@string/user_data" | ||
| 42 | android:textAlignment="viewStart" /> | ||
| 43 | |||
| 44 | <com.google.android.material.textview.MaterialTextView | ||
| 45 | android:id="@+id/description" | ||
| 46 | style="@style/TextAppearance.Material3.BodyMedium" | ||
| 47 | android:layout_width="match_parent" | ||
| 48 | android:layout_height="wrap_content" | ||
| 49 | android:layout_marginTop="6dp" | ||
| 50 | android:text="@string/user_data_description" | ||
| 51 | android:textAlignment="viewStart" /> | ||
| 52 | |||
| 53 | </LinearLayout> | ||
| 54 | |||
| 55 | <LinearLayout | ||
| 56 | android:layout_width="wrap_content" | ||
| 57 | android:layout_height="wrap_content" | ||
| 58 | android:orientation="vertical"> | ||
| 59 | |||
| 60 | <Button | ||
| 61 | android:id="@+id/button_install" | ||
| 62 | style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" | ||
| 63 | android:layout_width="wrap_content" | ||
| 64 | android:layout_height="wrap_content" | ||
| 65 | android:layout_gravity="center_vertical" | ||
| 66 | android:contentDescription="@string/string_import" | ||
| 67 | android:tooltipText="@string/string_import" | ||
| 68 | android:visibility="gone" | ||
| 69 | app:icon="@drawable/ic_import" | ||
| 70 | tools:visibility="visible" /> | ||
| 71 | |||
| 72 | <Button | ||
| 73 | android:id="@+id/button_export" | ||
| 74 | style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" | ||
| 75 | android:layout_width="wrap_content" | ||
| 76 | android:layout_height="wrap_content" | ||
| 77 | android:layout_gravity="center_vertical" | ||
| 78 | android:layout_marginTop="8dp" | ||
| 79 | android:contentDescription="@string/export" | ||
| 80 | android:tooltipText="@string/export" | ||
| 81 | android:visibility="gone" | ||
| 82 | app:icon="@drawable/ic_export" | ||
| 83 | tools:visibility="visible" /> | ||
| 84 | |||
| 85 | </LinearLayout> | ||
| 86 | |||
| 87 | </LinearLayout> | ||
| 88 | |||
| 89 | </com.google.android.material.card.MaterialCardView> | ||
diff --git a/src/android/app/src/main/res/layout/card_applet_option.xml b/src/android/app/src/main/res/layout/card_simple_outlined.xml index 19fbec9f1..b73930e7e 100644 --- a/src/android/app/src/main/res/layout/card_applet_option.xml +++ b/src/android/app/src/main/res/layout/card_simple_outlined.xml | |||
| @@ -16,7 +16,8 @@ | |||
| 16 | android:layout_height="wrap_content" | 16 | android:layout_height="wrap_content" |
| 17 | android:orientation="horizontal" | 17 | android:orientation="horizontal" |
| 18 | android:layout_gravity="center" | 18 | android:layout_gravity="center" |
| 19 | android:padding="24dp"> | 19 | android:paddingVertical="16dp" |
| 20 | android:paddingHorizontal="24dp"> | ||
| 20 | 21 | ||
| 21 | <ImageView | 22 | <ImageView |
| 22 | android:id="@+id/icon" | 23 | android:id="@+id/icon" |
| @@ -50,6 +51,23 @@ | |||
| 50 | android:textAlignment="viewStart" | 51 | android:textAlignment="viewStart" |
| 51 | tools:text="@string/applets_description" /> | 52 | tools:text="@string/applets_description" /> |
| 52 | 53 | ||
| 54 | <com.google.android.material.textview.MaterialTextView | ||
| 55 | style="@style/TextAppearance.Material3.LabelMedium" | ||
| 56 | android:id="@+id/details" | ||
| 57 | android:layout_width="match_parent" | ||
| 58 | android:layout_height="wrap_content" | ||
| 59 | android:textAlignment="viewStart" | ||
| 60 | android:textSize="14sp" | ||
| 61 | android:textStyle="bold" | ||
| 62 | android:singleLine="true" | ||
| 63 | android:marqueeRepeatLimit="marquee_forever" | ||
| 64 | android:ellipsize="none" | ||
| 65 | android:requiresFadingEdge="horizontal" | ||
| 66 | android:layout_marginTop="6dp" | ||
| 67 | android:visibility="gone" | ||
| 68 | tools:visibility="visible" | ||
| 69 | tools:text="/tree/primary:Games" /> | ||
| 70 | |||
| 53 | </LinearLayout> | 71 | </LinearLayout> |
| 54 | 72 | ||
| 55 | </LinearLayout> | 73 | </LinearLayout> |
diff --git a/src/android/app/src/main/res/layout/fragment_addons.xml b/src/android/app/src/main/res/layout/fragment_addons.xml new file mode 100644 index 000000000..a25e82766 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_addons.xml | |||
| @@ -0,0 +1,47 @@ | |||
| 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_about" | ||
| 5 | android:layout_width="match_parent" | ||
| 6 | android:layout_height="match_parent" | ||
| 7 | android:background="?attr/colorSurface"> | ||
| 8 | |||
| 9 | <com.google.android.material.appbar.AppBarLayout | ||
| 10 | android:id="@+id/appbar_addons" | ||
| 11 | android:layout_width="match_parent" | ||
| 12 | android:layout_height="wrap_content" | ||
| 13 | android:fitsSystemWindows="true" | ||
| 14 | app:layout_constraintEnd_toEndOf="parent" | ||
| 15 | app:layout_constraintStart_toStartOf="parent" | ||
| 16 | app:layout_constraintTop_toTopOf="parent"> | ||
| 17 | |||
| 18 | <com.google.android.material.appbar.MaterialToolbar | ||
| 19 | android:id="@+id/toolbar_addons" | ||
| 20 | android:layout_width="match_parent" | ||
| 21 | android:layout_height="?attr/actionBarSize" | ||
| 22 | app:navigationIcon="@drawable/ic_back" /> | ||
| 23 | |||
| 24 | </com.google.android.material.appbar.AppBarLayout> | ||
| 25 | |||
| 26 | <androidx.recyclerview.widget.RecyclerView | ||
| 27 | android:id="@+id/list_addons" | ||
| 28 | android:layout_width="match_parent" | ||
| 29 | android:layout_height="0dp" | ||
| 30 | android:clipToPadding="false" | ||
| 31 | app:layout_behavior="@string/appbar_scrolling_view_behavior" | ||
| 32 | app:layout_constraintBottom_toBottomOf="parent" | ||
| 33 | app:layout_constraintEnd_toEndOf="parent" | ||
| 34 | app:layout_constraintStart_toStartOf="parent" | ||
| 35 | app:layout_constraintTop_toBottomOf="@+id/appbar_addons" /> | ||
| 36 | |||
| 37 | <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton | ||
| 38 | android:id="@+id/button_install" | ||
| 39 | android:layout_width="wrap_content" | ||
| 40 | android:layout_height="wrap_content" | ||
| 41 | android:layout_gravity="bottom|end" | ||
| 42 | android:text="@string/install" | ||
| 43 | app:icon="@drawable/ic_add" | ||
| 44 | app:layout_constraintBottom_toBottomOf="parent" | ||
| 45 | app:layout_constraintEnd_toEndOf="parent" /> | ||
| 46 | |||
| 47 | </androidx.constraintlayout.widget.ConstraintLayout> | ||
diff --git a/src/android/app/src/main/res/layout/fragment_game_info.xml b/src/android/app/src/main/res/layout/fragment_game_info.xml new file mode 100644 index 000000000..80ede8a8c --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_game_info.xml | |||
| @@ -0,0 +1,125 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <androidx.coordinatorlayout.widget.CoordinatorLayout 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 | android:id="@+id/coordinator_about" | ||
| 6 | android:layout_width="match_parent" | ||
| 7 | android:layout_height="match_parent" | ||
| 8 | android:background="?attr/colorSurface"> | ||
| 9 | |||
| 10 | <com.google.android.material.appbar.AppBarLayout | ||
| 11 | android:id="@+id/appbar_info" | ||
| 12 | android:layout_width="match_parent" | ||
| 13 | android:layout_height="wrap_content" | ||
| 14 | android:fitsSystemWindows="true"> | ||
| 15 | |||
| 16 | <com.google.android.material.appbar.MaterialToolbar | ||
| 17 | android:id="@+id/toolbar_info" | ||
| 18 | android:layout_width="match_parent" | ||
| 19 | android:layout_height="?attr/actionBarSize" | ||
| 20 | app:navigationIcon="@drawable/ic_back" /> | ||
| 21 | |||
| 22 | </com.google.android.material.appbar.AppBarLayout> | ||
| 23 | |||
| 24 | <androidx.core.widget.NestedScrollView | ||
| 25 | android:id="@+id/scroll_info" | ||
| 26 | android:layout_width="match_parent" | ||
| 27 | android:layout_height="wrap_content" | ||
| 28 | app:layout_behavior="@string/appbar_scrolling_view_behavior"> | ||
| 29 | |||
| 30 | <LinearLayout | ||
| 31 | android:id="@+id/content_info" | ||
| 32 | android:layout_width="match_parent" | ||
| 33 | android:layout_height="wrap_content" | ||
| 34 | android:orientation="vertical" | ||
| 35 | android:paddingHorizontal="16dp"> | ||
| 36 | |||
| 37 | <com.google.android.material.textfield.TextInputLayout | ||
| 38 | android:id="@+id/path" | ||
| 39 | android:layout_width="match_parent" | ||
| 40 | android:layout_height="wrap_content" | ||
| 41 | android:paddingTop="16dp"> | ||
| 42 | |||
| 43 | <com.google.android.material.textfield.TextInputEditText | ||
| 44 | android:id="@+id/path_field" | ||
| 45 | android:layout_width="match_parent" | ||
| 46 | android:layout_height="wrap_content" | ||
| 47 | android:editable="false" | ||
| 48 | android:importantForAutofill="no" | ||
| 49 | android:inputType="none" | ||
| 50 | android:minHeight="48dp" | ||
| 51 | android:textAlignment="viewStart" | ||
| 52 | tools:text="1.0.0" /> | ||
| 53 | |||
| 54 | </com.google.android.material.textfield.TextInputLayout> | ||
| 55 | |||
| 56 | <com.google.android.material.textfield.TextInputLayout | ||
| 57 | android:id="@+id/program_id" | ||
| 58 | android:layout_width="match_parent" | ||
| 59 | android:layout_height="wrap_content" | ||
| 60 | android:paddingTop="16dp"> | ||
| 61 | |||
| 62 | <com.google.android.material.textfield.TextInputEditText | ||
| 63 | android:id="@+id/program_id_field" | ||
| 64 | android:layout_width="match_parent" | ||
| 65 | android:layout_height="wrap_content" | ||
| 66 | android:editable="false" | ||
| 67 | android:importantForAutofill="no" | ||
| 68 | android:inputType="none" | ||
| 69 | android:minHeight="48dp" | ||
| 70 | android:textAlignment="viewStart" | ||
| 71 | tools:text="1.0.0" /> | ||
| 72 | |||
| 73 | </com.google.android.material.textfield.TextInputLayout> | ||
| 74 | |||
| 75 | <com.google.android.material.textfield.TextInputLayout | ||
| 76 | android:id="@+id/developer" | ||
| 77 | android:layout_width="match_parent" | ||
| 78 | android:layout_height="wrap_content" | ||
| 79 | android:paddingTop="16dp"> | ||
| 80 | |||
| 81 | <com.google.android.material.textfield.TextInputEditText | ||
| 82 | android:id="@+id/developer_field" | ||
| 83 | android:layout_width="match_parent" | ||
| 84 | android:layout_height="wrap_content" | ||
| 85 | android:editable="false" | ||
| 86 | android:importantForAutofill="no" | ||
| 87 | android:inputType="none" | ||
| 88 | android:minHeight="48dp" | ||
| 89 | android:textAlignment="viewStart" | ||
| 90 | tools:text="1.0.0" /> | ||
| 91 | |||
| 92 | </com.google.android.material.textfield.TextInputLayout> | ||
| 93 | |||
| 94 | <com.google.android.material.textfield.TextInputLayout | ||
| 95 | android:id="@+id/version" | ||
| 96 | android:layout_width="match_parent" | ||
| 97 | android:layout_height="wrap_content" | ||
| 98 | android:paddingTop="16dp"> | ||
| 99 | |||
| 100 | <com.google.android.material.textfield.TextInputEditText | ||
| 101 | android:id="@+id/version_field" | ||
| 102 | android:layout_width="match_parent" | ||
| 103 | android:layout_height="wrap_content" | ||
| 104 | android:editable="false" | ||
| 105 | android:importantForAutofill="no" | ||
| 106 | android:inputType="none" | ||
| 107 | android:minHeight="48dp" | ||
| 108 | android:textAlignment="viewStart" | ||
| 109 | tools:text="1.0.0" /> | ||
| 110 | |||
| 111 | </com.google.android.material.textfield.TextInputLayout> | ||
| 112 | |||
| 113 | <com.google.android.material.button.MaterialButton | ||
| 114 | android:id="@+id/button_copy" | ||
| 115 | style="@style/Widget.Material3.Button" | ||
| 116 | android:layout_width="wrap_content" | ||
| 117 | android:layout_height="wrap_content" | ||
| 118 | android:layout_marginTop="16dp" | ||
| 119 | android:text="@string/copy_details" /> | ||
| 120 | |||
| 121 | </LinearLayout> | ||
| 122 | |||
| 123 | </androidx.core.widget.NestedScrollView> | ||
| 124 | |||
| 125 | </androidx.coordinatorlayout.widget.CoordinatorLayout> | ||
diff --git a/src/android/app/src/main/res/layout/fragment_game_properties.xml b/src/android/app/src/main/res/layout/fragment_game_properties.xml new file mode 100644 index 000000000..72ecbde30 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_game_properties.xml | |||
| @@ -0,0 +1,86 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <androidx.constraintlayout.widget.ConstraintLayout | ||
| 3 | xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 4 | xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 5 | xmlns:tools="http://schemas.android.com/tools" | ||
| 6 | android:layout_width="match_parent" | ||
| 7 | android:layout_height="match_parent" | ||
| 8 | android:background="?attr/colorSurface"> | ||
| 9 | |||
| 10 | <androidx.core.widget.NestedScrollView | ||
| 11 | android:id="@+id/list_all" | ||
| 12 | android:layout_width="match_parent" | ||
| 13 | android:layout_height="match_parent" | ||
| 14 | android:scrollbars="vertical" | ||
| 15 | android:fadeScrollbars="false" | ||
| 16 | android:clipToPadding="false"> | ||
| 17 | |||
| 18 | <LinearLayout | ||
| 19 | android:id="@+id/layout_all" | ||
| 20 | android:layout_width="match_parent" | ||
| 21 | android:layout_height="wrap_content" | ||
| 22 | android:orientation="vertical" | ||
| 23 | android:gravity="center_horizontal"> | ||
| 24 | |||
| 25 | <Button | ||
| 26 | android:id="@+id/button_back" | ||
| 27 | style="?attr/materialIconButtonStyle" | ||
| 28 | android:layout_width="wrap_content" | ||
| 29 | android:layout_height="wrap_content" | ||
| 30 | android:layout_margin="8dp" | ||
| 31 | android:layout_gravity="start" | ||
| 32 | app:icon="@drawable/ic_back" | ||
| 33 | app:iconSize="24dp" | ||
| 34 | app:iconTint="?attr/colorOnSurface" /> | ||
| 35 | |||
| 36 | <com.google.android.material.card.MaterialCardView | ||
| 37 | style="?attr/materialCardViewElevatedStyle" | ||
| 38 | android:layout_width="wrap_content" | ||
| 39 | android:layout_height="wrap_content" | ||
| 40 | android:layout_marginTop="8dp" | ||
| 41 | app:cardCornerRadius="4dp" | ||
| 42 | app:cardElevation="4dp"> | ||
| 43 | |||
| 44 | <ImageView | ||
| 45 | android:id="@+id/image_game_screen" | ||
| 46 | android:layout_width="175dp" | ||
| 47 | android:layout_height="175dp" | ||
| 48 | tools:src="@drawable/default_icon"/> | ||
| 49 | |||
| 50 | </com.google.android.material.card.MaterialCardView> | ||
| 51 | |||
| 52 | <com.google.android.material.textview.MaterialTextView | ||
| 53 | android:id="@+id/title" | ||
| 54 | style="@style/TextAppearance.Material3.TitleMedium" | ||
| 55 | android:layout_width="wrap_content" | ||
| 56 | android:layout_height="wrap_content" | ||
| 57 | android:layout_marginTop="12dp" | ||
| 58 | android:layout_marginBottom="12dp" | ||
| 59 | android:layout_marginHorizontal="16dp" | ||
| 60 | android:ellipsize="none" | ||
| 61 | android:marqueeRepeatLimit="marquee_forever" | ||
| 62 | android:requiresFadingEdge="horizontal" | ||
| 63 | android:singleLine="true" | ||
| 64 | android:textAlignment="center" | ||
| 65 | tools:text="deko_basic" /> | ||
| 66 | |||
| 67 | <androidx.recyclerview.widget.RecyclerView | ||
| 68 | android:id="@+id/list_properties" | ||
| 69 | android:layout_width="match_parent" | ||
| 70 | android:layout_height="match_parent" | ||
| 71 | tools:listitem="@layout/card_simple_outlined" /> | ||
| 72 | |||
| 73 | </LinearLayout> | ||
| 74 | |||
| 75 | </androidx.core.widget.NestedScrollView> | ||
| 76 | |||
| 77 | <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton | ||
| 78 | android:id="@+id/button_start" | ||
| 79 | android:layout_width="wrap_content" | ||
| 80 | android:layout_height="wrap_content" | ||
| 81 | android:text="@string/start" | ||
| 82 | app:icon="@drawable/ic_play" | ||
| 83 | app:layout_constraintBottom_toBottomOf="parent" | ||
| 84 | app:layout_constraintEnd_toEndOf="parent" /> | ||
| 85 | |||
| 86 | </androidx.constraintlayout.widget.ConstraintLayout> | ||
diff --git a/src/android/app/src/main/res/layout/list_item_addon.xml b/src/android/app/src/main/res/layout/list_item_addon.xml new file mode 100644 index 000000000..74ca04ef1 --- /dev/null +++ b/src/android/app/src/main/res/layout/list_item_addon.xml | |||
| @@ -0,0 +1,57 @@ | |||
| 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 | xmlns:tools="http://schemas.android.com/tools" | ||
| 5 | android:id="@+id/addon_container" | ||
| 6 | android:layout_width="match_parent" | ||
| 7 | android:layout_height="wrap_content" | ||
| 8 | android:background="?attr/selectableItemBackground" | ||
| 9 | android:focusable="true" | ||
| 10 | android:paddingHorizontal="20dp" | ||
| 11 | android:paddingVertical="16dp"> | ||
| 12 | |||
| 13 | <LinearLayout | ||
| 14 | android:id="@+id/text_container" | ||
| 15 | android:layout_width="0dp" | ||
| 16 | android:layout_height="wrap_content" | ||
| 17 | android:layout_marginEnd="16dp" | ||
| 18 | android:orientation="vertical" | ||
| 19 | app:layout_constraintBottom_toBottomOf="@+id/addon_switch" | ||
| 20 | app:layout_constraintEnd_toStartOf="@+id/addon_switch" | ||
| 21 | app:layout_constraintStart_toStartOf="parent" | ||
| 22 | app:layout_constraintTop_toTopOf="@+id/addon_switch"> | ||
| 23 | |||
| 24 | <com.google.android.material.textview.MaterialTextView | ||
| 25 | android:id="@+id/title" | ||
| 26 | style="@style/TextAppearance.Material3.HeadlineMedium" | ||
| 27 | android:layout_width="wrap_content" | ||
| 28 | android:layout_height="wrap_content" | ||
| 29 | android:textAlignment="viewStart" | ||
| 30 | android:textSize="17sp" | ||
| 31 | app:lineHeight="28dp" | ||
| 32 | tools:text="1440p Resolution" /> | ||
| 33 | |||
| 34 | <com.google.android.material.textview.MaterialTextView | ||
| 35 | android:id="@+id/version" | ||
| 36 | style="@style/TextAppearance.Material3.BodySmall" | ||
| 37 | android:layout_width="wrap_content" | ||
| 38 | android:layout_height="wrap_content" | ||
| 39 | android:layout_marginTop="@dimen/spacing_small" | ||
| 40 | android:textAlignment="viewStart" | ||
| 41 | tools:text="1.0.0" /> | ||
| 42 | |||
| 43 | </LinearLayout> | ||
| 44 | |||
| 45 | <com.google.android.material.materialswitch.MaterialSwitch | ||
| 46 | android:id="@+id/addon_switch" | ||
| 47 | android:layout_width="wrap_content" | ||
| 48 | android:layout_height="wrap_content" | ||
| 49 | android:focusable="true" | ||
| 50 | android:gravity="center" | ||
| 51 | android:nextFocusLeft="@id/addon_container" | ||
| 52 | app:layout_constraintBottom_toBottomOf="parent" | ||
| 53 | app:layout_constraintEnd_toEndOf="parent" | ||
| 54 | app:layout_constraintStart_toEndOf="@id/text_container" | ||
| 55 | app:layout_constraintTop_toTopOf="parent" /> | ||
| 56 | |||
| 57 | </androidx.constraintlayout.widget.ConstraintLayout> | ||
diff --git a/src/android/app/src/main/res/layout/list_item_setting.xml b/src/android/app/src/main/res/layout/list_item_setting.xml index 544280e75..1f80682f1 100644 --- a/src/android/app/src/main/res/layout/list_item_setting.xml +++ b/src/android/app/src/main/res/layout/list_item_setting.xml | |||
| @@ -62,6 +62,16 @@ | |||
| 62 | android:textSize="13sp" | 62 | android:textSize="13sp" |
| 63 | tools:text="1x" /> | 63 | tools:text="1x" /> |
| 64 | 64 | ||
| 65 | <com.google.android.material.button.MaterialButton | ||
| 66 | android:id="@+id/button_clear" | ||
| 67 | style="@style/Widget.Material3.Button.TonalButton" | ||
| 68 | android:layout_width="wrap_content" | ||
| 69 | android:layout_height="wrap_content" | ||
| 70 | android:layout_marginTop="16dp" | ||
| 71 | android:visibility="gone" | ||
| 72 | android:text="@string/clear" | ||
| 73 | tools:visibility="visible" /> | ||
| 74 | |||
| 65 | </LinearLayout> | 75 | </LinearLayout> |
| 66 | 76 | ||
| 67 | </LinearLayout> | 77 | </LinearLayout> |
diff --git a/src/android/app/src/main/res/layout/list_item_setting_switch.xml b/src/android/app/src/main/res/layout/list_item_setting_switch.xml index a8f5aff78..5cb84182e 100644 --- a/src/android/app/src/main/res/layout/list_item_setting_switch.xml +++ b/src/android/app/src/main/res/layout/list_item_setting_switch.xml | |||
| @@ -10,41 +10,62 @@ | |||
| 10 | android:minHeight="72dp" | 10 | android:minHeight="72dp" |
| 11 | android:padding="16dp"> | 11 | android:padding="16dp"> |
| 12 | 12 | ||
| 13 | <com.google.android.material.materialswitch.MaterialSwitch | ||
| 14 | android:id="@+id/switch_widget" | ||
| 15 | android:layout_width="wrap_content" | ||
| 16 | android:layout_height="wrap_content" | ||
| 17 | android:layout_alignParentEnd="true" | ||
| 18 | android:layout_centerVertical="true" /> | ||
| 19 | |||
| 20 | <LinearLayout | 13 | <LinearLayout |
| 21 | android:layout_width="match_parent" | 14 | android:layout_width="match_parent" |
| 22 | android:layout_height="wrap_content" | 15 | android:layout_height="wrap_content" |
| 23 | android:layout_alignParentTop="true" | ||
| 24 | android:layout_centerVertical="true" | ||
| 25 | android:layout_marginEnd="24dp" | ||
| 26 | android:layout_toStartOf="@+id/switch_widget" | ||
| 27 | android:gravity="center_vertical" | ||
| 28 | android:orientation="vertical"> | 16 | android:orientation="vertical"> |
| 29 | 17 | ||
| 30 | <com.google.android.material.textview.MaterialTextView | 18 | <LinearLayout |
| 31 | android:id="@+id/text_setting_name" | 19 | android:layout_width="match_parent" |
| 32 | style="@style/TextAppearance.Material3.HeadlineMedium" | ||
| 33 | android:layout_width="wrap_content" | ||
| 34 | android:layout_height="wrap_content" | 20 | android:layout_height="wrap_content" |
| 35 | android:textAlignment="viewStart" | 21 | android:orientation="horizontal"> |
| 36 | android:textSize="17sp" | 22 | |
| 37 | app:lineHeight="28dp" | 23 | <LinearLayout |
| 38 | tools:text="@string/frame_limit_enable" /> | 24 | android:layout_width="0dp" |
| 39 | 25 | android:layout_height="wrap_content" | |
| 40 | <com.google.android.material.textview.MaterialTextView | 26 | android:layout_marginEnd="24dp" |
| 41 | android:id="@+id/text_setting_description" | 27 | android:gravity="center_vertical" |
| 42 | style="@style/TextAppearance.Material3.BodySmall" | 28 | android:orientation="vertical" |
| 29 | android:layout_weight="1"> | ||
| 30 | |||
| 31 | <com.google.android.material.textview.MaterialTextView | ||
| 32 | android:id="@+id/text_setting_name" | ||
| 33 | style="@style/TextAppearance.Material3.HeadlineMedium" | ||
| 34 | android:layout_width="wrap_content" | ||
| 35 | android:layout_height="wrap_content" | ||
| 36 | android:textAlignment="viewStart" | ||
| 37 | android:textSize="17sp" | ||
| 38 | app:lineHeight="28dp" | ||
| 39 | tools:text="@string/frame_limit_enable" /> | ||
| 40 | |||
| 41 | <com.google.android.material.textview.MaterialTextView | ||
| 42 | android:id="@+id/text_setting_description" | ||
| 43 | style="@style/TextAppearance.Material3.BodySmall" | ||
| 44 | android:layout_width="wrap_content" | ||
| 45 | android:layout_height="wrap_content" | ||
| 46 | android:layout_marginTop="@dimen/spacing_small" | ||
| 47 | android:textAlignment="viewStart" | ||
| 48 | tools:text="@string/frame_limit_enable_description" /> | ||
| 49 | |||
| 50 | </LinearLayout> | ||
| 51 | |||
| 52 | <com.google.android.material.materialswitch.MaterialSwitch | ||
| 53 | android:id="@+id/switch_widget" | ||
| 54 | android:layout_width="wrap_content" | ||
| 55 | android:layout_height="wrap_content" | ||
| 56 | android:layout_gravity="center_vertical"/> | ||
| 57 | |||
| 58 | </LinearLayout> | ||
| 59 | |||
| 60 | <com.google.android.material.button.MaterialButton | ||
| 61 | android:id="@+id/button_clear" | ||
| 62 | style="@style/Widget.Material3.Button.TonalButton" | ||
| 43 | android:layout_width="wrap_content" | 63 | android:layout_width="wrap_content" |
| 44 | android:layout_height="wrap_content" | 64 | android:layout_height="wrap_content" |
| 45 | android:layout_marginTop="@dimen/spacing_small" | 65 | android:layout_marginTop="16dp" |
| 46 | android:textAlignment="viewStart" | 66 | android:text="@string/clear" |
| 47 | tools:text="@string/frame_limit_enable_description" /> | 67 | android:visibility="gone" |
| 68 | tools:visibility="visible" /> | ||
| 48 | 69 | ||
| 49 | </LinearLayout> | 70 | </LinearLayout> |
| 50 | 71 | ||
diff --git a/src/android/app/src/main/res/menu/menu_in_game.xml b/src/android/app/src/main/res/menu/menu_in_game.xml index f98f727b6..ac6ab06ff 100644 --- a/src/android/app/src/main/res/menu/menu_in_game.xml +++ b/src/android/app/src/main/res/menu/menu_in_game.xml | |||
| @@ -12,6 +12,11 @@ | |||
| 12 | android:title="@string/preferences_settings" /> | 12 | android:title="@string/preferences_settings" /> |
| 13 | 13 | ||
| 14 | <item | 14 | <item |
| 15 | android:id="@+id/menu_settings_per_game" | ||
| 16 | android:icon="@drawable/ic_settings_outline" | ||
| 17 | android:title="@string/per_game_settings" /> | ||
| 18 | |||
| 19 | <item | ||
| 15 | android:id="@+id/menu_overlay_controls" | 20 | android:id="@+id/menu_overlay_controls" |
| 16 | android:icon="@drawable/ic_controller" | 21 | android:icon="@drawable/ic_controller" |
| 17 | android:title="@string/emulation_input_overlay" /> | 22 | android:title="@string/emulation_input_overlay" /> |
diff --git a/src/android/app/src/main/res/navigation/emulation_navigation.xml b/src/android/app/src/main/res/navigation/emulation_navigation.xml index cfc494b3f..2f8c3fa0d 100644 --- a/src/android/app/src/main/res/navigation/emulation_navigation.xml +++ b/src/android/app/src/main/res/navigation/emulation_navigation.xml | |||
| @@ -15,6 +15,10 @@ | |||
| 15 | app:argType="org.yuzu.yuzu_emu.model.Game" | 15 | app:argType="org.yuzu.yuzu_emu.model.Game" |
| 16 | app:nullable="true" | 16 | app:nullable="true" |
| 17 | android:defaultValue="@null" /> | 17 | android:defaultValue="@null" /> |
| 18 | <argument | ||
| 19 | android:name="custom" | ||
| 20 | app:argType="boolean" | ||
| 21 | android:defaultValue="false" /> | ||
| 18 | </fragment> | 22 | </fragment> |
| 19 | 23 | ||
| 20 | <activity | 24 | <activity |
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 cf70b4bc4..37a03a8d1 100644 --- a/src/android/app/src/main/res/navigation/home_navigation.xml +++ b/src/android/app/src/main/res/navigation/home_navigation.xml | |||
| @@ -77,6 +77,10 @@ | |||
| 77 | app:argType="org.yuzu.yuzu_emu.model.Game" | 77 | app:argType="org.yuzu.yuzu_emu.model.Game" |
| 78 | app:nullable="true" | 78 | app:nullable="true" |
| 79 | android:defaultValue="@null" /> | 79 | android:defaultValue="@null" /> |
| 80 | <argument | ||
| 81 | android:name="custom" | ||
| 82 | app:argType="boolean" | ||
| 83 | android:defaultValue="false" /> | ||
| 80 | </activity> | 84 | </activity> |
| 81 | 85 | ||
| 82 | <action | 86 | <action |
| @@ -107,7 +111,13 @@ | |||
| 107 | <fragment | 111 | <fragment |
| 108 | android:id="@+id/driverManagerFragment" | 112 | android:id="@+id/driverManagerFragment" |
| 109 | android:name="org.yuzu.yuzu_emu.fragments.DriverManagerFragment" | 113 | android:name="org.yuzu.yuzu_emu.fragments.DriverManagerFragment" |
| 110 | android:label="DriverManagerFragment" /> | 114 | android:label="DriverManagerFragment" > |
| 115 | <argument | ||
| 116 | android:name="game" | ||
| 117 | app:argType="org.yuzu.yuzu_emu.model.Game" | ||
| 118 | app:nullable="true" | ||
| 119 | android:defaultValue="@null" /> | ||
| 120 | </fragment> | ||
| 111 | <fragment | 121 | <fragment |
| 112 | android:id="@+id/appletLauncherFragment" | 122 | android:id="@+id/appletLauncherFragment" |
| 113 | android:name="org.yuzu.yuzu_emu.fragments.AppletLauncherFragment" | 123 | android:name="org.yuzu.yuzu_emu.fragments.AppletLauncherFragment" |
| @@ -124,5 +134,41 @@ | |||
| 124 | android:id="@+id/gameFoldersFragment" | 134 | android:id="@+id/gameFoldersFragment" |
| 125 | android:name="org.yuzu.yuzu_emu.fragments.GameFoldersFragment" | 135 | android:name="org.yuzu.yuzu_emu.fragments.GameFoldersFragment" |
| 126 | android:label="GameFoldersFragment" /> | 136 | android:label="GameFoldersFragment" /> |
| 137 | <fragment | ||
| 138 | android:id="@+id/perGamePropertiesFragment" | ||
| 139 | android:name="org.yuzu.yuzu_emu.fragments.GamePropertiesFragment" | ||
| 140 | android:label="PerGamePropertiesFragment" > | ||
| 141 | <argument | ||
| 142 | android:name="game" | ||
| 143 | app:argType="org.yuzu.yuzu_emu.model.Game" /> | ||
| 144 | <action | ||
| 145 | android:id="@+id/action_perGamePropertiesFragment_to_gameInfoFragment" | ||
| 146 | app:destination="@id/gameInfoFragment" /> | ||
| 147 | <action | ||
| 148 | android:id="@+id/action_perGamePropertiesFragment_to_addonsFragment" | ||
| 149 | app:destination="@id/addonsFragment" /> | ||
| 150 | <action | ||
| 151 | android:id="@+id/action_perGamePropertiesFragment_to_driverManagerFragment" | ||
| 152 | app:destination="@id/driverManagerFragment" /> | ||
| 153 | </fragment> | ||
| 154 | <action | ||
| 155 | android:id="@+id/action_global_perGamePropertiesFragment" | ||
| 156 | app:destination="@id/perGamePropertiesFragment" /> | ||
| 157 | <fragment | ||
| 158 | android:id="@+id/gameInfoFragment" | ||
| 159 | android:name="org.yuzu.yuzu_emu.fragments.GameInfoFragment" | ||
| 160 | android:label="GameInfoFragment" > | ||
| 161 | <argument | ||
| 162 | android:name="game" | ||
| 163 | app:argType="org.yuzu.yuzu_emu.model.Game" /> | ||
| 164 | </fragment> | ||
| 165 | <fragment | ||
| 166 | android:id="@+id/addonsFragment" | ||
| 167 | android:name="org.yuzu.yuzu_emu.fragments.AddonsFragment" | ||
| 168 | android:label="AddonsFragment" > | ||
| 169 | <argument | ||
| 170 | android:name="game" | ||
| 171 | app:argType="org.yuzu.yuzu_emu.model.Game" /> | ||
| 172 | </fragment> | ||
| 127 | 173 | ||
| 128 | </navigation> | 174 | </navigation> |
diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml index 380d14213..992b5ae44 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">76dp</dimen> | 16 | <dimen name="spacing_bottom_list_fab">96dp</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 a6ccef8a1..c86c43df2 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml | |||
| @@ -91,7 +91,10 @@ | |||
| 91 | <string name="notification_no_directory_link_description">Please locate the user folder with the file manager\'s side panel manually.</string> | 91 | <string name="notification_no_directory_link_description">Please locate the user folder with the file manager\'s side panel manually.</string> |
| 92 | <string name="manage_save_data">Manage save data</string> | 92 | <string name="manage_save_data">Manage save data</string> |
| 93 | <string name="manage_save_data_description">Save data found. Please select an option below.</string> | 93 | <string name="manage_save_data_description">Save data found. Please select an option below.</string> |
| 94 | <string name="import_save_warning">Import save data</string> | ||
| 95 | <string name="import_save_warning_description">This will overwrite all existing save data with the provided file. Are you sure that you want to continue?</string> | ||
| 94 | <string name="import_export_saves_description">Import or export save files</string> | 96 | <string name="import_export_saves_description">Import or export save files</string> |
| 97 | <string name="save_files_importing">Importing save files…</string> | ||
| 95 | <string name="save_files_exporting">Exporting save files…</string> | 98 | <string name="save_files_exporting">Exporting save files…</string> |
| 96 | <string name="save_file_imported_success">Imported successfully</string> | 99 | <string name="save_file_imported_success">Imported successfully</string> |
| 97 | <string name="save_file_invalid_zip_structure">Invalid save directory structure</string> | 100 | <string name="save_file_invalid_zip_structure">Invalid save directory structure</string> |
| @@ -266,6 +269,11 @@ | |||
| 266 | <string name="delete">Delete</string> | 269 | <string name="delete">Delete</string> |
| 267 | <string name="edit">Edit</string> | 270 | <string name="edit">Edit</string> |
| 268 | <string name="export_success">Exported successfully</string> | 271 | <string name="export_success">Exported successfully</string> |
| 272 | <string name="start">Start</string> | ||
| 273 | <string name="clear">Clear</string> | ||
| 274 | <string name="global">Global</string> | ||
| 275 | <string name="custom">Custom</string> | ||
| 276 | <string name="notice">Notice</string> | ||
| 269 | 277 | ||
| 270 | <!-- GPU driver installation --> | 278 | <!-- GPU driver installation --> |
| 271 | <string name="select_gpu_driver">Select GPU driver</string> | 279 | <string name="select_gpu_driver">Select GPU driver</string> |
| @@ -291,6 +299,44 @@ | |||
| 291 | <string name="preferences_debug">Debug</string> | 299 | <string name="preferences_debug">Debug</string> |
| 292 | <string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string> | 300 | <string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string> |
| 293 | 301 | ||
| 302 | <!-- Game properties --> | ||
| 303 | <string name="info">Info</string> | ||
| 304 | <string name="info_description">Program ID, developer, version</string> | ||
| 305 | <string name="per_game_settings">Per-game settings</string> | ||
| 306 | <string name="per_game_settings_description">Edit settings specific to this game</string> | ||
| 307 | <string name="launch_options">Launch config</string> | ||
| 308 | <string name="path">Path</string> | ||
| 309 | <string name="program_id">Program ID</string> | ||
| 310 | <string name="developer">Developer</string> | ||
| 311 | <string name="version">Version</string> | ||
| 312 | <string name="copy_details">Copy details</string> | ||
| 313 | <string name="add_ons">Add-ons</string> | ||
| 314 | <string name="add_ons_description">Toggle mods, updates and DLC</string> | ||
| 315 | <string name="clear_shader_cache">Clear shader cache</string> | ||
| 316 | <string name="clear_shader_cache_description">Removes all shaders built while playing this game</string> | ||
| 317 | <string name="clear_shader_cache_warning_description">You will experience more stuttering as the shader cache regenerates</string> | ||
| 318 | <string name="cleared_shaders_successfully">Cleared shaders successfully</string> | ||
| 319 | <string name="addons_game">Addons: %1$s</string> | ||
| 320 | <string name="save_data">Save data</string> | ||
| 321 | <string name="save_data_description">Manage save data specific to this game</string> | ||
| 322 | <string name="delete_save_data">Delete save data</string> | ||
| 323 | <string name="delete_save_data_description">Removes all save data specific to this game</string> | ||
| 324 | <string name="delete_save_data_warning_description">This irrecoverably removes all of this game\'s save data. Are you sure you want to continue?</string> | ||
| 325 | <string name="save_data_deleted_successfully">Save data deleted successfully</string> | ||
| 326 | <string name="select_content_type">Content type</string> | ||
| 327 | <string name="updates_and_dlc">Updates and DLC</string> | ||
| 328 | <string name="mods_and_cheats">Mods and cheats</string> | ||
| 329 | <string name="addon_notice">Important addon notice</string> | ||
| 330 | <!-- "cheats/" "romfs/" and "exefs/ should not be translated --> | ||
| 331 | <string name="addon_notice_description">In order to install mods and cheats, you must select a folder that contains a cheats/, romfs/, or exefs/ directory. We can\'t verify if these will be compatible with your game so be careful!</string> | ||
| 332 | <string name="invalid_directory">Invalid directory</string> | ||
| 333 | <!-- "cheats/" "romfs/" and "exefs/ should not be translated --> | ||
| 334 | <string name="invalid_directory_description">Please make sure that the directory you selected contains a cheats/, romfs/, or exefs/ folder and try again.</string> | ||
| 335 | <string name="addon_installed_successfully">Addon installed successfully</string> | ||
| 336 | <string name="verifying_content">Verifying content…</string> | ||
| 337 | <string name="content_install_notice">Content install notice</string> | ||
| 338 | <string name="content_install_notice_description">The content that you selected does not match this game.\nInstall anyway?</string> | ||
| 339 | |||
| 294 | <!-- ROM loading errors --> | 340 | <!-- ROM loading errors --> |
| 295 | <string name="loader_error_encrypted">Your ROM is encrypted</string> | 341 | <string name="loader_error_encrypted">Your ROM is encrypted</string> |
| 296 | <string name="loader_error_encrypted_roms_description"><![CDATA[Please follow the guides to redump your <a href="https://yuzu-emu.org/help/quickstart/#dumping-physical-titles-game-cards">game cartidges</a> or <a href="https://yuzu-emu.org/help/quickstart/#dumping-digital-titles-eshop">installed titles</a>.]]></string> | 342 | <string name="loader_error_encrypted_roms_description"><![CDATA[Please follow the guides to redump your <a href="https://yuzu-emu.org/help/quickstart/#dumping-physical-titles-game-cards">game cartidges</a> or <a href="https://yuzu-emu.org/help/quickstart/#dumping-digital-titles-eshop">installed titles</a>.]]></string> |
| @@ -369,6 +415,7 @@ | |||
| 369 | 415 | ||
| 370 | <!-- Memory Sizes --> | 416 | <!-- Memory Sizes --> |
| 371 | <string name="memory_byte">Byte</string> | 417 | <string name="memory_byte">Byte</string> |
| 418 | <string name="memory_byte_shorthand">B</string> | ||
| 372 | <string name="memory_kilobyte">KB</string> | 419 | <string name="memory_kilobyte">KB</string> |
| 373 | <string name="memory_megabyte">MB</string> | 420 | <string name="memory_megabyte">MB</string> |
| 374 | <string name="memory_gigabyte">GB</string> | 421 | <string name="memory_gigabyte">GB</string> |
diff --git a/src/common/settings.cpp b/src/common/settings.cpp index 88f509ba7..ea52bbfa6 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp | |||
| @@ -211,6 +211,8 @@ const char* TranslateCategory(Category category) { | |||
| 211 | case Category::Debugging: | 211 | case Category::Debugging: |
| 212 | case Category::DebuggingGraphics: | 212 | case Category::DebuggingGraphics: |
| 213 | return "Debugging"; | 213 | return "Debugging"; |
| 214 | case Category::GpuDriver: | ||
| 215 | return "GpuDriver"; | ||
| 214 | case Category::Miscellaneous: | 216 | case Category::Miscellaneous: |
| 215 | return "Miscellaneous"; | 217 | return "Miscellaneous"; |
| 216 | case Category::Network: | 218 | case Category::Network: |
diff --git a/src/common/settings.h b/src/common/settings.h index 7dc18fffe..07dba53ab 100644 --- a/src/common/settings.h +++ b/src/common/settings.h | |||
| @@ -197,7 +197,7 @@ struct Values { | |||
| 197 | SwitchableSetting<CpuAccuracy, true> cpu_accuracy{linkage, CpuAccuracy::Auto, | 197 | SwitchableSetting<CpuAccuracy, true> cpu_accuracy{linkage, CpuAccuracy::Auto, |
| 198 | CpuAccuracy::Auto, CpuAccuracy::Paranoid, | 198 | CpuAccuracy::Auto, CpuAccuracy::Paranoid, |
| 199 | "cpu_accuracy", Category::Cpu}; | 199 | "cpu_accuracy", Category::Cpu}; |
| 200 | Setting<bool> cpu_debug_mode{linkage, false, "cpu_debug_mode", Category::CpuDebug}; | 200 | SwitchableSetting<bool> cpu_debug_mode{linkage, false, "cpu_debug_mode", Category::CpuDebug}; |
| 201 | 201 | ||
| 202 | Setting<bool> cpuopt_page_tables{linkage, true, "cpuopt_page_tables", Category::CpuDebug}; | 202 | Setting<bool> cpuopt_page_tables{linkage, true, "cpuopt_page_tables", Category::CpuDebug}; |
| 203 | Setting<bool> cpuopt_block_linking{linkage, true, "cpuopt_block_linking", Category::CpuDebug}; | 203 | Setting<bool> cpuopt_block_linking{linkage, true, "cpuopt_block_linking", Category::CpuDebug}; |
| @@ -211,9 +211,9 @@ struct Values { | |||
| 211 | Setting<bool> cpuopt_misc_ir{linkage, true, "cpuopt_misc_ir", Category::CpuDebug}; | 211 | Setting<bool> cpuopt_misc_ir{linkage, true, "cpuopt_misc_ir", Category::CpuDebug}; |
| 212 | Setting<bool> cpuopt_reduce_misalign_checks{linkage, true, "cpuopt_reduce_misalign_checks", | 212 | Setting<bool> cpuopt_reduce_misalign_checks{linkage, true, "cpuopt_reduce_misalign_checks", |
| 213 | Category::CpuDebug}; | 213 | Category::CpuDebug}; |
| 214 | Setting<bool> cpuopt_fastmem{linkage, true, "cpuopt_fastmem", Category::CpuDebug}; | 214 | SwitchableSetting<bool> cpuopt_fastmem{linkage, true, "cpuopt_fastmem", Category::CpuDebug}; |
| 215 | Setting<bool> cpuopt_fastmem_exclusives{linkage, true, "cpuopt_fastmem_exclusives", | 215 | SwitchableSetting<bool> cpuopt_fastmem_exclusives{linkage, true, "cpuopt_fastmem_exclusives", |
| 216 | Category::CpuDebug}; | 216 | Category::CpuDebug}; |
| 217 | Setting<bool> cpuopt_recompile_exclusives{linkage, true, "cpuopt_recompile_exclusives", | 217 | Setting<bool> cpuopt_recompile_exclusives{linkage, true, "cpuopt_recompile_exclusives", |
| 218 | Category::CpuDebug}; | 218 | Category::CpuDebug}; |
| 219 | Setting<bool> cpuopt_ignore_memory_aborts{linkage, true, "cpuopt_ignore_memory_aborts", | 219 | Setting<bool> cpuopt_ignore_memory_aborts{linkage, true, "cpuopt_ignore_memory_aborts", |
| @@ -256,7 +256,7 @@ struct Values { | |||
| 256 | AstcDecodeMode::CpuAsynchronous, | 256 | AstcDecodeMode::CpuAsynchronous, |
| 257 | "accelerate_astc", | 257 | "accelerate_astc", |
| 258 | Category::Renderer}; | 258 | Category::Renderer}; |
| 259 | Setting<VSyncMode, true> vsync_mode{ | 259 | SwitchableSetting<VSyncMode, true> vsync_mode{ |
| 260 | linkage, VSyncMode::Fifo, VSyncMode::Immediate, VSyncMode::FifoRelaxed, | 260 | linkage, VSyncMode::Fifo, VSyncMode::Immediate, VSyncMode::FifoRelaxed, |
| 261 | "use_vsync", Category::Renderer, Specialization::RuntimeList, true, | 261 | "use_vsync", Category::Renderer, Specialization::RuntimeList, true, |
| 262 | true}; | 262 | true}; |
diff --git a/src/common/settings_common.h b/src/common/settings_common.h index 344c04439..c82e17495 100644 --- a/src/common/settings_common.h +++ b/src/common/settings_common.h | |||
| @@ -26,6 +26,7 @@ enum class Category : u32 { | |||
| 26 | DataStorage, | 26 | DataStorage, |
| 27 | Debugging, | 27 | Debugging, |
| 28 | DebuggingGraphics, | 28 | DebuggingGraphics, |
| 29 | GpuDriver, | ||
| 29 | Miscellaneous, | 30 | Miscellaneous, |
| 30 | Network, | 31 | Network, |
| 31 | WebService, | 32 | WebService, |
diff --git a/src/common/settings_setting.h b/src/common/settings_setting.h index 3175ab07d..0b18ca5ec 100644 --- a/src/common/settings_setting.h +++ b/src/common/settings_setting.h | |||
| @@ -81,6 +81,9 @@ public: | |||
| 81 | [[nodiscard]] virtual const Type& GetValue() const { | 81 | [[nodiscard]] virtual const Type& GetValue() const { |
| 82 | return value; | 82 | return value; |
| 83 | } | 83 | } |
| 84 | [[nodiscard]] virtual const Type& GetValue(bool need_global) const { | ||
| 85 | return value; | ||
| 86 | } | ||
| 84 | 87 | ||
| 85 | /** | 88 | /** |
| 86 | * Sets the setting to the given value. | 89 | * Sets the setting to the given value. |
| @@ -353,7 +356,7 @@ public: | |||
| 353 | } | 356 | } |
| 354 | return custom; | 357 | return custom; |
| 355 | } | 358 | } |
| 356 | [[nodiscard]] const Type& GetValue(bool need_global) const { | 359 | [[nodiscard]] const Type& GetValue(bool need_global) const override final { |
| 357 | if (use_global || need_global) { | 360 | if (use_global || need_global) { |
| 358 | return this->value; | 361 | return this->value; |
| 359 | } | 362 | } |
diff --git a/src/core/hle/service/nvnflinger/fb_share_buffer_manager.cpp b/src/core/hle/service/nvnflinger/fb_share_buffer_manager.cpp index 75bf31e32..2fef6cc1a 100644 --- a/src/core/hle/service/nvnflinger/fb_share_buffer_manager.cpp +++ b/src/core/hle/service/nvnflinger/fb_share_buffer_manager.cpp | |||
| @@ -204,8 +204,9 @@ Result FbShareBufferManager::Initialize(u64* out_buffer_id, u64* out_layer_id, u | |||
| 204 | // Record the display id. | 204 | // Record the display id. |
| 205 | m_display_id = display_id; | 205 | m_display_id = display_id; |
| 206 | 206 | ||
| 207 | // Create a layer for the display. | 207 | // Create and open a layer for the display. |
| 208 | m_layer_id = m_flinger.CreateLayer(m_display_id).value(); | 208 | m_layer_id = m_flinger.CreateLayer(m_display_id).value(); |
| 209 | m_flinger.OpenLayer(m_layer_id); | ||
| 209 | 210 | ||
| 210 | // Set up the buffer. | 211 | // Set up the buffer. |
| 211 | m_buffer_id = m_next_buffer_id++; | 212 | m_buffer_id = m_next_buffer_id++; |
diff --git a/src/core/hle/service/nvnflinger/nvnflinger.cpp b/src/core/hle/service/nvnflinger/nvnflinger.cpp index 0745434c5..6352b09a9 100644 --- a/src/core/hle/service/nvnflinger/nvnflinger.cpp +++ b/src/core/hle/service/nvnflinger/nvnflinger.cpp | |||
| @@ -176,17 +176,37 @@ void Nvnflinger::CreateLayerAtId(VI::Display& display, u64 layer_id) { | |||
| 176 | display.CreateLayer(layer_id, buffer_id, nvdrv->container); | 176 | display.CreateLayer(layer_id, buffer_id, nvdrv->container); |
| 177 | } | 177 | } |
| 178 | 178 | ||
| 179 | void Nvnflinger::OpenLayer(u64 layer_id) { | ||
| 180 | const auto lock_guard = Lock(); | ||
| 181 | |||
| 182 | for (auto& display : displays) { | ||
| 183 | if (auto* layer = display.FindLayer(layer_id); layer) { | ||
| 184 | layer->Open(); | ||
| 185 | } | ||
| 186 | } | ||
| 187 | } | ||
| 188 | |||
| 179 | void Nvnflinger::CloseLayer(u64 layer_id) { | 189 | void Nvnflinger::CloseLayer(u64 layer_id) { |
| 180 | const auto lock_guard = Lock(); | 190 | const auto lock_guard = Lock(); |
| 181 | 191 | ||
| 182 | for (auto& display : displays) { | 192 | for (auto& display : displays) { |
| 183 | display.CloseLayer(layer_id); | 193 | if (auto* layer = display.FindLayer(layer_id); layer) { |
| 194 | layer->Close(); | ||
| 195 | } | ||
| 196 | } | ||
| 197 | } | ||
| 198 | |||
| 199 | void Nvnflinger::DestroyLayer(u64 layer_id) { | ||
| 200 | const auto lock_guard = Lock(); | ||
| 201 | |||
| 202 | for (auto& display : displays) { | ||
| 203 | display.DestroyLayer(layer_id); | ||
| 184 | } | 204 | } |
| 185 | } | 205 | } |
| 186 | 206 | ||
| 187 | std::optional<u32> Nvnflinger::FindBufferQueueId(u64 display_id, u64 layer_id) { | 207 | std::optional<u32> Nvnflinger::FindBufferQueueId(u64 display_id, u64 layer_id) { |
| 188 | const auto lock_guard = Lock(); | 208 | const auto lock_guard = Lock(); |
| 189 | const auto* const layer = FindOrCreateLayer(display_id, layer_id); | 209 | const auto* const layer = FindLayer(display_id, layer_id); |
| 190 | 210 | ||
| 191 | if (layer == nullptr) { | 211 | if (layer == nullptr) { |
| 192 | return std::nullopt; | 212 | return std::nullopt; |
| @@ -240,24 +260,6 @@ VI::Layer* Nvnflinger::FindLayer(u64 display_id, u64 layer_id) { | |||
| 240 | return display->FindLayer(layer_id); | 260 | return display->FindLayer(layer_id); |
| 241 | } | 261 | } |
| 242 | 262 | ||
| 243 | VI::Layer* Nvnflinger::FindOrCreateLayer(u64 display_id, u64 layer_id) { | ||
| 244 | auto* const display = FindDisplay(display_id); | ||
| 245 | |||
| 246 | if (display == nullptr) { | ||
| 247 | return nullptr; | ||
| 248 | } | ||
| 249 | |||
| 250 | auto* layer = display->FindLayer(layer_id); | ||
| 251 | |||
| 252 | if (layer == nullptr) { | ||
| 253 | LOG_DEBUG(Service_Nvnflinger, "Layer at id {} not found. Trying to create it.", layer_id); | ||
| 254 | CreateLayerAtId(*display, layer_id); | ||
| 255 | return display->FindLayer(layer_id); | ||
| 256 | } | ||
| 257 | |||
| 258 | return layer; | ||
| 259 | } | ||
| 260 | |||
| 261 | void Nvnflinger::Compose() { | 263 | void Nvnflinger::Compose() { |
| 262 | for (auto& display : displays) { | 264 | for (auto& display : displays) { |
| 263 | // Trigger vsync for this display at the end of drawing | 265 | // Trigger vsync for this display at the end of drawing |
diff --git a/src/core/hle/service/nvnflinger/nvnflinger.h b/src/core/hle/service/nvnflinger/nvnflinger.h index f5d73acdb..871285764 100644 --- a/src/core/hle/service/nvnflinger/nvnflinger.h +++ b/src/core/hle/service/nvnflinger/nvnflinger.h | |||
| @@ -73,9 +73,15 @@ public: | |||
| 73 | /// If an invalid display ID is specified, then an empty optional is returned. | 73 | /// If an invalid display ID is specified, then an empty optional is returned. |
| 74 | [[nodiscard]] std::optional<u64> CreateLayer(u64 display_id); | 74 | [[nodiscard]] std::optional<u64> CreateLayer(u64 display_id); |
| 75 | 75 | ||
| 76 | /// Opens a layer on all displays for the given layer ID. | ||
| 77 | void OpenLayer(u64 layer_id); | ||
| 78 | |||
| 76 | /// Closes a layer on all displays for the given layer ID. | 79 | /// Closes a layer on all displays for the given layer ID. |
| 77 | void CloseLayer(u64 layer_id); | 80 | void CloseLayer(u64 layer_id); |
| 78 | 81 | ||
| 82 | /// Destroys the given layer ID. | ||
| 83 | void DestroyLayer(u64 layer_id); | ||
| 84 | |||
| 79 | /// Finds the buffer queue ID of the specified layer in the specified display. | 85 | /// Finds the buffer queue ID of the specified layer in the specified display. |
| 80 | /// | 86 | /// |
| 81 | /// If an invalid display ID or layer ID is provided, then an empty optional is returned. | 87 | /// If an invalid display ID or layer ID is provided, then an empty optional is returned. |
| @@ -117,11 +123,6 @@ private: | |||
| 117 | /// Finds the layer identified by the specified ID in the desired display. | 123 | /// Finds the layer identified by the specified ID in the desired display. |
| 118 | [[nodiscard]] VI::Layer* FindLayer(u64 display_id, u64 layer_id); | 124 | [[nodiscard]] VI::Layer* FindLayer(u64 display_id, u64 layer_id); |
| 119 | 125 | ||
| 120 | /// Finds the layer identified by the specified ID in the desired display, | ||
| 121 | /// or creates the layer if it is not found. | ||
| 122 | /// To be used when the system expects the specified ID to already exist. | ||
| 123 | [[nodiscard]] VI::Layer* FindOrCreateLayer(u64 display_id, u64 layer_id); | ||
| 124 | |||
| 125 | /// Creates a layer with the specified layer ID in the desired display. | 126 | /// Creates a layer with the specified layer ID in the desired display. |
| 126 | void CreateLayerAtId(VI::Display& display, u64 layer_id); | 127 | void CreateLayerAtId(VI::Display& display, u64 layer_id); |
| 127 | 128 | ||
diff --git a/src/core/hle/service/vi/display/vi_display.cpp b/src/core/hle/service/vi/display/vi_display.cpp index d30f49877..71ce9be50 100644 --- a/src/core/hle/service/vi/display/vi_display.cpp +++ b/src/core/hle/service/vi/display/vi_display.cpp | |||
| @@ -51,11 +51,24 @@ Display::~Display() { | |||
| 51 | } | 51 | } |
| 52 | 52 | ||
| 53 | Layer& Display::GetLayer(std::size_t index) { | 53 | Layer& Display::GetLayer(std::size_t index) { |
| 54 | return *layers.at(index); | 54 | size_t i = 0; |
| 55 | for (auto& layer : layers) { | ||
| 56 | if (!layer->IsOpen()) { | ||
| 57 | continue; | ||
| 58 | } | ||
| 59 | |||
| 60 | if (i == index) { | ||
| 61 | return *layer; | ||
| 62 | } | ||
| 63 | |||
| 64 | i++; | ||
| 65 | } | ||
| 66 | |||
| 67 | UNREACHABLE(); | ||
| 55 | } | 68 | } |
| 56 | 69 | ||
| 57 | const Layer& Display::GetLayer(std::size_t index) const { | 70 | size_t Display::GetNumLayers() const { |
| 58 | return *layers.at(index); | 71 | return std::ranges::count_if(layers, [](auto& l) { return l->IsOpen(); }); |
| 59 | } | 72 | } |
| 60 | 73 | ||
| 61 | Result Display::GetVSyncEvent(Kernel::KReadableEvent** out_vsync_event) { | 74 | Result Display::GetVSyncEvent(Kernel::KReadableEvent** out_vsync_event) { |
| @@ -92,7 +105,11 @@ void Display::CreateLayer(u64 layer_id, u32 binder_id, | |||
| 92 | hos_binder_driver_server.RegisterProducer(std::move(producer)); | 105 | hos_binder_driver_server.RegisterProducer(std::move(producer)); |
| 93 | } | 106 | } |
| 94 | 107 | ||
| 95 | void Display::CloseLayer(u64 layer_id) { | 108 | void Display::DestroyLayer(u64 layer_id) { |
| 109 | if (auto* layer = this->FindLayer(layer_id); layer != nullptr) { | ||
| 110 | layer->GetConsumer().Abandon(); | ||
| 111 | } | ||
| 112 | |||
| 96 | std::erase_if(layers, | 113 | std::erase_if(layers, |
| 97 | [layer_id](const auto& layer) { return layer->GetLayerId() == layer_id; }); | 114 | [layer_id](const auto& layer) { return layer->GetLayerId() == layer_id; }); |
| 98 | } | 115 | } |
diff --git a/src/core/hle/service/vi/display/vi_display.h b/src/core/hle/service/vi/display/vi_display.h index 101cbce20..1d9360b96 100644 --- a/src/core/hle/service/vi/display/vi_display.h +++ b/src/core/hle/service/vi/display/vi_display.h | |||
| @@ -66,18 +66,13 @@ public: | |||
| 66 | 66 | ||
| 67 | /// Whether or not this display has any layers added to it. | 67 | /// Whether or not this display has any layers added to it. |
| 68 | bool HasLayers() const { | 68 | bool HasLayers() const { |
| 69 | return !layers.empty(); | 69 | return GetNumLayers() > 0; |
| 70 | } | 70 | } |
| 71 | 71 | ||
| 72 | /// Gets a layer for this display based off an index. | 72 | /// Gets a layer for this display based off an index. |
| 73 | Layer& GetLayer(std::size_t index); | 73 | Layer& GetLayer(std::size_t index); |
| 74 | 74 | ||
| 75 | /// Gets a layer for this display based off an index. | 75 | std::size_t GetNumLayers() const; |
| 76 | const Layer& GetLayer(std::size_t index) const; | ||
| 77 | |||
| 78 | std::size_t GetNumLayers() const { | ||
| 79 | return layers.size(); | ||
| 80 | } | ||
| 81 | 76 | ||
| 82 | /** | 77 | /** |
| 83 | * Gets the internal vsync event. | 78 | * Gets the internal vsync event. |
| @@ -100,11 +95,11 @@ public: | |||
| 100 | /// | 95 | /// |
| 101 | void CreateLayer(u64 layer_id, u32 binder_id, Service::Nvidia::NvCore::Container& core); | 96 | void CreateLayer(u64 layer_id, u32 binder_id, Service::Nvidia::NvCore::Container& core); |
| 102 | 97 | ||
| 103 | /// Closes and removes a layer from this display with the given ID. | 98 | /// Removes a layer from this display with the given ID. |
| 104 | /// | 99 | /// |
| 105 | /// @param layer_id The ID assigned to the layer to close. | 100 | /// @param layer_id The ID assigned to the layer to destroy. |
| 106 | /// | 101 | /// |
| 107 | void CloseLayer(u64 layer_id); | 102 | void DestroyLayer(u64 layer_id); |
| 108 | 103 | ||
| 109 | /// Resets the display for a new connection. | 104 | /// Resets the display for a new connection. |
| 110 | void Reset() { | 105 | void Reset() { |
diff --git a/src/core/hle/service/vi/layer/vi_layer.cpp b/src/core/hle/service/vi/layer/vi_layer.cpp index 9ae2e0e44..04e52a23b 100644 --- a/src/core/hle/service/vi/layer/vi_layer.cpp +++ b/src/core/hle/service/vi/layer/vi_layer.cpp | |||
| @@ -8,8 +8,8 @@ namespace Service::VI { | |||
| 8 | Layer::Layer(u64 layer_id_, u32 binder_id_, android::BufferQueueCore& core_, | 8 | Layer::Layer(u64 layer_id_, u32 binder_id_, android::BufferQueueCore& core_, |
| 9 | android::BufferQueueProducer& binder_, | 9 | android::BufferQueueProducer& binder_, |
| 10 | std::shared_ptr<android::BufferItemConsumer>&& consumer_) | 10 | std::shared_ptr<android::BufferItemConsumer>&& consumer_) |
| 11 | : layer_id{layer_id_}, binder_id{binder_id_}, core{core_}, binder{binder_}, consumer{std::move( | 11 | : layer_id{layer_id_}, binder_id{binder_id_}, core{core_}, binder{binder_}, |
| 12 | consumer_)} {} | 12 | consumer{std::move(consumer_)}, open{false} {} |
| 13 | 13 | ||
| 14 | Layer::~Layer() = default; | 14 | Layer::~Layer() = default; |
| 15 | 15 | ||
diff --git a/src/core/hle/service/vi/layer/vi_layer.h b/src/core/hle/service/vi/layer/vi_layer.h index 8cf1b5275..295005e23 100644 --- a/src/core/hle/service/vi/layer/vi_layer.h +++ b/src/core/hle/service/vi/layer/vi_layer.h | |||
| @@ -71,12 +71,25 @@ public: | |||
| 71 | return core; | 71 | return core; |
| 72 | } | 72 | } |
| 73 | 73 | ||
| 74 | bool IsOpen() const { | ||
| 75 | return open; | ||
| 76 | } | ||
| 77 | |||
| 78 | void Close() { | ||
| 79 | open = false; | ||
| 80 | } | ||
| 81 | |||
| 82 | void Open() { | ||
| 83 | open = true; | ||
| 84 | } | ||
| 85 | |||
| 74 | private: | 86 | private: |
| 75 | const u64 layer_id; | 87 | const u64 layer_id; |
| 76 | const u32 binder_id; | 88 | const u32 binder_id; |
| 77 | android::BufferQueueCore& core; | 89 | android::BufferQueueCore& core; |
| 78 | android::BufferQueueProducer& binder; | 90 | android::BufferQueueProducer& binder; |
| 79 | std::shared_ptr<android::BufferItemConsumer> consumer; | 91 | std::shared_ptr<android::BufferItemConsumer> consumer; |
| 92 | bool open; | ||
| 80 | }; | 93 | }; |
| 81 | 94 | ||
| 82 | } // namespace Service::VI | 95 | } // namespace Service::VI |
diff --git a/src/core/hle/service/vi/vi.cpp b/src/core/hle/service/vi/vi.cpp index b1bfb9898..9ab8788e3 100644 --- a/src/core/hle/service/vi/vi.cpp +++ b/src/core/hle/service/vi/vi.cpp | |||
| @@ -719,6 +719,8 @@ private: | |||
| 719 | return; | 719 | return; |
| 720 | } | 720 | } |
| 721 | 721 | ||
| 722 | nv_flinger.OpenLayer(layer_id); | ||
| 723 | |||
| 722 | android::OutputParcel parcel; | 724 | android::OutputParcel parcel; |
| 723 | parcel.WriteInterface(NativeWindow{*buffer_queue_id}); | 725 | parcel.WriteInterface(NativeWindow{*buffer_queue_id}); |
| 724 | 726 | ||
| @@ -783,6 +785,7 @@ private: | |||
| 783 | const u64 layer_id = rp.Pop<u64>(); | 785 | const u64 layer_id = rp.Pop<u64>(); |
| 784 | 786 | ||
| 785 | LOG_WARNING(Service_VI, "(STUBBED) called. layer_id=0x{:016X}", layer_id); | 787 | LOG_WARNING(Service_VI, "(STUBBED) called. layer_id=0x{:016X}", layer_id); |
| 788 | nv_flinger.DestroyLayer(layer_id); | ||
| 786 | 789 | ||
| 787 | IPC::ResponseBuilder rb{ctx, 2}; | 790 | IPC::ResponseBuilder rb{ctx, 2}; |
| 788 | rb.Push(ResultSuccess); | 791 | rb.Push(ResultSuccess); |
diff --git a/src/frontend_common/config.cpp b/src/frontend_common/config.cpp index 1a0491c2c..d9f99148b 100644 --- a/src/frontend_common/config.cpp +++ b/src/frontend_common/config.cpp | |||
| @@ -214,6 +214,7 @@ void Config::ReadControlValues() { | |||
| 214 | } | 214 | } |
| 215 | 215 | ||
| 216 | void Config::ReadMotionTouchValues() { | 216 | void Config::ReadMotionTouchValues() { |
| 217 | Settings::values.touch_from_button_maps.clear(); | ||
| 217 | int num_touch_from_button_maps = BeginArray(std::string("touch_from_button_maps")); | 218 | int num_touch_from_button_maps = BeginArray(std::string("touch_from_button_maps")); |
| 218 | 219 | ||
| 219 | if (num_touch_from_button_maps > 0) { | 220 | if (num_touch_from_button_maps > 0) { |
diff --git a/src/video_core/renderer_opengl/gl_buffer_cache.cpp b/src/video_core/renderer_opengl/gl_buffer_cache.cpp index e6c70fb34..a71866b75 100644 --- a/src/video_core/renderer_opengl/gl_buffer_cache.cpp +++ b/src/video_core/renderer_opengl/gl_buffer_cache.cpp | |||
| @@ -333,7 +333,7 @@ void BufferCacheRuntime::BindTransformFeedbackBuffers(VideoCommon::HostBindings< | |||
| 333 | glBindBuffersRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, | 333 | glBindBuffersRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, |
| 334 | static_cast<GLsizei>(bindings.buffers.size()), buffer_handles.data(), | 334 | static_cast<GLsizei>(bindings.buffers.size()), buffer_handles.data(), |
| 335 | reinterpret_cast<const GLintptr*>(bindings.offsets.data()), | 335 | reinterpret_cast<const GLintptr*>(bindings.offsets.data()), |
| 336 | reinterpret_cast<const GLsizeiptr*>(bindings.strides.data())); | 336 | reinterpret_cast<const GLsizeiptr*>(bindings.sizes.data())); |
| 337 | } | 337 | } |
| 338 | 338 | ||
| 339 | void BufferCacheRuntime::BindTextureBuffer(Buffer& buffer, u32 offset, u32 size, | 339 | void BufferCacheRuntime::BindTextureBuffer(Buffer& buffer, u32 offset, u32 size, |
diff --git a/src/video_core/renderer_vulkan/vk_present_manager.cpp b/src/video_core/renderer_vulkan/vk_present_manager.cpp index 8e4c74b5c..a59e2d2d1 100644 --- a/src/video_core/renderer_vulkan/vk_present_manager.cpp +++ b/src/video_core/renderer_vulkan/vk_present_manager.cpp | |||
| @@ -102,8 +102,8 @@ PresentManager::PresentManager(const vk::Instance& instance_, | |||
| 102 | memory_allocator{memory_allocator_}, scheduler{scheduler_}, swapchain{swapchain_}, | 102 | memory_allocator{memory_allocator_}, scheduler{scheduler_}, swapchain{swapchain_}, |
| 103 | surface{surface_}, blit_supported{CanBlitToSwapchain(device.GetPhysical(), | 103 | surface{surface_}, blit_supported{CanBlitToSwapchain(device.GetPhysical(), |
| 104 | swapchain.GetImageViewFormat())}, | 104 | swapchain.GetImageViewFormat())}, |
| 105 | use_present_thread{Settings::values.async_presentation.GetValue()}, | 105 | use_present_thread{Settings::values.async_presentation.GetValue()} { |
| 106 | image_count{swapchain.GetImageCount()} { | 106 | SetImageCount(); |
| 107 | 107 | ||
| 108 | auto& dld = device.GetLogical(); | 108 | auto& dld = device.GetLogical(); |
| 109 | cmdpool = dld.CreateCommandPool({ | 109 | cmdpool = dld.CreateCommandPool({ |
| @@ -289,7 +289,14 @@ void PresentManager::PresentThread(std::stop_token token) { | |||
| 289 | 289 | ||
| 290 | void PresentManager::RecreateSwapchain(Frame* frame) { | 290 | void PresentManager::RecreateSwapchain(Frame* frame) { |
| 291 | swapchain.Create(*surface, frame->width, frame->height); | 291 | swapchain.Create(*surface, frame->width, frame->height); |
| 292 | image_count = swapchain.GetImageCount(); | 292 | SetImageCount(); |
| 293 | } | ||
| 294 | |||
| 295 | void PresentManager::SetImageCount() { | ||
| 296 | // We cannot have more than 5 images in flight at any given time. | ||
| 297 | // FRAMES_IN_FLIGHT is 7, and the cache TICKS_TO_DESTROY is 6. | ||
| 298 | // Mali drivers will give us 6. | ||
| 299 | image_count = std::min<size_t>(swapchain.GetImageCount(), 5); | ||
| 293 | } | 300 | } |
| 294 | 301 | ||
| 295 | void PresentManager::CopyToSwapchain(Frame* frame) { | 302 | void PresentManager::CopyToSwapchain(Frame* frame) { |
diff --git a/src/video_core/renderer_vulkan/vk_present_manager.h b/src/video_core/renderer_vulkan/vk_present_manager.h index 337171a09..23ee61c8c 100644 --- a/src/video_core/renderer_vulkan/vk_present_manager.h +++ b/src/video_core/renderer_vulkan/vk_present_manager.h | |||
| @@ -62,6 +62,8 @@ private: | |||
| 62 | 62 | ||
| 63 | void RecreateSwapchain(Frame* frame); | 63 | void RecreateSwapchain(Frame* frame); |
| 64 | 64 | ||
| 65 | void SetImageCount(); | ||
| 66 | |||
| 65 | private: | 67 | private: |
| 66 | const vk::Instance& instance; | 68 | const vk::Instance& instance; |
| 67 | Core::Frontend::EmuWindow& render_window; | 69 | Core::Frontend::EmuWindow& render_window; |