aboutsummaryrefslogtreecommitdiff
path: root/src/android
diff options
context:
space:
mode:
authorkleidis <kleidis1@protonmail.com>2025-12-04 07:33:04 +0100
committercrueter <crueter@eden-emu.dev>2025-12-04 07:33:04 +0100
commitf882ff72eb4ca20a7c680f5c87a52ea7d349770e (patch)
tree366782b58863a6824358a5df6ae8f03c01c4d028 /src/android
parent55cc4d5edef956c6542611958ebec1b36ef077f9 (diff)
android: Rework setup fragment to use multiple buttons per-page (#2854)
Adapted from https://github.com/azahar-emu/azahar/commit/f771952e62a3c7c175a99c91635509b24a8300f6#diff-e59f69380a076aef2745f7ab65072ca25fc26c598e2ed177475a15fe44121b4d Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2854 Reviewed-by: Caio Oliveira <caiooliveirafarias0@gmail.com> Reviewed-by: MaranBr <maranbr@eden-emu.dev> Co-authored-by: kleidis <kleidis1@protonmail.com> Co-committed-by: kleidis <kleidis1@protonmail.com>
Diffstat (limited to 'src/android')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt88
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt9
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt398
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupWarningDialogFragment.kt62
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt7
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt32
-rw-r--r--src/android/app/src/main/res/drawable/ic_permission.xml9
-rw-r--r--src/android/app/src/main/res/layout-w600dp/fragment_setup.xml2
-rw-r--r--src/android/app/src/main/res/layout-w600dp/page_setup.xml108
-rw-r--r--src/android/app/src/main/res/layout/fragment_setup.xml2
-rw-r--r--src/android/app/src/main/res/layout/page_button.xml8
-rw-r--r--src/android/app/src/main/res/layout/page_setup.xml29
-rw-r--r--src/android/app/src/main/res/values/strings.xml4
13 files changed, 466 insertions, 292 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt
index a5f610b317..3057eea3e4 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -8,16 +11,16 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat
-import androidx.lifecycle.ViewModelProvider
import com.google.android.material.button.MaterialButton
import org.yuzu.yuzu_emu.databinding.PageSetupBinding
-import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.model.PageState
import org.yuzu.yuzu_emu.model.SetupCallback
import org.yuzu.yuzu_emu.model.SetupPage
-import org.yuzu.yuzu_emu.model.StepState
import org.yuzu.yuzu_emu.utils.ViewUtils
-import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
+import android.content.res.ColorStateList
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.model.ButtonState
class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
AbstractListAdapter<SetupPage, SetupAdapter.SetupPageViewHolder>(pages) {
@@ -29,9 +32,40 @@ class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
inner class SetupPageViewHolder(val binding: PageSetupBinding) :
AbstractViewHolder<SetupPage>(binding), SetupCallback {
override fun bind(model: SetupPage) {
- if (model.stepCompleted.invoke() == StepState.COMPLETE) {
- binding.buttonAction.setVisible(visible = false, gone = false)
- binding.textConfirmation.setVisible(true)
+ if (model.pageSteps.invoke() == PageState.COMPLETE) {
+ onStepCompleted(0, pageFullyCompleted = true)
+ }
+
+ if (model.pageButtons != null && model.pageSteps.invoke() != PageState.COMPLETE) {
+ for (pageButton in model.pageButtons) {
+ val pageButtonView = LayoutInflater.from(activity)
+ .inflate(
+ R.layout.page_button,
+ binding.pageButtonContainer,
+ false
+ ) as MaterialButton
+
+ pageButtonView.apply {
+ id = pageButton.titleId
+ icon = ResourcesCompat.getDrawable(
+ activity.resources,
+ pageButton.iconId,
+ activity.theme
+ )
+ text = activity.resources.getString(pageButton.titleId)
+ }
+
+ pageButtonView.setOnClickListener {
+ pageButton.buttonAction.invoke(this@SetupPageViewHolder)
+ }
+
+ binding.pageButtonContainer.addView(pageButtonView)
+
+ // Disable buton add if its already completed
+ if (pageButton.buttonState.invoke() == ButtonState.BUTTON_ACTION_COMPLETE) {
+ onStepCompleted(pageButton.titleId, pageFullyCompleted = false)
+ }
+ }
}
binding.icon.setImageDrawable(
@@ -44,32 +78,26 @@ class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
binding.textTitle.text = activity.resources.getString(model.titleId)
binding.textDescription.text =
Html.fromHtml(activity.resources.getString(model.descriptionId), 0)
+ }
- binding.buttonAction.apply {
- text = activity.resources.getString(model.buttonTextId)
- if (model.buttonIconId != 0) {
- icon = ResourcesCompat.getDrawable(
- activity.resources,
- model.buttonIconId,
- activity.theme
- )
- }
- iconGravity =
- if (model.leftAlignedIcon) {
- MaterialButton.ICON_GRAVITY_START
- } else {
- MaterialButton.ICON_GRAVITY_END
- }
- setOnClickListener {
- model.buttonAction.invoke(this@SetupPageViewHolder)
- }
+ override fun onStepCompleted(pageButtonId: Int, pageFullyCompleted: Boolean) {
+ val button = binding.pageButtonContainer.findViewById<MaterialButton>(pageButtonId)
+
+ if (pageFullyCompleted) {
+ ViewUtils.hideView(binding.pageButtonContainer, 200)
+ ViewUtils.showView(binding.textConfirmation, 200)
}
- }
- override fun onStepCompleted() {
- ViewUtils.hideView(binding.buttonAction, 200)
- ViewUtils.showView(binding.textConfirmation, 200)
- ViewModelProvider(activity)[HomeViewModel::class.java].setShouldPageForward(true)
+ if (button != null) {
+ button.isEnabled = false
+ button.animate()
+ .alpha(0.38f)
+ .setDuration(200)
+ .start()
+ button.setTextColor(button.context.getColor(com.google.android.material.R.color.material_on_surface_disabled))
+ button.iconTint =
+ ColorStateList.valueOf(button.context.getColor(com.google.android.material.R.color.material_on_surface_disabled))
+ }
}
}
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt
index e2cb5f600d..ca66ae3894 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
@@ -32,12 +35,14 @@ class AddGameFolderDialogFragment : DialogFragment() {
.setTitle(R.string.add_game_folder)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked)
- homeViewModel.setGamesDirSelected(true)
val calledFromGameFragment = requireArguments().getBoolean(
"calledFromGameFragment",
false
)
- gamesViewModel.addFolder(newGameDir, calledFromGameFragment)
+ val job = gamesViewModel.addFolder(newGameDir, calledFromGameFragment)
+ job.invokeOnCompletion {
+ homeViewModel.setGamesDirSelected(true)
+ }
}
.setNegativeButton(android.R.string.cancel, null)
.setView(binding.root)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
index eff96248e0..41f2da08a7 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
@@ -26,7 +26,6 @@ import androidx.navigation.findNavController
import androidx.preference.PreferenceManager
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.google.android.material.transition.MaterialFadeThrough
-import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.NativeLibrary
import java.io.File
import org.yuzu.yuzu_emu.R
@@ -34,10 +33,13 @@ import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.adapters.SetupAdapter
import org.yuzu.yuzu_emu.databinding.FragmentSetupBinding
import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.model.ButtonState
+import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.model.PageButton
import org.yuzu.yuzu_emu.model.SetupCallback
import org.yuzu.yuzu_emu.model.SetupPage
-import org.yuzu.yuzu_emu.model.StepState
+import org.yuzu.yuzu_emu.model.PageState
import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.NativeConfig
@@ -50,11 +52,16 @@ class SetupFragment : Fragment() {
private val binding get() = _binding!!
private val homeViewModel: HomeViewModel by activityViewModels()
+ private val gamesViewModel: GamesViewModel by activityViewModels()
private lateinit var mainActivity: MainActivity
private lateinit var hasBeenWarned: BooleanArray
+ private lateinit var pages: MutableList<SetupPage>
+
+ private lateinit var pageButtonCallback: SetupCallback
+
companion object {
const val KEY_NEXT_VISIBILITY = "NextButtonVisibility"
const val KEY_BACK_VISIBILITY = "BackButtonVisibility"
@@ -94,124 +101,142 @@ class SetupFragment : Fragment() {
requireActivity().window.navigationBarColor =
ContextCompat.getColor(requireContext(), android.R.color.transparent)
- val pages = mutableListOf<SetupPage>()
+ pages = mutableListOf<SetupPage>()
pages.apply {
add(
SetupPage(
- R.drawable.ic_yuzu_title,
- R.string.welcome,
- R.string.welcome_description,
- 0,
- true,
- R.string.get_started,
- { pageForward() },
- false
- )
- )
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- add(
- SetupPage(
- R.drawable.ic_notification,
- R.string.notifications,
- R.string.notifications_description,
- 0,
- false,
- R.string.give_permission,
- {
- notificationCallback = it
- permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
- },
- true,
- R.string.notification_warning,
- R.string.notification_warning_description,
- 0,
- {
- if (NotificationManagerCompat.from(requireContext())
- .areNotificationsEnabled()
- ) {
- StepState.COMPLETE
- } else {
- StepState.INCOMPLETE
- }
- }
- )
- )
- }
-
- add(
- SetupPage(
- R.drawable.ic_key,
- R.string.keys,
- R.string.keys_description,
- R.drawable.ic_add,
- true,
- R.string.select_keys,
- {
- keyCallback = it
- getProdKey.launch(arrayOf("*/*"))
- },
- true,
- R.string.install_prod_keys_warning,
- R.string.install_prod_keys_warning_description,
- R.string.install_prod_keys_warning_help,
- {
- val file = File(DirectoryInitialization.userDirectory + "/keys/prod.keys")
- if (file.exists() && NativeLibrary.areKeysPresent()) {
- StepState.COMPLETE
- } else {
- StepState.INCOMPLETE
+ R.drawable.ic_permission,
+ R.string.permissions,
+ R.string.permissions_description,
+ mutableListOf<PageButton>().apply {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ add(
+ PageButton(
+ R.drawable.ic_notification,
+ R.string.notifications,
+ R.string.notifications_description,
+ {
+ pageButtonCallback = it
+ permissionLauncher.launch(
+ Manifest.permission.POST_NOTIFICATIONS
+ )
+ },
+ {
+ if (NotificationManagerCompat.from(requireContext())
+ .areNotificationsEnabled()
+ ) {
+ ButtonState.BUTTON_ACTION_COMPLETE
+ } else {
+ ButtonState.BUTTON_ACTION_INCOMPLETE
+ }
+ },
+ false,
+ false,
+ )
+ )
}
- }
- )
- )
- add(
- SetupPage(
- R.drawable.ic_firmware,
- R.string.firmware,
- R.string.firmware_description,
- R.drawable.ic_add,
- true,
- R.string.select_firmware,
- {
- firmwareCallback = it
- getFirmware.launch(arrayOf("application/zip"))
},
- true,
- R.string.install_firmware_warning,
- R.string.install_firmware_warning_description,
- R.string.install_firmware_warning_help,
{
- if (NativeLibrary.isFirmwareAvailable()) {
- StepState.COMPLETE
+ if (NotificationManagerCompat.from(requireContext())
+ .areNotificationsEnabled()
+ ) {
+ PageState.COMPLETE
} else {
- StepState.INCOMPLETE
+ PageState.INCOMPLETE
}
}
)
)
-
add(
SetupPage(
- R.drawable.ic_controller,
- R.string.games,
- R.string.games_description,
- R.drawable.ic_add,
- true,
- R.string.add_games,
- {
- gamesDirCallback = it
- getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
+ R.drawable.ic_folder_open,
+ R.string.emulator_data,
+ R.string.emulator_data_description,
+ mutableListOf<PageButton>().apply {
+ add(
+ PageButton(
+ R.drawable.ic_key,
+ R.string.keys,
+ R.string.keys_description,
+ {
+ pageButtonCallback = it
+ getProdKey.launch(arrayOf("*/*"))
+ },
+ {
+ val file = File(
+ DirectoryInitialization.userDirectory + "/keys/prod.keys"
+ )
+ if (file.exists() && NativeLibrary.areKeysPresent()) {
+ ButtonState.BUTTON_ACTION_COMPLETE
+ } else {
+ ButtonState.BUTTON_ACTION_INCOMPLETE
+ }
+ },
+ false,
+ true,
+ R.string.install_prod_keys_warning,
+ R.string.install_prod_keys_warning_description,
+ R.string.install_prod_keys_warning_help,
+ )
+ )
+ add(
+ PageButton(
+ R.drawable.ic_firmware,
+ R.string.firmware,
+ R.string.firmware_description,
+ {
+ pageButtonCallback = it
+ getFirmware.launch(arrayOf("application/zip"))
+ },
+ {
+ if (NativeLibrary.isFirmwareAvailable()) {
+ ButtonState.BUTTON_ACTION_COMPLETE
+ } else {
+ ButtonState.BUTTON_ACTION_INCOMPLETE
+ }
+ },
+ false,
+ true,
+ R.string.install_firmware_warning,
+ R.string.install_firmware_warning_description,
+ R.string.install_firmware_warning_help,
+ )
+ )
+ add(
+ PageButton(
+ R.drawable.ic_controller,
+ R.string.games,
+ R.string.games_description,
+ {
+ pageButtonCallback = it
+ getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
+ },
+ {
+ if (NativeConfig.getGameDirs().isNotEmpty()) {
+ ButtonState.BUTTON_ACTION_COMPLETE
+ } else {
+ ButtonState.BUTTON_ACTION_INCOMPLETE
+ }
+ },
+ false,
+ true,
+ R.string.add_games_warning,
+ R.string.add_games_warning_description,
+ R.string.add_games_warning_help,
+ )
+ )
},
- true,
- R.string.add_games_warning,
- R.string.add_games_warning_description,
- R.string.add_games_warning_help,
{
- if (NativeConfig.getGameDirs().isNotEmpty()) {
- StepState.COMPLETE
+ val file = File(
+ DirectoryInitialization.userDirectory + "/keys/prod.keys"
+ )
+ if (file.exists() && NativeLibrary.areKeysPresent() &&
+ NativeLibrary.isFirmwareAvailable() && NativeConfig.getGameDirs()
+ .isNotEmpty()
+ ) {
+ PageState.COMPLETE
} else {
- StepState.INCOMPLETE
+ PageState.INCOMPLETE
}
}
)
@@ -221,12 +246,22 @@ class SetupFragment : Fragment() {
R.drawable.ic_check,
R.string.done,
R.string.done_description,
- R.drawable.ic_arrow_forward,
- false,
- R.string.text_continue,
- { finishSetup() },
- false
- )
+ mutableListOf<PageButton>().apply {
+ add(
+ PageButton(
+ R.drawable.ic_arrow_forward,
+ R.string.get_started,
+ 0,
+ buttonAction = {
+ finishSetup()
+ },
+ buttonState = {
+ ButtonState.BUTTON_ACTION_UNDEFINED
+ },
+ )
+ )
+ }
+ ) { PageState.UNDEFINED }
)
}
@@ -237,7 +272,7 @@ class SetupFragment : Fragment() {
homeViewModel.gamesDirSelected.collect(
viewLifecycleOwner,
resetState = { homeViewModel.setGamesDirSelected(false) }
- ) { if (it) gamesDirCallback.onStepCompleted() }
+ ) { if (it) checkForButtonState.invoke() }
binding.viewPager2.apply {
adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages)
@@ -251,15 +286,18 @@ class SetupFragment : Fragment() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
- if (position == 1 && previousPosition == 0) {
- ViewUtils.showView(binding.buttonNext)
- ViewUtils.showView(binding.buttonBack)
- } else if (position == 0 && previousPosition == 1) {
+ val isFirstPage = position == 0
+ val isLastPage = position == pages.size - 1
+
+ if (isFirstPage) {
ViewUtils.hideView(binding.buttonBack)
+ } else {
+ ViewUtils.showView(binding.buttonBack)
+ }
+
+ if (isLastPage) {
ViewUtils.hideView(binding.buttonNext)
- } else if (position == pages.size - 1 && previousPosition == pages.size - 2) {
- ViewUtils.hideView(binding.buttonNext)
- } else if (position == pages.size - 2 && previousPosition == pages.size - 1) {
+ } else {
ViewUtils.showView(binding.buttonNext)
}
@@ -271,35 +309,63 @@ class SetupFragment : Fragment() {
val index = binding.viewPager2.currentItem
val currentPage = pages[index]
- // Checks if the user has completed the task on the current page
- if (currentPage.hasWarning) {
- val stepState = currentPage.stepCompleted.invoke()
- if (stepState != StepState.INCOMPLETE) {
- pageForward()
- return@setOnClickListener
- }
+ val warningMessages =
+ mutableListOf<Triple<Int, Int, Int>>() // title, description, helpLink
- if (!hasBeenWarned[index]) {
- SetupWarningDialogFragment.newInstance(
- currentPage.warningTitleId,
- currentPage.warningDescriptionId,
- currentPage.warningHelpLinkId,
- index
- ).show(childFragmentManager, SetupWarningDialogFragment.TAG)
- return@setOnClickListener
+ currentPage.pageButtons?.forEach { button ->
+ if (button.hasWarning || button.isUnskippable) {
+ val buttonState = button.buttonState()
+ if (buttonState == ButtonState.BUTTON_ACTION_COMPLETE) {
+ return@forEach
+ }
+
+ if (button.isUnskippable) {
+ MessageDialogFragment.newInstance(
+ activity = requireActivity(),
+ titleId = button.warningTitleId,
+ descriptionId = button.warningDescriptionId,
+ helpLinkId = button.warningHelpLinkId
+ ).show(childFragmentManager, MessageDialogFragment.TAG)
+ return@setOnClickListener
+ }
+
+ if (!hasBeenWarned[index]) {
+ warningMessages.add(
+ Triple(
+ button.warningTitleId,
+ button.warningDescriptionId,
+ button.warningHelpLinkId
+ )
+ )
+ }
}
}
+
+ if (warningMessages.isNotEmpty()) {
+ SetupWarningDialogFragment.newInstance(
+ warningMessages.map { it.first }.toIntArray(),
+ warningMessages.map { it.second }.toIntArray(),
+ warningMessages.map { it.third }.toIntArray(),
+ index
+ ).show(childFragmentManager, SetupWarningDialogFragment.TAG)
+ return@setOnClickListener
+ }
pageForward()
}
binding.buttonBack.setOnClickListener { pageBackward() }
+
if (savedInstanceState != null) {
val nextIsVisible = savedInstanceState.getBoolean(KEY_NEXT_VISIBILITY)
val backIsVisible = savedInstanceState.getBoolean(KEY_BACK_VISIBILITY)
hasBeenWarned = savedInstanceState.getBooleanArray(KEY_HAS_BEEN_WARNED)!!
- binding.buttonNext.setVisible(nextIsVisible)
- binding.buttonBack.setVisible(backIsVisible)
+ if (nextIsVisible) {
+ binding.buttonNext.visibility = View.VISIBLE
+ }
+ if (backIsVisible) {
+ binding.buttonBack.visibility = View.VISIBLE
+ }
} else {
hasBeenWarned = BooleanArray(pages.size)
}
@@ -307,6 +373,7 @@ class SetupFragment : Fragment() {
setInsets()
}
+
override fun onStop() {
super.onStop()
NativeConfig.saveGlobalConfig()
@@ -314,10 +381,8 @@ class SetupFragment : Fragment() {
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
- if (_binding != null) {
- outState.putBoolean(KEY_NEXT_VISIBILITY, binding.buttonNext.isVisible)
- outState.putBoolean(KEY_BACK_VISIBILITY, binding.buttonBack.isVisible)
- }
+ outState.putBoolean(KEY_NEXT_VISIBILITY, binding.buttonNext.isVisible)
+ outState.putBoolean(KEY_BACK_VISIBILITY, binding.buttonBack.isVisible)
outState.putBooleanArray(KEY_HAS_BEEN_WARNED, hasBeenWarned)
}
@@ -326,13 +391,27 @@ class SetupFragment : Fragment() {
_binding = null
}
- private lateinit var notificationCallback: SetupCallback
+ private val checkForButtonState: () -> Unit = {
+ val page = pages[binding.viewPager2.currentItem]
+ page.pageButtons?.forEach {
+ if (it.buttonState() == ButtonState.BUTTON_ACTION_COMPLETE) {
+ pageButtonCallback.onStepCompleted(
+ it.titleId,
+ pageFullyCompleted = false
+ )
+ }
+
+ if (page.pageSteps() == PageState.COMPLETE) {
+ pageButtonCallback.onStepCompleted(0, pageFullyCompleted = true)
+ }
+ }
+ }
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private val permissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) {
- notificationCallback.onStepCompleted()
+ checkForButtonState.invoke()
}
if (!it &&
@@ -345,15 +424,13 @@ class SetupFragment : Fragment() {
}
}
- private lateinit var keyCallback: SetupCallback
- private lateinit var firmwareCallback: SetupCallback
val getProdKey =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result != null) {
mainActivity.processKey(result, "keys")
if (NativeLibrary.areKeysPresent()) {
- keyCallback.onStepCompleted()
+ checkForButtonState.invoke()
}
}
}
@@ -363,14 +440,12 @@ class SetupFragment : Fragment() {
if (result != null) {
mainActivity.processFirmware(result) {
if (NativeLibrary.isFirmwareAvailable()) {
- firmwareCallback.onStepCompleted()
+ checkForButtonState.invoke()
}
}
}
}
- private lateinit var gamesDirCallback: SetupCallback
-
val getGamesDirectory =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
if (result != null) {
@@ -379,9 +454,13 @@ class SetupFragment : Fragment() {
}
private fun finishSetup() {
- PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
+ PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+ .edit()
.putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false)
.apply()
+
+ gamesViewModel.reloadGames(directoriesChanged = true, firstStartup = false)
+
mainActivity.finishSetup(binding.root.findNavController())
}
@@ -405,8 +484,10 @@ class SetupFragment : Fragment() {
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { _: View, windowInsets: WindowInsetsCompat ->
- val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
- val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+ val barInsets =
+ windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val cutoutInsets =
+ windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val leftPadding = barInsets.left + cutoutInsets.left
val topPadding = barInsets.top + cutoutInsets.top
@@ -415,11 +496,22 @@ class SetupFragment : Fragment() {
if (resources.getBoolean(R.bool.small_layout)) {
binding.viewPager2
- .updatePadding(left = leftPadding, top = topPadding, right = rightPadding)
+ .updatePadding(
+ left = leftPadding,
+ top = topPadding,
+ right = rightPadding
+ )
binding.constraintButtons
- .updatePadding(left = leftPadding, right = rightPadding, bottom = bottomPadding)
+ .updatePadding(
+ left = leftPadding,
+ right = rightPadding,
+ bottom = bottomPadding
+ )
} else {
- binding.viewPager2.updatePadding(top = topPadding, bottom = bottomPadding)
+ binding.viewPager2.updatePadding(
+ top = topPadding,
+ bottom = bottomPadding
+ )
binding.constraintButtons
.updatePadding(
left = leftPadding,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupWarningDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupWarningDialogFragment.kt
index b2c1d54af3..cfc981aea1 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupWarningDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupWarningDialogFragment.kt
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -11,20 +14,21 @@ import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.R
+import androidx.core.net.toUri
class SetupWarningDialogFragment : DialogFragment() {
- private var titleId: Int = 0
- private var descriptionId: Int = 0
- private var helpLinkId: Int = 0
+ private var titleIds: IntArray = intArrayOf()
+ private var descriptionIds: IntArray = intArrayOf()
+ private var helpLinkIds: IntArray = intArrayOf()
private var page: Int = 0
private lateinit var setupFragment: SetupFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- titleId = requireArguments().getInt(TITLE)
- descriptionId = requireArguments().getInt(DESCRIPTION)
- helpLinkId = requireArguments().getInt(HELP_LINK)
+ titleIds = requireArguments().getIntArray(TITLES) ?: intArrayOf()
+ descriptionIds = requireArguments().getIntArray(DESCRIPTIONS) ?: intArrayOf()
+ helpLinkIds = requireArguments().getIntArray(HELP_LINKS) ?: intArrayOf()
page = requireArguments().getInt(PAGE)
setupFragment = requireParentFragment() as SetupFragment
@@ -38,18 +42,24 @@ class SetupWarningDialogFragment : DialogFragment() {
}
.setNegativeButton(R.string.warning_cancel, null)
- if (titleId != 0) {
- builder.setTitle(titleId)
- } else {
- builder.setTitle("")
- }
- if (descriptionId != 0) {
- builder.setMessage(descriptionId)
+ val messageBuilder = StringBuilder()
+ for (i in titleIds.indices) {
+ if (titleIds[i] != 0) {
+ messageBuilder.append(getString(titleIds[i])).append("\n\n")
+ }
+ if (descriptionIds[i] != 0) {
+ messageBuilder.append(getString(descriptionIds[i])).append("\n\n")
+ }
}
- if (helpLinkId != 0) {
+
+ builder.setTitle("Warning")
+ builder.setMessage(messageBuilder.toString().trim())
+
+ if (helpLinkIds.any { it != 0 }) {
builder.setNeutralButton(R.string.warning_help) { _: DialogInterface?, _: Int ->
- val helpLink = resources.getString(R.string.install_prod_keys_warning_help)
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse(helpLink))
+ val helpLinkId = helpLinkIds.first { it != 0 }
+ val helpLink = resources.getString(helpLinkId)
+ val intent = Intent(Intent.ACTION_VIEW, helpLink.toUri())
startActivity(intent)
}
}
@@ -60,27 +70,27 @@ class SetupWarningDialogFragment : DialogFragment() {
companion object {
const val TAG = "SetupWarningDialogFragment"
- private const val TITLE = "Title"
- private const val DESCRIPTION = "Description"
- private const val HELP_LINK = "HelpLink"
+ private const val TITLES = "Titles"
+ private const val DESCRIPTIONS = "Descriptions"
+ private const val HELP_LINKS = "HelpLinks"
private const val PAGE = "Page"
fun newInstance(
- titleId: Int,
- descriptionId: Int,
- helpLinkId: Int,
+ titleIds: IntArray,
+ descriptionIds: IntArray,
+ helpLinkIds: IntArray,
page: Int
): SetupWarningDialogFragment {
val dialog = SetupWarningDialogFragment()
val bundle = Bundle()
bundle.apply {
- putInt(TITLE, titleId)
- putInt(DESCRIPTION, descriptionId)
- putInt(HELP_LINK, helpLinkId)
+ putIntArray(TITLES, titleIds)
+ putIntArray(DESCRIPTIONS, descriptionIds)
+ putIntArray(HELP_LINKS, helpLinkIds)
putInt(PAGE, page)
}
dialog.arguments = bundle
return dialog
}
}
-}
+} \ No newline at end of file
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
index 72ce006a7e..ae5f8f89de 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.model
@@ -145,7 +145,10 @@ class GamesViewModel : ViewModel() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
NativeConfig.addGameDir(gameDir)
- getGameDirs(true)
+ val isFirstTimeSetup = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+ .getBoolean(org.yuzu.yuzu_emu.features.settings.model.Settings.PREF_FIRST_APP_LAUNCH, true)
+
+ getGameDirs(!isFirstTimeSetup)
}
if (savedFromGameFragment) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt
index 09a128ae65..ae37634ce7 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -7,23 +10,36 @@ data class SetupPage(
val iconId: Int,
val titleId: Int,
val descriptionId: Int,
- val buttonIconId: Int,
- val leftAlignedIcon: Boolean,
- val buttonTextId: Int,
+ val pageButtons: List<PageButton>? = null,
+ val pageSteps: () -> PageState = { PageState.COMPLETE },
+
+ )
+
+data class PageButton(
+ val iconId: Int,
+ val titleId: Int,
+ val descriptionId: Int,
val buttonAction: (callback: SetupCallback) -> Unit,
- val hasWarning: Boolean,
+ val buttonState: () -> ButtonState = { ButtonState.BUTTON_ACTION_UNDEFINED },
+ val isUnskippable: Boolean = false,
+ val hasWarning: Boolean = false,
val warningTitleId: Int = 0,
val warningDescriptionId: Int = 0,
- val warningHelpLinkId: Int = 0,
- val stepCompleted: () -> StepState = { StepState.UNDEFINED }
+ val warningHelpLinkId: Int = 0
)
interface SetupCallback {
- fun onStepCompleted()
+ fun onStepCompleted(pageButtonId: Int, pageFullyCompleted: Boolean)
}
-enum class StepState {
+enum class PageState {
COMPLETE,
INCOMPLETE,
UNDEFINED
}
+
+enum class ButtonState {
+ BUTTON_ACTION_COMPLETE,
+ BUTTON_ACTION_INCOMPLETE,
+ BUTTON_ACTION_UNDEFINED
+}
diff --git a/src/android/app/src/main/res/drawable/ic_permission.xml b/src/android/app/src/main/res/drawable/ic_permission.xml
new file mode 100644
index 0000000000..b28cb13845
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_permission.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="?attr/colorControlNormal"
+ android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12V5l-9,-4zM12,11.99h7c-0.53,4.12 -3.28,7.79 -7,8.94V12H5V6.3l7,-3.11v8.8z"/>
+</vector> \ No newline at end of file
diff --git a/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml b/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml
index 72a8afc1d4..76920e3dc7 100644
--- a/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml
+++ b/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml
@@ -27,7 +27,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/next"
- android:visibility="invisible"
+ android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
diff --git a/src/android/app/src/main/res/layout-w600dp/page_setup.xml b/src/android/app/src/main/res/layout-w600dp/page_setup.xml
index 595abe01c7..e15e59577f 100644
--- a/src/android/app/src/main/res/layout-w600dp/page_setup.xml
+++ b/src/android/app/src/main/res/layout-w600dp/page_setup.xml
@@ -1,97 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout
+<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
- <LinearLayout
- android:layout_width="match_parent"
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/left_content"
+ android:layout_width="0dp"
android:layout_height="match_parent"
- android:orientation="vertical"
- android:layout_weight="1"
- android:gravity="center">
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/right_content"
+ app:layout_constraintHorizontal_weight="2">
<ImageView
android:id="@+id/icon"
- android:layout_width="260dp"
- android:layout_height="260dp"
- android:layout_gravity="center" />
-
- </LinearLayout>
-
- <androidx.constraintlayout.widget.ConstraintLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_weight="1">
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginTop="32dp"
+ app:layout_constraintBottom_toTopOf="@+id/text_title"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHeight_max="160dp"
+ app:layout_constraintHeight_min="80dp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintWidth_max="160dp"
+ app:layout_constraintWidth_min="80dp"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_constraintVertical_weight="3"
+ tools:src="@drawable/ic_notification" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_title"
- style="@style/TextAppearance.Material3.DisplaySmall"
+ style="@style/SynthwaveText.Header"
android:layout_width="0dp"
- android:layout_height="0dp"
- android:gravity="center"
- android:textColor="?attr/colorOnSurface"
+ android:layout_height="wrap_content"
+ android:textAlignment="center"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/text_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintVertical_weight="2"
+ app:layout_constraintTop_toBottomOf="@+id/icon"
tools:text="@string/welcome" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_description"
style="@style/TextAppearance.Material3.TitleLarge"
android:layout_width="0dp"
- android:layout_height="0dp"
- android:gravity="center"
- android:textSize="20sp"
+ android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
- app:layout_constraintBottom_toTopOf="@+id/button_action"
+ android:textAlignment="center"
+ android:textSize="20sp"
+ app:layout_constraintBottom_toTopOf="@+id/text_confirmation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_title"
- app:layout_constraintVertical_weight="2"
app:lineHeight="30sp"
tools:text="@string/welcome_description" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_confirmation"
- style="@style/TextAppearance.Material3.TitleLarge"
- android:layout_width="0dp"
- android:layout_height="0dp"
+ style="@style/SynthwaveText.Accent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
- android:paddingBottom="20dp"
- android:gravity="center"
- android:textSize="30sp"
- android:visibility="invisible"
android:text="@string/step_complete"
+ android:textAlignment="center"
+ android:textSize="30sp"
android:textStyle="bold"
+ android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_description"
- app:layout_constraintVertical_weight="1"
app:lineHeight="30sp" />
- <com.google.android.material.button.MaterialButton
- android:id="@+id/button_action"
- style="@style/EdenButton.Primary"
- android:layout_width="wrap_content"
- android:layout_height="56dp"
- android:layout_marginTop="16dp"
- android:layout_marginBottom="48dp"
- android:textSize="20sp"
- app:iconGravity="end"
- app:iconSize="24sp"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/text_description"
- tools:text="Get started" />
-
</androidx.constraintlayout.widget.ConstraintLayout>
-</LinearLayout>
+ <LinearLayout
+ android:id="@+id/right_content"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:padding="16dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/left_content"
+ app:layout_constraintHorizontal_weight="1">
+
+ <LinearLayout
+ android:id="@+id/page_button_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center" />
+
+ </LinearLayout>
+
+ </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/src/android/app/src/main/res/layout/fragment_setup.xml b/src/android/app/src/main/res/layout/fragment_setup.xml
index e953d52ea6..76b749db52 100644
--- a/src/android/app/src/main/res/layout/fragment_setup.xml
+++ b/src/android/app/src/main/res/layout/fragment_setup.xml
@@ -27,7 +27,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/next"
- android:visibility="invisible"
+ android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
diff --git a/src/android/app/src/main/res/layout/page_button.xml b/src/android/app/src/main/res/layout/page_button.xml
new file mode 100644
index 0000000000..36e176ab06
--- /dev/null
+++ b/src/android/app/src/main/res/layout/page_button.xml
@@ -0,0 +1,8 @@
+<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="170dp"
+ android:layout_height="55dp"
+ android:layout_marginBottom="16dp"
+ app:iconTint="?attr/colorOnPrimary"
+ app:iconSize="24dp"
+ style="@style/Widget.Material3.Button.UnelevatedButton" /> \ No newline at end of file
diff --git a/src/android/app/src/main/res/layout/page_setup.xml b/src/android/app/src/main/res/layout/page_setup.xml
index a334fcce2f..443a98bbda 100644
--- a/src/android/app/src/main/res/layout/page_setup.xml
+++ b/src/android/app/src/main/res/layout/page_setup.xml
@@ -11,7 +11,6 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="64dp"
- android:layout_marginBottom="32dp"
app:layout_constraintBottom_toTopOf="@+id/text_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="220dp"
@@ -45,7 +44,7 @@
android:textAlignment="center"
android:textSize="20sp"
android:paddingHorizontal="16dp"
- app:layout_constraintBottom_toTopOf="@+id/button_action"
+ app:layout_constraintBottom_toTopOf="@+id/text_confirmation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_title"
@@ -56,8 +55,8 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_confirmation"
style="@style/SynthwaveText.Accent"
- android:layout_width="wrap_content"
- android:layout_height="0dp"
+ android:layout_width="213dp"
+ android:layout_height="226dp"
android:paddingHorizontal="16dp"
android:paddingTop="24dp"
android:textAlignment="center"
@@ -71,20 +70,16 @@
app:layout_constraintVertical_weight="1"
app:lineHeight="30sp" />
- <com.google.android.material.button.MaterialButton
- android:id="@+id/button_action"
- style="@style/EdenButton.Primary"
- android:layout_width="wrap_content"
- android:layout_height="56dp"
- android:layout_marginTop="16dp"
- android:layout_marginBottom="48dp"
- android:textSize="20sp"
- app:iconGravity="end"
- app:iconSize="24sp"
+ <LinearLayout
+ android:id="@+id/page_button_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="16dp"
+ android:gravity="center"
+ app:layout_constraintTop_toBottomOf="@+id/text_description"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/text_description"
- tools:text="Get started" />
+ app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 0d863305d1..19554e7fe9 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -298,6 +298,10 @@
<string name="install_prod_keys_warning_description">Valid keys are required to emulate retail games. Only homebrew apps will function if you continue.</string>
<string name="install_prod_keys_warning_help">https://yuzu-mirror.github.io/help/quickstart/#guide-introduction</string>
<string name="install_firmware_warning">Skip adding firmware?</string>
+ <string name="emulator_data">Setup Emulator Data</string>
+ <string name="emulator_data_description">Keys are required in order for the emulator to work and firmware is recommended and required for using the QLaunch applet</string>
+ <string name="permissions">Grant Permissions</string>
+ <string name="permissions_description">Grant optional permissions to use specific features of the emulator</string>
<string name="install_firmware_warning_description">Many games require access to firmware to run properly.</string>
<string name="install_firmware_warning_help">https://yuzu-mirror.github.io/help/quickstart/#guide-introduction</string>
<string name="notifications">Notifications</string>