diff options
| author | 2023-12-10 20:27:50 -0500 | |
|---|---|---|
| committer | 2023-12-12 17:25:36 -0500 | |
| commit | e975f3cde9d4dcb1d9e2bbce116f9a9ba99bf03f (patch) | |
| tree | 7195fba36da6368913d75d633649d74915383cd8 /src/android | |
| parent | frontend_common: Fix settings reload bug (diff) | |
| download | yuzu-e975f3cde9d4dcb1d9e2bbce116f9a9ba99bf03f.tar.gz yuzu-e975f3cde9d4dcb1d9e2bbce116f9a9ba99bf03f.tar.xz yuzu-e975f3cde9d4dcb1d9e2bbce116f9a9ba99bf03f.zip | |
android: Add Game properties
This commit has the UI for viewing a game's properties on long-press and some links to useful tools like
- Game info
- Shortcut to settings (global in this commit)
- Addon manager with installer
- Save data manager
- Option to clear all save data
- Option to clear shader cache
Diffstat (limited to 'src/android')
40 files changed, 2227 insertions, 253 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..95b98798d 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,23 @@ 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 | /** | ||
| 542 | * Button type for use in onTouchEvent | 551 | * Button type for use in onTouchEvent |
| 543 | */ | 552 | */ |
| 544 | object ButtonType { | 553 | object ButtonType { |
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/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt index 2ef638559..928bfe5a7 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) : |
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..ff6270fa8 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt | |||
| @@ -0,0 +1,133 @@ | |||
| 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.CardInstallableBinding | ||
| 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 | CardInstallableBinding.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: CardInstallableBinding) : | ||
| 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 | |||
| 118 | if (installableProperty.install != null) { | ||
| 119 | binding.buttonInstall.visibility = View.VISIBLE | ||
| 120 | binding.buttonInstall.setOnClickListener { installableProperty.install.invoke() } | ||
| 121 | } | ||
| 122 | if (installableProperty.export != null) { | ||
| 123 | binding.buttonExport.visibility = View.VISIBLE | ||
| 124 | binding.buttonExport.setOnClickListener { installableProperty.export.invoke() } | ||
| 125 | } | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | enum class PropertyType { | ||
| 130 | Submenu, | ||
| 131 | Installable | ||
| 132 | } | ||
| 133 | } | ||
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/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..485989e2e --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt | |||
| @@ -0,0 +1,418 @@ | |||
| 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.os.Bundle | ||
| 7 | import android.text.TextUtils | ||
| 8 | import android.view.LayoutInflater | ||
| 9 | import android.view.View | ||
| 10 | import android.view.ViewGroup | ||
| 11 | import android.widget.Toast | ||
| 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.fragment.app.Fragment | ||
| 17 | import androidx.fragment.app.activityViewModels | ||
| 18 | import androidx.lifecycle.Lifecycle | ||
| 19 | import androidx.lifecycle.lifecycleScope | ||
| 20 | import androidx.lifecycle.repeatOnLifecycle | ||
| 21 | import androidx.navigation.findNavController | ||
| 22 | import androidx.navigation.fragment.navArgs | ||
| 23 | import androidx.recyclerview.widget.GridLayoutManager | ||
| 24 | import com.google.android.material.transition.MaterialSharedAxis | ||
| 25 | import kotlinx.coroutines.Dispatchers | ||
| 26 | import kotlinx.coroutines.launch | ||
| 27 | import kotlinx.coroutines.withContext | ||
| 28 | import org.yuzu.yuzu_emu.HomeNavigationDirections | ||
| 29 | import org.yuzu.yuzu_emu.R | ||
| 30 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 31 | import org.yuzu.yuzu_emu.adapters.GamePropertiesAdapter | ||
| 32 | import org.yuzu.yuzu_emu.databinding.FragmentGamePropertiesBinding | ||
| 33 | import org.yuzu.yuzu_emu.features.settings.model.Settings | ||
| 34 | import org.yuzu.yuzu_emu.model.DriverViewModel | ||
| 35 | import org.yuzu.yuzu_emu.model.GameProperty | ||
| 36 | import org.yuzu.yuzu_emu.model.GamesViewModel | ||
| 37 | import org.yuzu.yuzu_emu.model.HomeViewModel | ||
| 38 | import org.yuzu.yuzu_emu.model.InstallableProperty | ||
| 39 | import org.yuzu.yuzu_emu.model.SubmenuProperty | ||
| 40 | import org.yuzu.yuzu_emu.model.TaskState | ||
| 41 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization | ||
| 42 | import org.yuzu.yuzu_emu.utils.FileUtil | ||
| 43 | import org.yuzu.yuzu_emu.utils.GameIconUtils | ||
| 44 | import org.yuzu.yuzu_emu.utils.GpuDriverHelper | ||
| 45 | import org.yuzu.yuzu_emu.utils.MemoryUtil | ||
| 46 | import java.io.BufferedInputStream | ||
| 47 | import java.io.BufferedOutputStream | ||
| 48 | import java.io.File | ||
| 49 | |||
| 50 | class GamePropertiesFragment : Fragment() { | ||
| 51 | private var _binding: FragmentGamePropertiesBinding? = null | ||
| 52 | private val binding get() = _binding!! | ||
| 53 | |||
| 54 | private val homeViewModel: HomeViewModel by activityViewModels() | ||
| 55 | private val gamesViewModel: GamesViewModel by activityViewModels() | ||
| 56 | private val driverViewModel: DriverViewModel by activityViewModels() | ||
| 57 | |||
| 58 | private val args by navArgs<GamePropertiesFragmentArgs>() | ||
| 59 | |||
| 60 | override fun onCreate(savedInstanceState: Bundle?) { | ||
| 61 | super.onCreate(savedInstanceState) | ||
| 62 | enterTransition = MaterialSharedAxis(MaterialSharedAxis.Y, true) | ||
| 63 | returnTransition = MaterialSharedAxis(MaterialSharedAxis.Y, false) | ||
| 64 | reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) | ||
| 65 | } | ||
| 66 | |||
| 67 | override fun onCreateView( | ||
| 68 | inflater: LayoutInflater, | ||
| 69 | container: ViewGroup?, | ||
| 70 | savedInstanceState: Bundle? | ||
| 71 | ): View { | ||
| 72 | _binding = FragmentGamePropertiesBinding.inflate(layoutInflater) | ||
| 73 | return binding.root | ||
| 74 | } | ||
| 75 | |||
| 76 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| 77 | super.onViewCreated(view, savedInstanceState) | ||
| 78 | homeViewModel.setNavigationVisibility(visible = false, animated = true) | ||
| 79 | homeViewModel.setStatusBarShadeVisibility(true) | ||
| 80 | |||
| 81 | binding.buttonBack.setOnClickListener { | ||
| 82 | view.findNavController().popBackStack() | ||
| 83 | } | ||
| 84 | |||
| 85 | GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen) | ||
| 86 | binding.title.text = args.game.title | ||
| 87 | binding.title.postDelayed( | ||
| 88 | { | ||
| 89 | binding.title.ellipsize = TextUtils.TruncateAt.MARQUEE | ||
| 90 | binding.title.isSelected = true | ||
| 91 | }, | ||
| 92 | 3000 | ||
| 93 | ) | ||
| 94 | |||
| 95 | binding.buttonStart.setOnClickListener { | ||
| 96 | LaunchGameDialogFragment.newInstance(args.game) | ||
| 97 | .show(childFragmentManager, LaunchGameDialogFragment.TAG) | ||
| 98 | } | ||
| 99 | |||
| 100 | reloadList() | ||
| 101 | |||
| 102 | viewLifecycleOwner.lifecycleScope.launch { | ||
| 103 | repeatOnLifecycle(Lifecycle.State.STARTED) { | ||
| 104 | homeViewModel.openImportSaves.collect { | ||
| 105 | if (it) { | ||
| 106 | importSaves.launch(arrayOf("application/zip")) | ||
| 107 | homeViewModel.setOpenImportSaves(false) | ||
| 108 | } | ||
| 109 | } | ||
| 110 | } | ||
| 111 | } | ||
| 112 | |||
| 113 | setInsets() | ||
| 114 | } | ||
| 115 | |||
| 116 | override fun onDestroy() { | ||
| 117 | super.onDestroy() | ||
| 118 | gamesViewModel.reloadGames(true) | ||
| 119 | } | ||
| 120 | |||
| 121 | private fun reloadList() { | ||
| 122 | _binding ?: return | ||
| 123 | |||
| 124 | driverViewModel.updateDriverNameForGame(args.game) | ||
| 125 | val properties = mutableListOf<GameProperty>().apply { | ||
| 126 | add( | ||
| 127 | SubmenuProperty( | ||
| 128 | R.string.info, | ||
| 129 | R.string.info_description, | ||
| 130 | R.drawable.ic_info_outline | ||
| 131 | ) { | ||
| 132 | val action = GamePropertiesFragmentDirections | ||
| 133 | .actionPerGamePropertiesFragmentToGameInfoFragment(args.game) | ||
| 134 | binding.root.findNavController().navigate(action) | ||
| 135 | } | ||
| 136 | ) | ||
| 137 | add( | ||
| 138 | SubmenuProperty( | ||
| 139 | R.string.preferences_settings, | ||
| 140 | R.string.per_game_settings_description, | ||
| 141 | R.drawable.ic_settings | ||
| 142 | ) { | ||
| 143 | val action = HomeNavigationDirections.actionGlobalSettingsActivity( | ||
| 144 | args.game, | ||
| 145 | Settings.MenuTag.SECTION_ROOT | ||
| 146 | ) | ||
| 147 | binding.root.findNavController().navigate(action) | ||
| 148 | } | ||
| 149 | ) | ||
| 150 | |||
| 151 | if (!args.game.isHomebrew) { | ||
| 152 | add( | ||
| 153 | SubmenuProperty( | ||
| 154 | R.string.add_ons, | ||
| 155 | R.string.add_ons_description, | ||
| 156 | R.drawable.ic_edit | ||
| 157 | ) { | ||
| 158 | val action = GamePropertiesFragmentDirections | ||
| 159 | .actionPerGamePropertiesFragmentToAddonsFragment(args.game) | ||
| 160 | binding.root.findNavController().navigate(action) | ||
| 161 | } | ||
| 162 | ) | ||
| 163 | add( | ||
| 164 | InstallableProperty( | ||
| 165 | R.string.save_data, | ||
| 166 | R.string.save_data_description, | ||
| 167 | { | ||
| 168 | MessageDialogFragment.newInstance( | ||
| 169 | requireActivity(), | ||
| 170 | titleId = R.string.import_save_warning, | ||
| 171 | descriptionId = R.string.import_save_warning_description, | ||
| 172 | positiveAction = { homeViewModel.setOpenImportSaves(true) } | ||
| 173 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 174 | }, | ||
| 175 | if (File(args.game.saveDir).exists()) { | ||
| 176 | { exportSaves.launch(args.game.saveZipName) } | ||
| 177 | } else { | ||
| 178 | null | ||
| 179 | } | ||
| 180 | ) | ||
| 181 | ) | ||
| 182 | |||
| 183 | val saveDirFile = File(args.game.saveDir) | ||
| 184 | if (saveDirFile.exists()) { | ||
| 185 | add( | ||
| 186 | SubmenuProperty( | ||
| 187 | R.string.delete_save_data, | ||
| 188 | R.string.delete_save_data_description, | ||
| 189 | R.drawable.ic_delete, | ||
| 190 | action = { | ||
| 191 | MessageDialogFragment.newInstance( | ||
| 192 | requireActivity(), | ||
| 193 | titleId = R.string.delete_save_data, | ||
| 194 | descriptionId = R.string.delete_save_data_warning_description, | ||
| 195 | positiveAction = { | ||
| 196 | File(args.game.saveDir).deleteRecursively() | ||
| 197 | Toast.makeText( | ||
| 198 | YuzuApplication.appContext, | ||
| 199 | R.string.save_data_deleted_successfully, | ||
| 200 | Toast.LENGTH_SHORT | ||
| 201 | ).show() | ||
| 202 | reloadList() | ||
| 203 | } | ||
| 204 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 205 | } | ||
| 206 | ) | ||
| 207 | ) | ||
| 208 | } | ||
| 209 | |||
| 210 | val shaderCacheDir = File( | ||
| 211 | DirectoryInitialization.userDirectory + | ||
| 212 | "/shader/" + args.game.settingsName.lowercase() | ||
| 213 | ) | ||
| 214 | if (shaderCacheDir.exists()) { | ||
| 215 | add( | ||
| 216 | SubmenuProperty( | ||
| 217 | R.string.clear_shader_cache, | ||
| 218 | R.string.clear_shader_cache_description, | ||
| 219 | R.drawable.ic_delete, | ||
| 220 | { | ||
| 221 | if (shaderCacheDir.exists()) { | ||
| 222 | val bytes = shaderCacheDir.walkTopDown().filter { it.isFile } | ||
| 223 | .map { it.length() }.sum() | ||
| 224 | MemoryUtil.bytesToSizeUnit(bytes.toFloat()) | ||
| 225 | } else { | ||
| 226 | MemoryUtil.bytesToSizeUnit(0f) | ||
| 227 | } | ||
| 228 | } | ||
| 229 | ) { | ||
| 230 | shaderCacheDir.deleteRecursively() | ||
| 231 | Toast.makeText( | ||
| 232 | YuzuApplication.appContext, | ||
| 233 | R.string.cleared_shaders_successfully, | ||
| 234 | Toast.LENGTH_SHORT | ||
| 235 | ).show() | ||
| 236 | reloadList() | ||
| 237 | } | ||
| 238 | ) | ||
| 239 | } | ||
| 240 | } | ||
| 241 | } | ||
| 242 | binding.listProperties.apply { | ||
| 243 | layoutManager = | ||
| 244 | GridLayoutManager(requireContext(), resources.getInteger(R.integer.grid_columns)) | ||
| 245 | adapter = GamePropertiesAdapter(viewLifecycleOwner, properties) | ||
| 246 | } | ||
| 247 | } | ||
| 248 | |||
| 249 | override fun onResume() { | ||
| 250 | super.onResume() | ||
| 251 | driverViewModel.updateDriverNameForGame(args.game) | ||
| 252 | } | ||
| 253 | |||
| 254 | private fun setInsets() = | ||
| 255 | ViewCompat.setOnApplyWindowInsetsListener( | ||
| 256 | binding.root | ||
| 257 | ) { _: View, windowInsets: WindowInsetsCompat -> | ||
| 258 | val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) | ||
| 259 | val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) | ||
| 260 | |||
| 261 | val leftInsets = barInsets.left + cutoutInsets.left | ||
| 262 | val rightInsets = barInsets.right + cutoutInsets.right | ||
| 263 | |||
| 264 | val smallLayout = resources.getBoolean(R.bool.small_layout) | ||
| 265 | if (smallLayout) { | ||
| 266 | val mlpListAll = | ||
| 267 | binding.listAll.layoutParams as ViewGroup.MarginLayoutParams | ||
| 268 | mlpListAll.leftMargin = leftInsets | ||
| 269 | mlpListAll.rightMargin = rightInsets | ||
| 270 | binding.listAll.layoutParams = mlpListAll | ||
| 271 | } else { | ||
| 272 | if (ViewCompat.getLayoutDirection(binding.root) == | ||
| 273 | ViewCompat.LAYOUT_DIRECTION_LTR | ||
| 274 | ) { | ||
| 275 | val mlpListAll = | ||
| 276 | binding.listAll.layoutParams as ViewGroup.MarginLayoutParams | ||
| 277 | mlpListAll.rightMargin = rightInsets | ||
| 278 | binding.listAll.layoutParams = mlpListAll | ||
| 279 | |||
| 280 | val mlpIconLayout = | ||
| 281 | binding.iconLayout!!.layoutParams as ViewGroup.MarginLayoutParams | ||
| 282 | mlpIconLayout.topMargin = barInsets.top | ||
| 283 | mlpIconLayout.leftMargin = leftInsets | ||
| 284 | binding.iconLayout!!.layoutParams = mlpIconLayout | ||
| 285 | } else { | ||
| 286 | val mlpListAll = | ||
| 287 | binding.listAll.layoutParams as ViewGroup.MarginLayoutParams | ||
| 288 | mlpListAll.leftMargin = leftInsets | ||
| 289 | binding.listAll.layoutParams = mlpListAll | ||
| 290 | |||
| 291 | val mlpIconLayout = | ||
| 292 | binding.iconLayout!!.layoutParams as ViewGroup.MarginLayoutParams | ||
| 293 | mlpIconLayout.topMargin = barInsets.top | ||
| 294 | mlpIconLayout.rightMargin = rightInsets | ||
| 295 | binding.iconLayout!!.layoutParams = mlpIconLayout | ||
| 296 | } | ||
| 297 | } | ||
| 298 | |||
| 299 | val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab) | ||
| 300 | val mlpFab = | ||
| 301 | binding.buttonStart.layoutParams as ViewGroup.MarginLayoutParams | ||
| 302 | mlpFab.leftMargin = leftInsets + fabSpacing | ||
| 303 | mlpFab.rightMargin = rightInsets + fabSpacing | ||
| 304 | mlpFab.bottomMargin = barInsets.bottom + fabSpacing | ||
| 305 | binding.buttonStart.layoutParams = mlpFab | ||
| 306 | |||
| 307 | binding.layoutAll.updatePadding( | ||
| 308 | top = barInsets.top, | ||
| 309 | bottom = barInsets.bottom + | ||
| 310 | resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab) | ||
| 311 | ) | ||
| 312 | |||
| 313 | windowInsets | ||
| 314 | } | ||
| 315 | |||
| 316 | private val importSaves = | ||
| 317 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> | ||
| 318 | if (result == null) { | ||
| 319 | return@registerForActivityResult | ||
| 320 | } | ||
| 321 | |||
| 322 | val inputZip = requireContext().contentResolver.openInputStream(result) | ||
| 323 | val savesFolder = File(args.game.saveDir) | ||
| 324 | val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") | ||
| 325 | cacheSaveDir.mkdir() | ||
| 326 | |||
| 327 | if (inputZip == null) { | ||
| 328 | Toast.makeText( | ||
| 329 | YuzuApplication.appContext, | ||
| 330 | getString(R.string.fatal_error), | ||
| 331 | Toast.LENGTH_LONG | ||
| 332 | ).show() | ||
| 333 | return@registerForActivityResult | ||
| 334 | } | ||
| 335 | |||
| 336 | IndeterminateProgressDialogFragment.newInstance( | ||
| 337 | requireActivity(), | ||
| 338 | R.string.save_files_importing, | ||
| 339 | false | ||
| 340 | ) { | ||
| 341 | try { | ||
| 342 | FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) | ||
| 343 | val files = cacheSaveDir.listFiles() | ||
| 344 | var savesFolderFile: File? = null | ||
| 345 | if (files != null) { | ||
| 346 | val savesFolderName = args.game.programIdHex | ||
| 347 | for (file in files) { | ||
| 348 | if (file.isDirectory && file.name == savesFolderName) { | ||
| 349 | savesFolderFile = file | ||
| 350 | break | ||
| 351 | } | ||
| 352 | } | ||
| 353 | } | ||
| 354 | |||
| 355 | if (savesFolderFile != null) { | ||
| 356 | savesFolder.deleteRecursively() | ||
| 357 | savesFolder.mkdir() | ||
| 358 | savesFolderFile.copyRecursively(savesFolder) | ||
| 359 | savesFolderFile.deleteRecursively() | ||
| 360 | } | ||
| 361 | |||
| 362 | withContext(Dispatchers.Main) { | ||
| 363 | if (savesFolderFile == null) { | ||
| 364 | MessageDialogFragment.newInstance( | ||
| 365 | requireActivity(), | ||
| 366 | titleId = R.string.save_file_invalid_zip_structure, | ||
| 367 | descriptionId = R.string.save_file_invalid_zip_structure_description | ||
| 368 | ).show(parentFragmentManager, MessageDialogFragment.TAG) | ||
| 369 | return@withContext | ||
| 370 | } | ||
| 371 | Toast.makeText( | ||
| 372 | YuzuApplication.appContext, | ||
| 373 | getString(R.string.save_file_imported_success), | ||
| 374 | Toast.LENGTH_LONG | ||
| 375 | ).show() | ||
| 376 | reloadList() | ||
| 377 | } | ||
| 378 | |||
| 379 | cacheSaveDir.deleteRecursively() | ||
| 380 | } catch (e: Exception) { | ||
| 381 | Toast.makeText( | ||
| 382 | YuzuApplication.appContext, | ||
| 383 | getString(R.string.fatal_error), | ||
| 384 | Toast.LENGTH_LONG | ||
| 385 | ).show() | ||
| 386 | } | ||
| 387 | }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) | ||
| 388 | } | ||
| 389 | |||
| 390 | /** | ||
| 391 | * Exports the save file located in the given folder path by creating a zip file and opening a | ||
| 392 | * file picker to save. | ||
| 393 | */ | ||
| 394 | private val exportSaves = registerForActivityResult( | ||
| 395 | ActivityResultContracts.CreateDocument("application/zip") | ||
| 396 | ) { result -> | ||
| 397 | if (result == null) { | ||
| 398 | return@registerForActivityResult | ||
| 399 | } | ||
| 400 | |||
| 401 | IndeterminateProgressDialogFragment.newInstance( | ||
| 402 | requireActivity(), | ||
| 403 | R.string.save_files_exporting, | ||
| 404 | false | ||
| 405 | ) { | ||
| 406 | val saveLocation = args.game.saveDir | ||
| 407 | val zipResult = FileUtil.zipFromInternalStorage( | ||
| 408 | File(saveLocation), | ||
| 409 | saveLocation.replaceAfterLast("/", ""), | ||
| 410 | BufferedOutputStream(requireContext().contentResolver.openOutputStream(result)) | ||
| 411 | ) | ||
| 412 | return@newInstance when (zipResult) { | ||
| 413 | TaskState.Completed -> getString(R.string.export_success) | ||
| 414 | TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) | ||
| 415 | } | ||
| 416 | }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) | ||
| 417 | } | ||
| 418 | } | ||
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/LaunchGameDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt new file mode 100644 index 000000000..f653826a6 --- /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 = 0 | ||
| 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, 0) { _: 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..3ac054d8f 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 | |||
| @@ -60,7 +60,9 @@ class SearchFragment : Fragment() { | |||
| 60 | // This is using the correct scope, lint is just acting up | 60 | // This is using the correct scope, lint is just acting up |
| 61 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") | 61 | @SuppressLint("UnsafeRepeatOnLifecycleDetector") |
| 62 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 62 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 63 | homeViewModel.setNavigationVisibility(visible = true, animated = false) | 63 | super.onViewCreated(view, savedInstanceState) |
| 64 | homeViewModel.setNavigationVisibility(visible = true, animated = true) | ||
| 65 | homeViewModel.setStatusBarShadeVisibility(true) | ||
| 64 | preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) | 66 | preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) |
| 65 | 67 | ||
| 66 | if (savedInstanceState != null) { | 68 | if (savedInstanceState != 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/Game.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt index 2fa3ab31b..ac642c16e 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 |
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..bb3df5bd0 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProperties.kt | |||
| @@ -0,0 +1,34 @@ | |||
| 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 | get() = -1 | ||
| 14 | |||
| 15 | @get:StringRes | ||
| 16 | val descriptionId: Int | ||
| 17 | get() = -1 | ||
| 18 | } | ||
| 19 | |||
| 20 | data class SubmenuProperty( | ||
| 21 | override val titleId: Int, | ||
| 22 | override val descriptionId: Int, | ||
| 23 | @DrawableRes val iconId: Int, | ||
| 24 | val details: (() -> String)? = null, | ||
| 25 | val detailsFlow: StateFlow<String>? = null, | ||
| 26 | val action: () -> Unit | ||
| 27 | ) : GameProperty | ||
| 28 | |||
| 29 | data class InstallableProperty( | ||
| 30 | override val titleId: Int, | ||
| 31 | override val descriptionId: Int, | ||
| 32 | val install: (() -> Unit)? = null, | ||
| 33 | val export: (() -> Unit)? = null | ||
| 34 | ) : GameProperty | ||
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..d801db105 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,12 @@ 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 | |||
| 24 | var navigatedToSetup = false | 31 | var navigatedToSetup = false |
| 25 | 32 | ||
| 26 | fun setNavigationVisibility(visible: Boolean, animated: Boolean) { | 33 | fun setNavigationVisibility(visible: Boolean, animated: Boolean) { |
| @@ -44,4 +51,12 @@ class HomeViewModel : ViewModel() { | |||
| 44 | fun setGamesDirSelected(selected: Boolean) { | 51 | fun setGamesDirSelected(selected: Boolean) { |
| 45 | _gamesDirSelected.value = selected | 52 | _gamesDirSelected.value = selected |
| 46 | } | 53 | } |
| 54 | |||
| 55 | fun setOpenImportSaves(import: Boolean) { | ||
| 56 | _openImportSaves.value = import | ||
| 57 | } | ||
| 58 | |||
| 59 | fun setContentToInstall(documents: List<Uri>?) { | ||
| 60 | _contentToInstall.value = documents | ||
| 61 | } | ||
| 47 | } | 62 | } |
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/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..d5acf8479 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( |
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..09ddd1bbd 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 | |||
| @@ -43,7 +43,7 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings | |||
| 43 | import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment | 43 | import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment |
| 44 | import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment | 44 | import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment |
| 45 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment | 45 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment |
| 46 | import org.yuzu.yuzu_emu.getPublicFilesDir | 46 | import org.yuzu.yuzu_emu.model.AddonViewModel |
| 47 | import org.yuzu.yuzu_emu.model.GamesViewModel | 47 | import org.yuzu.yuzu_emu.model.GamesViewModel |
| 48 | import org.yuzu.yuzu_emu.model.HomeViewModel | 48 | import org.yuzu.yuzu_emu.model.HomeViewModel |
| 49 | import org.yuzu.yuzu_emu.model.TaskState | 49 | import org.yuzu.yuzu_emu.model.TaskState |
| @@ -60,15 +60,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 60 | private val homeViewModel: HomeViewModel by viewModels() | 60 | private val homeViewModel: HomeViewModel by viewModels() |
| 61 | private val gamesViewModel: GamesViewModel by viewModels() | 61 | private val gamesViewModel: GamesViewModel by viewModels() |
| 62 | private val taskViewModel: TaskViewModel by viewModels() | 62 | private val taskViewModel: TaskViewModel by viewModels() |
| 63 | private val addonViewModel: AddonViewModel by viewModels() | ||
| 63 | 64 | ||
| 64 | override var themeId: Int = 0 | 65 | override var themeId: Int = 0 |
| 65 | 66 | ||
| 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?) { | 67 | override fun onCreate(savedInstanceState: Bundle?) { |
| 73 | val splashScreen = installSplashScreen() | 68 | val splashScreen = installSplashScreen() |
| 74 | splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } | 69 | splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } |
| @@ -145,6 +140,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 145 | homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) } | 140 | homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) } |
| 146 | } | 141 | } |
| 147 | } | 142 | } |
| 143 | launch { | ||
| 144 | repeatOnLifecycle(Lifecycle.State.CREATED) { | ||
| 145 | homeViewModel.contentToInstall.collect { | ||
| 146 | if (it != null) { | ||
| 147 | installContent(it) | ||
| 148 | homeViewModel.setContentToInstall(null) | ||
| 149 | } | ||
| 150 | } | ||
| 151 | } | ||
| 152 | } | ||
| 148 | } | 153 | } |
| 149 | 154 | ||
| 150 | // Dismiss previous notifications (should not happen unless a crash occurred) | 155 | // Dismiss previous notifications (should not happen unless a crash occurred) |
| @@ -468,110 +473,150 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 468 | val installGameUpdate = registerForActivityResult( | 473 | val installGameUpdate = registerForActivityResult( |
| 469 | ActivityResultContracts.OpenMultipleDocuments() | 474 | ActivityResultContracts.OpenMultipleDocuments() |
| 470 | ) { documents: List<Uri> -> | 475 | ) { documents: List<Uri> -> |
| 471 | if (documents.isNotEmpty()) { | 476 | if (documents.isEmpty()) { |
| 472 | IndeterminateProgressDialogFragment.newInstance( | 477 | return@registerForActivityResult |
| 473 | this@MainActivity, | 478 | } |
| 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 | 479 | ||
| 492 | NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> { | 480 | if (addonViewModel.game == null) { |
| 493 | installOverwrite += 1 | 481 | installContent(documents) |
| 494 | } | 482 | return@registerForActivityResult |
| 483 | } | ||
| 495 | 484 | ||
| 496 | NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> { | 485 | IndeterminateProgressDialogFragment.newInstance( |
| 497 | errorBaseGame += 1 | 486 | this@MainActivity, |
| 498 | } | 487 | R.string.verifying_content, |
| 488 | false | ||
| 489 | ) { | ||
| 490 | var updatesMatchProgram = true | ||
| 491 | for (document in documents) { | ||
| 492 | val valid = NativeLibrary.doesUpdateMatchProgram( | ||
| 493 | addonViewModel.game!!.programId, | ||
| 494 | document.toString() | ||
| 495 | ) | ||
| 496 | if (!valid) { | ||
| 497 | updatesMatchProgram = false | ||
| 498 | break | ||
| 499 | } | ||
| 500 | } | ||
| 499 | 501 | ||
| 500 | NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> { | 502 | if (updatesMatchProgram) { |
| 501 | errorExtension += 1 | 503 | homeViewModel.setContentToInstall(documents) |
| 502 | } | 504 | } else { |
| 505 | MessageDialogFragment.newInstance( | ||
| 506 | this@MainActivity, | ||
| 507 | titleId = R.string.content_install_notice, | ||
| 508 | descriptionId = R.string.content_install_notice_description, | ||
| 509 | positiveAction = { homeViewModel.setContentToInstall(documents) } | ||
| 510 | ) | ||
| 511 | } | ||
| 512 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) | ||
| 513 | } | ||
| 503 | 514 | ||
| 504 | else -> { | 515 | private fun installContent(documents: List<Uri>) { |
| 505 | errorOther += 1 | 516 | IndeterminateProgressDialogFragment.newInstance( |
| 506 | } | 517 | this@MainActivity, |
| 518 | R.string.installing_game_content | ||
| 519 | ) { | ||
| 520 | var installSuccess = 0 | ||
| 521 | var installOverwrite = 0 | ||
| 522 | var errorBaseGame = 0 | ||
| 523 | var errorExtension = 0 | ||
| 524 | var errorOther = 0 | ||
| 525 | documents.forEach { | ||
| 526 | when ( | ||
| 527 | NativeLibrary.installFileToNand( | ||
| 528 | it.toString(), | ||
| 529 | FileUtil.getExtension(it) | ||
| 530 | ) | ||
| 531 | ) { | ||
| 532 | NativeLibrary.InstallFileToNandResult.Success -> { | ||
| 533 | installSuccess += 1 | ||
| 534 | } | ||
| 535 | |||
| 536 | NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> { | ||
| 537 | installOverwrite += 1 | ||
| 538 | } | ||
| 539 | |||
| 540 | NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> { | ||
| 541 | errorBaseGame += 1 | ||
| 542 | } | ||
| 543 | |||
| 544 | NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> { | ||
| 545 | errorExtension += 1 | ||
| 546 | } | ||
| 547 | |||
| 548 | else -> { | ||
| 549 | errorOther += 1 | ||
| 507 | } | 550 | } |
| 508 | } | 551 | } |
| 552 | } | ||
| 509 | 553 | ||
| 510 | val separator = System.getProperty("line.separator") ?: "\n" | 554 | addonViewModel.refreshAddons() |
| 511 | val installResult = StringBuilder() | 555 | |
| 512 | if (installSuccess > 0) { | 556 | val separator = System.getProperty("line.separator") ?: "\n" |
| 513 | installResult.append( | 557 | val installResult = StringBuilder() |
| 514 | getString( | 558 | if (installSuccess > 0) { |
| 515 | R.string.install_game_content_success_install, | 559 | installResult.append( |
| 516 | installSuccess | 560 | getString( |
| 517 | ) | 561 | R.string.install_game_content_success_install, |
| 562 | installSuccess | ||
| 563 | ) | ||
| 564 | ) | ||
| 565 | installResult.append(separator) | ||
| 566 | } | ||
| 567 | if (installOverwrite > 0) { | ||
| 568 | installResult.append( | ||
| 569 | getString( | ||
| 570 | R.string.install_game_content_success_overwrite, | ||
| 571 | installOverwrite | ||
| 518 | ) | 572 | ) |
| 573 | ) | ||
| 574 | installResult.append(separator) | ||
| 575 | } | ||
| 576 | val errorTotal: Int = errorBaseGame + errorExtension + errorOther | ||
| 577 | if (errorTotal > 0) { | ||
| 578 | installResult.append(separator) | ||
| 579 | installResult.append( | ||
| 580 | getString( | ||
| 581 | R.string.install_game_content_failed_count, | ||
| 582 | errorTotal | ||
| 583 | ) | ||
| 584 | ) | ||
| 585 | installResult.append(separator) | ||
| 586 | if (errorBaseGame > 0) { | ||
| 519 | installResult.append(separator) | 587 | installResult.append(separator) |
| 520 | } | ||
| 521 | if (installOverwrite > 0) { | ||
| 522 | installResult.append( | 588 | installResult.append( |
| 523 | getString( | 589 | getString(R.string.install_game_content_failure_base) |
| 524 | R.string.install_game_content_success_overwrite, | ||
| 525 | installOverwrite | ||
| 526 | ) | ||
| 527 | ) | 590 | ) |
| 528 | installResult.append(separator) | 591 | installResult.append(separator) |
| 529 | } | 592 | } |
| 530 | val errorTotal: Int = errorBaseGame + errorExtension + errorOther | 593 | if (errorExtension > 0) { |
| 531 | if (errorTotal > 0) { | ||
| 532 | installResult.append(separator) | 594 | installResult.append(separator) |
| 533 | installResult.append( | 595 | installResult.append( |
| 534 | getString( | 596 | getString(R.string.install_game_content_failure_file_extension) |
| 535 | R.string.install_game_content_failed_count, | ||
| 536 | errorTotal | ||
| 537 | ) | ||
| 538 | ) | 597 | ) |
| 539 | installResult.append(separator) | 598 | installResult.append(separator) |
| 540 | if (errorBaseGame > 0) { | 599 | } |
| 541 | installResult.append(separator) | 600 | if (errorOther > 0) { |
| 542 | installResult.append( | 601 | installResult.append( |
| 543 | getString(R.string.install_game_content_failure_base) | 602 | 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 | ) | 603 | ) |
| 604 | installResult.append(separator) | ||
| 572 | } | 605 | } |
| 573 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) | 606 | return@newInstance MessageDialogFragment.newInstance( |
| 574 | } | 607 | this, |
| 608 | titleId = R.string.install_game_content_failure, | ||
| 609 | descriptionString = installResult.toString().trim(), | ||
| 610 | helpLinkId = R.string.install_game_content_help_link | ||
| 611 | ) | ||
| 612 | } else { | ||
| 613 | return@newInstance MessageDialogFragment.newInstance( | ||
| 614 | this, | ||
| 615 | titleId = R.string.install_game_content_success, | ||
| 616 | descriptionString = installResult.toString().trim() | ||
| 617 | ) | ||
| 618 | } | ||
| 619 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) | ||
| 575 | } | 620 | } |
| 576 | 621 | ||
| 577 | val exportUserData = registerForActivityResult( | 622 | val exportUserData = registerForActivityResult( |
| @@ -657,102 +702,4 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 657 | return@newInstance getString(R.string.user_data_import_success) | 702 | return@newInstance getString(R.string.user_data_import_success) |
| 658 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) | 703 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) |
| 659 | } | 704 | } |
| 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 | } | 705 | } |
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/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/NativeConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt index 4c7316ba3..7d629b7d5 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 | |||
| @@ -105,4 +105,23 @@ object NativeConfig { | |||
| 105 | */ | 105 | */ |
| 106 | @Synchronized | 106 | @Synchronized |
| 107 | external fun addGameDir(dir: GameDir) | 107 | external fun addGameDir(dir: GameDir) |
| 108 | |||
| 109 | /** | ||
| 110 | * Gets an array of the addons that are disabled for a given game | ||
| 111 | * | ||
| 112 | * @param programId String representation of a game's program ID | ||
| 113 | * @return An array of disabled addons | ||
| 114 | */ | ||
| 115 | @Synchronized | ||
| 116 | external fun getDisabledAddons(programId: String): Array<String> | ||
| 117 | |||
| 118 | /** | ||
| 119 | * Clears the disabled addons array corresponding to [programId] and replaces them | ||
| 120 | * with [disabledAddons] | ||
| 121 | * | ||
| 122 | * @param programId String representation of a game's program ID | ||
| 123 | * @param disabledAddons Replacement array of disabled addons | ||
| 124 | */ | ||
| 125 | @Synchronized | ||
| 126 | external fun setDisabledAddons(programId: String, disabledAddons: Array<String>) | ||
| 108 | } | 127 | } |
diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp index a56ed5662..df8935178 100644 --- a/src/android/app/src/main/jni/id_cache.cpp +++ b/src/android/app/src/main/jni/id_cache.cpp | |||
| @@ -20,6 +20,12 @@ 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_string_class; | ||
| 24 | static jclass s_pair_class; | ||
| 25 | static jmethodID s_pair_constructor; | ||
| 26 | static jfieldID s_pair_first_field; | ||
| 27 | static jfieldID s_pair_second_field; | ||
| 28 | |||
| 23 | static constexpr jint JNI_VERSION = JNI_VERSION_1_6; | 29 | static constexpr jint JNI_VERSION = JNI_VERSION_1_6; |
| 24 | 30 | ||
| 25 | namespace IDCache { | 31 | namespace IDCache { |
| @@ -79,6 +85,26 @@ jmethodID GetOnEmulationStopped() { | |||
| 79 | return s_on_emulation_stopped; | 85 | return s_on_emulation_stopped; |
| 80 | } | 86 | } |
| 81 | 87 | ||
| 88 | jclass GetStringClass() { | ||
| 89 | return s_string_class; | ||
| 90 | } | ||
| 91 | |||
| 92 | jclass GetPairClass() { | ||
| 93 | return s_pair_class; | ||
| 94 | } | ||
| 95 | |||
| 96 | jmethodID GetPairConstructor() { | ||
| 97 | return s_pair_constructor; | ||
| 98 | } | ||
| 99 | |||
| 100 | jfieldID GetPairFirstField() { | ||
| 101 | return s_pair_first_field; | ||
| 102 | } | ||
| 103 | |||
| 104 | jfieldID GetPairSecondField() { | ||
| 105 | return s_pair_second_field; | ||
| 106 | } | ||
| 107 | |||
| 82 | } // namespace IDCache | 108 | } // namespace IDCache |
| 83 | 109 | ||
| 84 | #ifdef __cplusplus | 110 | #ifdef __cplusplus |
| @@ -115,6 +141,18 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { | |||
| 115 | s_on_emulation_stopped = | 141 | s_on_emulation_stopped = |
| 116 | env->GetStaticMethodID(s_native_library_class, "onEmulationStopped", "(I)V"); | 142 | env->GetStaticMethodID(s_native_library_class, "onEmulationStopped", "(I)V"); |
| 117 | 143 | ||
| 144 | const jclass string_class = env->FindClass("java/lang/String"); | ||
| 145 | s_string_class = reinterpret_cast<jclass>(env->NewGlobalRef(string_class)); | ||
| 146 | env->DeleteLocalRef(string_class); | ||
| 147 | |||
| 148 | const jclass pair_class = env->FindClass("kotlin/Pair"); | ||
| 149 | s_pair_class = reinterpret_cast<jclass>(env->NewGlobalRef(pair_class)); | ||
| 150 | s_pair_constructor = | ||
| 151 | env->GetMethodID(pair_class, "<init>", "(Ljava/lang/Object;Ljava/lang/Object;)V"); | ||
| 152 | s_pair_first_field = env->GetFieldID(pair_class, "first", "Ljava/lang/Object;"); | ||
| 153 | s_pair_second_field = env->GetFieldID(pair_class, "second", "Ljava/lang/Object;"); | ||
| 154 | env->DeleteLocalRef(pair_class); | ||
| 155 | |||
| 118 | // Initialize Android Storage | 156 | // Initialize Android Storage |
| 119 | Common::FS::Android::RegisterCallbacks(env, s_native_library_class); | 157 | Common::FS::Android::RegisterCallbacks(env, s_native_library_class); |
| 120 | 158 | ||
| @@ -136,6 +174,8 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { | |||
| 136 | env->DeleteGlobalRef(s_disk_cache_progress_class); | 174 | env->DeleteGlobalRef(s_disk_cache_progress_class); |
| 137 | env->DeleteGlobalRef(s_load_callback_stage_class); | 175 | env->DeleteGlobalRef(s_load_callback_stage_class); |
| 138 | env->DeleteGlobalRef(s_game_dir_class); | 176 | env->DeleteGlobalRef(s_game_dir_class); |
| 177 | env->DeleteGlobalRef(s_string_class); | ||
| 178 | env->DeleteGlobalRef(s_pair_class); | ||
| 139 | 179 | ||
| 140 | // UnInitialize applets | 180 | // UnInitialize applets |
| 141 | SoftwareKeyboard::CleanupJNI(env); | 181 | 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..36233423e 100644 --- a/src/android/app/src/main/jni/id_cache.h +++ b/src/android/app/src/main/jni/id_cache.h | |||
| @@ -20,4 +20,10 @@ jmethodID GetDiskCacheLoadProgress(); | |||
| 20 | jmethodID GetOnEmulationStarted(); | 20 | jmethodID GetOnEmulationStarted(); |
| 21 | jmethodID GetOnEmulationStopped(); | 21 | jmethodID GetOnEmulationStopped(); |
| 22 | 22 | ||
| 23 | jclass GetStringClass(); | ||
| 24 | jclass GetPairClass(); | ||
| 25 | jmethodID GetPairConstructor(); | ||
| 26 | jfieldID GetPairFirstField(); | ||
| 27 | jfieldID GetPairSecondField(); | ||
| 28 | |||
| 23 | } // namespace IDCache | 29 | } // namespace IDCache |
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index e5d3158c8..ce570b811 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::ContentProvider() { | ||
| 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,60 @@ 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 | |||
| 795 | } // extern "C" | 883 | } // extern "C" |
diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h index f1457bd1f..96c22d52b 100644 --- a/src/android/app/src/main/jni/native.h +++ b/src/android/app/src/main/jni/native.h | |||
| @@ -54,6 +54,8 @@ public: | |||
| 54 | 54 | ||
| 55 | static void OnEmulationStarted(); | 55 | static void OnEmulationStarted(); |
| 56 | 56 | ||
| 57 | static u64 GetProgramId(JNIEnv* env, jstring jprogramId); | ||
| 58 | |||
| 57 | private: | 59 | private: |
| 58 | static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max); | 60 | static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max); |
| 59 | static void OnEmulationStopped(Core::SystemResultStatus result); | 61 | 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 9439d11e1..7f2485720 100644 --- a/src/android/app/src/main/jni/native_config.cpp +++ b/src/android/app/src/main/jni/native_config.cpp | |||
| @@ -283,4 +283,30 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_addGameDir(JNIEnv* env, jobject | |||
| 283 | AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)}); | 283 | AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)}); |
| 284 | } | 284 | } |
| 285 | 285 | ||
| 286 | jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getDisabledAddons(JNIEnv* env, jobject obj, | ||
| 287 | jstring jprogramId) { | ||
| 288 | auto program_id = EmulationSession::GetProgramId(env, jprogramId); | ||
| 289 | auto& disabledAddons = Settings::values.disabled_addons[program_id]; | ||
| 290 | jobjectArray jdisabledAddonsArray = | ||
| 291 | env->NewObjectArray(disabledAddons.size(), IDCache::GetStringClass(), ToJString(env, "")); | ||
| 292 | for (size_t i = 0; i < disabledAddons.size(); ++i) { | ||
| 293 | env->SetObjectArrayElement(jdisabledAddonsArray, i, ToJString(env, disabledAddons[i])); | ||
| 294 | } | ||
| 295 | return jdisabledAddonsArray; | ||
| 296 | } | ||
| 297 | |||
| 298 | void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setDisabledAddons(JNIEnv* env, jobject obj, | ||
| 299 | jstring jprogramId, | ||
| 300 | jobjectArray jdisabledAddons) { | ||
| 301 | auto program_id = EmulationSession::GetProgramId(env, jprogramId); | ||
| 302 | Settings::values.disabled_addons[program_id].clear(); | ||
| 303 | std::vector<std::string> disabled_addons; | ||
| 304 | const int size = env->GetArrayLength(jdisabledAddons); | ||
| 305 | for (int i = 0; i < size; ++i) { | ||
| 306 | auto jaddon = static_cast<jstring>(env->GetObjectArrayElement(jdisabledAddons, i)); | ||
| 307 | disabled_addons.push_back(GetJString(env, jaddon)); | ||
| 308 | } | ||
| 309 | Settings::values.disabled_addons[program_id] = disabled_addons; | ||
| 310 | } | ||
| 311 | |||
| 286 | } // extern "C" | 312 | } // extern "C" |
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_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/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml index cf70b4bc4..1c69bf0db 100644 --- a/src/android/app/src/main/res/navigation/home_navigation.xml +++ b/src/android/app/src/main/res/navigation/home_navigation.xml | |||
| @@ -124,5 +124,38 @@ | |||
| 124 | android:id="@+id/gameFoldersFragment" | 124 | android:id="@+id/gameFoldersFragment" |
| 125 | android:name="org.yuzu.yuzu_emu.fragments.GameFoldersFragment" | 125 | android:name="org.yuzu.yuzu_emu.fragments.GameFoldersFragment" |
| 126 | android:label="GameFoldersFragment" /> | 126 | android:label="GameFoldersFragment" /> |
| 127 | <fragment | ||
| 128 | android:id="@+id/perGamePropertiesFragment" | ||
| 129 | android:name="org.yuzu.yuzu_emu.fragments.GamePropertiesFragment" | ||
| 130 | android:label="PerGamePropertiesFragment" > | ||
| 131 | <argument | ||
| 132 | android:name="game" | ||
| 133 | app:argType="org.yuzu.yuzu_emu.model.Game" /> | ||
| 134 | <action | ||
| 135 | android:id="@+id/action_perGamePropertiesFragment_to_gameInfoFragment" | ||
| 136 | app:destination="@id/gameInfoFragment" /> | ||
| 137 | <action | ||
| 138 | android:id="@+id/action_perGamePropertiesFragment_to_addonsFragment" | ||
| 139 | app:destination="@id/addonsFragment" /> | ||
| 140 | </fragment> | ||
| 141 | <action | ||
| 142 | android:id="@+id/action_global_perGamePropertiesFragment" | ||
| 143 | app:destination="@id/perGamePropertiesFragment" /> | ||
| 144 | <fragment | ||
| 145 | android:id="@+id/gameInfoFragment" | ||
| 146 | android:name="org.yuzu.yuzu_emu.fragments.GameInfoFragment" | ||
| 147 | android:label="GameInfoFragment" > | ||
| 148 | <argument | ||
| 149 | android:name="game" | ||
| 150 | app:argType="org.yuzu.yuzu_emu.model.Game" /> | ||
| 151 | </fragment> | ||
| 152 | <fragment | ||
| 153 | android:id="@+id/addonsFragment" | ||
| 154 | android:name="org.yuzu.yuzu_emu.fragments.AddonsFragment" | ||
| 155 | android:label="AddonsFragment" > | ||
| 156 | <argument | ||
| 157 | android:name="game" | ||
| 158 | app:argType="org.yuzu.yuzu_emu.model.Game" /> | ||
| 159 | </fragment> | ||
| 127 | 160 | ||
| 128 | </navigation> | 161 | </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..cd5571aa9 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,43 @@ | |||
| 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="cleared_shaders_successfully">Cleared shaders successfully</string> | ||
| 318 | <string name="addons_game">Addons: %1$s</string> | ||
| 319 | <string name="save_data">Save data</string> | ||
| 320 | <string name="save_data_description">Manage save data specific to this game</string> | ||
| 321 | <string name="delete_save_data">Delete save data</string> | ||
| 322 | <string name="delete_save_data_description">Removes all save data specific to this game</string> | ||
| 323 | <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> | ||
| 324 | <string name="save_data_deleted_successfully">Save data deleted successfully</string> | ||
| 325 | <string name="select_content_type">Content type</string> | ||
| 326 | <string name="updates_and_dlc">Updates and DLC</string> | ||
| 327 | <string name="mods_and_cheats">Mods and cheats</string> | ||
| 328 | <string name="addon_notice">Important addon notice</string> | ||
| 329 | <!-- "cheats/" "romfs/" and "exefs/ should not be translated --> | ||
| 330 | <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> | ||
| 331 | <string name="invalid_directory">Invalid directory</string> | ||
| 332 | <!-- "cheats/" "romfs/" and "exefs/ should not be translated --> | ||
| 333 | <string name="invalid_directory_description">Please make sure that the directory you selected contains a cheats/, romfs/, or exefs/ folder and try again.</string> | ||
| 334 | <string name="addon_installed_successfully">Addon installed successfully</string> | ||
| 335 | <string name="verifying_content">Verifying content…</string> | ||
| 336 | <string name="content_install_notice">Content install notice</string> | ||
| 337 | <string name="content_install_notice_description">The content that you selected does not match this game.\nInstall anyway?</string> | ||
| 338 | |||
| 294 | <!-- ROM loading errors --> | 339 | <!-- ROM loading errors --> |
| 295 | <string name="loader_error_encrypted">Your ROM is encrypted</string> | 340 | <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> | 341 | <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> |