android: Rework setup fragment to use multiple buttons per-page (#2854)

Adapted from f771952e62 (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>
This commit is contained in:
kleidis 2025-12-04 07:33:04 +01:00 committed by crueter
parent 55cc4d5ede
commit f882ff72eb
No known key found for this signature in database
GPG Key ID: 425ACD2D4830EBC6
13 changed files with 473 additions and 299 deletions

View File

@ -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-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
@ -8,16 +11,16 @@ import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import org.yuzu.yuzu_emu.databinding.PageSetupBinding 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.SetupCallback
import org.yuzu.yuzu_emu.model.SetupPage 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
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder 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>) : class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
AbstractListAdapter<SetupPage, SetupAdapter.SetupPageViewHolder>(pages) { AbstractListAdapter<SetupPage, SetupAdapter.SetupPageViewHolder>(pages) {
@ -29,9 +32,40 @@ class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
inner class SetupPageViewHolder(val binding: PageSetupBinding) : inner class SetupPageViewHolder(val binding: PageSetupBinding) :
AbstractViewHolder<SetupPage>(binding), SetupCallback { AbstractViewHolder<SetupPage>(binding), SetupCallback {
override fun bind(model: SetupPage) { override fun bind(model: SetupPage) {
if (model.stepCompleted.invoke() == StepState.COMPLETE) { if (model.pageSteps.invoke() == PageState.COMPLETE) {
binding.buttonAction.setVisible(visible = false, gone = false) onStepCompleted(0, pageFullyCompleted = true)
binding.textConfirmation.setVisible(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( binding.icon.setImageDrawable(
@ -44,32 +78,26 @@ class SetupAdapter(val activity: AppCompatActivity, pages: List<SetupPage>) :
binding.textTitle.text = activity.resources.getString(model.titleId) binding.textTitle.text = activity.resources.getString(model.titleId)
binding.textDescription.text = binding.textDescription.text =
Html.fromHtml(activity.resources.getString(model.descriptionId), 0) 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() { override fun onStepCompleted(pageButtonId: Int, pageFullyCompleted: Boolean) {
ViewUtils.hideView(binding.buttonAction, 200) val button = binding.pageButtonContainer.findViewById<MaterialButton>(pageButtonId)
ViewUtils.showView(binding.textConfirmation, 200)
ViewModelProvider(activity)[HomeViewModel::class.java].setShouldPageForward(true) if (pageFullyCompleted) {
ViewUtils.hideView(binding.pageButtonContainer, 200)
ViewUtils.showView(binding.textConfirmation, 200)
}
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))
}
} }
} }
} }

View File

@ -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-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
@ -32,12 +35,14 @@ class AddGameFolderDialogFragment : DialogFragment() {
.setTitle(R.string.add_game_folder) .setTitle(R.string.add_game_folder)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked) val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked)
homeViewModel.setGamesDirSelected(true)
val calledFromGameFragment = requireArguments().getBoolean( val calledFromGameFragment = requireArguments().getBoolean(
"calledFromGameFragment", "calledFromGameFragment",
false false
) )
gamesViewModel.addFolder(newGameDir, calledFromGameFragment) val job = gamesViewModel.addFolder(newGameDir, calledFromGameFragment)
job.invokeOnCompletion {
homeViewModel.setGamesDirSelected(true)
}
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.setView(binding.root) .setView(binding.root)

View File

@ -26,7 +26,6 @@ import androidx.navigation.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.google.android.material.transition.MaterialFadeThrough import com.google.android.material.transition.MaterialFadeThrough
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import java.io.File import java.io.File
import org.yuzu.yuzu_emu.R 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.adapters.SetupAdapter
import org.yuzu.yuzu_emu.databinding.FragmentSetupBinding import org.yuzu.yuzu_emu.databinding.FragmentSetupBinding
import org.yuzu.yuzu_emu.features.settings.model.Settings 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.HomeViewModel
import org.yuzu.yuzu_emu.model.PageButton
import org.yuzu.yuzu_emu.model.SetupCallback import org.yuzu.yuzu_emu.model.SetupCallback
import org.yuzu.yuzu_emu.model.SetupPage 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.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NativeConfig
@ -50,11 +52,16 @@ class SetupFragment : Fragment() {
private val binding get() = _binding!! private val binding get() = _binding!!
private val homeViewModel: HomeViewModel by activityViewModels() private val homeViewModel: HomeViewModel by activityViewModels()
private val gamesViewModel: GamesViewModel by activityViewModels()
private lateinit var mainActivity: MainActivity private lateinit var mainActivity: MainActivity
private lateinit var hasBeenWarned: BooleanArray private lateinit var hasBeenWarned: BooleanArray
private lateinit var pages: MutableList<SetupPage>
private lateinit var pageButtonCallback: SetupCallback
companion object { companion object {
const val KEY_NEXT_VISIBILITY = "NextButtonVisibility" const val KEY_NEXT_VISIBILITY = "NextButtonVisibility"
const val KEY_BACK_VISIBILITY = "BackButtonVisibility" const val KEY_BACK_VISIBILITY = "BackButtonVisibility"
@ -94,124 +101,142 @@ class SetupFragment : Fragment() {
requireActivity().window.navigationBarColor = requireActivity().window.navigationBarColor =
ContextCompat.getColor(requireContext(), android.R.color.transparent) ContextCompat.getColor(requireContext(), android.R.color.transparent)
val pages = mutableListOf<SetupPage>() pages = mutableListOf<SetupPage>()
pages.apply { pages.apply {
add( add(
SetupPage( SetupPage(
R.drawable.ic_yuzu_title, R.drawable.ic_permission,
R.string.welcome, R.string.permissions,
R.string.welcome_description, R.string.permissions_description,
0, mutableListOf<PageButton>().apply {
true, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
R.string.get_started, add(
{ pageForward() }, PageButton(
false R.drawable.ic_notification,
) R.string.notifications,
) R.string.notifications_description,
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { pageButtonCallback = it
add( permissionLauncher.launch(
SetupPage( Manifest.permission.POST_NOTIFICATIONS
R.drawable.ic_notification, )
R.string.notifications, },
R.string.notifications_description, {
0, if (NotificationManagerCompat.from(requireContext())
false, .areNotificationsEnabled()
R.string.give_permission, ) {
{ ButtonState.BUTTON_ACTION_COMPLETE
notificationCallback = it } else {
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) ButtonState.BUTTON_ACTION_INCOMPLETE
}, }
true, },
R.string.notification_warning, false,
R.string.notification_warning_description, false,
0, )
{ )
if (NotificationManagerCompat.from(requireContext()) }
},
{
if (NotificationManagerCompat.from(requireContext())
.areNotificationsEnabled() .areNotificationsEnabled()
) { ) {
StepState.COMPLETE PageState.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 { } else {
StepState.INCOMPLETE PageState.INCOMPLETE
} }
} }
) )
) )
add( add(
SetupPage( SetupPage(
R.drawable.ic_firmware, R.drawable.ic_folder_open,
R.string.firmware, R.string.emulator_data,
R.string.firmware_description, R.string.emulator_data_description,
R.drawable.ic_add, mutableListOf<PageButton>().apply {
true, add(
R.string.select_firmware, PageButton(
{ R.drawable.ic_key,
firmwareCallback = it R.string.keys,
getFirmware.launch(arrayOf("application/zip")) 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.install_firmware_warning,
R.string.install_firmware_warning_description,
R.string.install_firmware_warning_help,
{ {
if (NativeLibrary.isFirmwareAvailable()) { val file = File(
StepState.COMPLETE DirectoryInitialization.userDirectory + "/keys/prod.keys"
)
if (file.exists() && NativeLibrary.areKeysPresent() &&
NativeLibrary.isFirmwareAvailable() && NativeConfig.getGameDirs()
.isNotEmpty()
) {
PageState.COMPLETE
} else { } 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)
},
true,
R.string.add_games_warning,
R.string.add_games_warning_description,
R.string.add_games_warning_help,
{
if (NativeConfig.getGameDirs().isNotEmpty()) {
StepState.COMPLETE
} else {
StepState.INCOMPLETE
} }
} }
) )
@ -221,12 +246,22 @@ class SetupFragment : Fragment() {
R.drawable.ic_check, R.drawable.ic_check,
R.string.done, R.string.done,
R.string.done_description, R.string.done_description,
R.drawable.ic_arrow_forward, mutableListOf<PageButton>().apply {
false, add(
R.string.text_continue, PageButton(
{ finishSetup() }, R.drawable.ic_arrow_forward,
false R.string.get_started,
) 0,
buttonAction = {
finishSetup()
},
buttonState = {
ButtonState.BUTTON_ACTION_UNDEFINED
},
)
)
}
) { PageState.UNDEFINED }
) )
} }
@ -237,7 +272,7 @@ class SetupFragment : Fragment() {
homeViewModel.gamesDirSelected.collect( homeViewModel.gamesDirSelected.collect(
viewLifecycleOwner, viewLifecycleOwner,
resetState = { homeViewModel.setGamesDirSelected(false) } resetState = { homeViewModel.setGamesDirSelected(false) }
) { if (it) gamesDirCallback.onStepCompleted() } ) { if (it) checkForButtonState.invoke() }
binding.viewPager2.apply { binding.viewPager2.apply {
adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages) adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages)
@ -251,15 +286,18 @@ class SetupFragment : Fragment() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
super.onPageSelected(position) super.onPageSelected(position)
if (position == 1 && previousPosition == 0) { val isFirstPage = position == 0
ViewUtils.showView(binding.buttonNext) val isLastPage = position == pages.size - 1
ViewUtils.showView(binding.buttonBack)
} else if (position == 0 && previousPosition == 1) { if (isFirstPage) {
ViewUtils.hideView(binding.buttonBack) ViewUtils.hideView(binding.buttonBack)
} else {
ViewUtils.showView(binding.buttonBack)
}
if (isLastPage) {
ViewUtils.hideView(binding.buttonNext) ViewUtils.hideView(binding.buttonNext)
} else if (position == pages.size - 1 && previousPosition == pages.size - 2) { } else {
ViewUtils.hideView(binding.buttonNext)
} else if (position == pages.size - 2 && previousPosition == pages.size - 1) {
ViewUtils.showView(binding.buttonNext) ViewUtils.showView(binding.buttonNext)
} }
@ -271,35 +309,63 @@ class SetupFragment : Fragment() {
val index = binding.viewPager2.currentItem val index = binding.viewPager2.currentItem
val currentPage = pages[index] val currentPage = pages[index]
// Checks if the user has completed the task on the current page val warningMessages =
if (currentPage.hasWarning) { mutableListOf<Triple<Int, Int, Int>>() // title, description, helpLink
val stepState = currentPage.stepCompleted.invoke()
if (stepState != StepState.INCOMPLETE) {
pageForward()
return@setOnClickListener
}
if (!hasBeenWarned[index]) { currentPage.pageButtons?.forEach { button ->
SetupWarningDialogFragment.newInstance( if (button.hasWarning || button.isUnskippable) {
currentPage.warningTitleId, val buttonState = button.buttonState()
currentPage.warningDescriptionId, if (buttonState == ButtonState.BUTTON_ACTION_COMPLETE) {
currentPage.warningHelpLinkId, return@forEach
index }
).show(childFragmentManager, SetupWarningDialogFragment.TAG)
return@setOnClickListener 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() pageForward()
} }
binding.buttonBack.setOnClickListener { pageBackward() } binding.buttonBack.setOnClickListener { pageBackward() }
if (savedInstanceState != null) { if (savedInstanceState != null) {
val nextIsVisible = savedInstanceState.getBoolean(KEY_NEXT_VISIBILITY) val nextIsVisible = savedInstanceState.getBoolean(KEY_NEXT_VISIBILITY)
val backIsVisible = savedInstanceState.getBoolean(KEY_BACK_VISIBILITY) val backIsVisible = savedInstanceState.getBoolean(KEY_BACK_VISIBILITY)
hasBeenWarned = savedInstanceState.getBooleanArray(KEY_HAS_BEEN_WARNED)!! hasBeenWarned = savedInstanceState.getBooleanArray(KEY_HAS_BEEN_WARNED)!!
binding.buttonNext.setVisible(nextIsVisible) if (nextIsVisible) {
binding.buttonBack.setVisible(backIsVisible) binding.buttonNext.visibility = View.VISIBLE
}
if (backIsVisible) {
binding.buttonBack.visibility = View.VISIBLE
}
} else { } else {
hasBeenWarned = BooleanArray(pages.size) hasBeenWarned = BooleanArray(pages.size)
} }
@ -307,6 +373,7 @@ class SetupFragment : Fragment() {
setInsets() setInsets()
} }
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
NativeConfig.saveGlobalConfig() NativeConfig.saveGlobalConfig()
@ -314,10 +381,8 @@ class SetupFragment : Fragment() {
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
if (_binding != null) { outState.putBoolean(KEY_NEXT_VISIBILITY, binding.buttonNext.isVisible)
outState.putBoolean(KEY_NEXT_VISIBILITY, binding.buttonNext.isVisible) outState.putBoolean(KEY_BACK_VISIBILITY, binding.buttonBack.isVisible)
outState.putBoolean(KEY_BACK_VISIBILITY, binding.buttonBack.isVisible)
}
outState.putBooleanArray(KEY_HAS_BEEN_WARNED, hasBeenWarned) outState.putBooleanArray(KEY_HAS_BEEN_WARNED, hasBeenWarned)
} }
@ -326,13 +391,27 @@ class SetupFragment : Fragment() {
_binding = null _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) @RequiresApi(Build.VERSION_CODES.TIRAMISU)
private val permissionLauncher = private val permissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) { if (it) {
notificationCallback.onStepCompleted() checkForButtonState.invoke()
} }
if (!it && if (!it &&
@ -345,15 +424,13 @@ class SetupFragment : Fragment() {
} }
} }
private lateinit var keyCallback: SetupCallback
private lateinit var firmwareCallback: SetupCallback
val getProdKey = val getProdKey =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result != null) { if (result != null) {
mainActivity.processKey(result, "keys") mainActivity.processKey(result, "keys")
if (NativeLibrary.areKeysPresent()) { if (NativeLibrary.areKeysPresent()) {
keyCallback.onStepCompleted() checkForButtonState.invoke()
} }
} }
} }
@ -363,14 +440,12 @@ class SetupFragment : Fragment() {
if (result != null) { if (result != null) {
mainActivity.processFirmware(result) { mainActivity.processFirmware(result) {
if (NativeLibrary.isFirmwareAvailable()) { if (NativeLibrary.isFirmwareAvailable()) {
firmwareCallback.onStepCompleted() checkForButtonState.invoke()
} }
} }
} }
} }
private lateinit var gamesDirCallback: SetupCallback
val getGamesDirectory = val getGamesDirectory =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
if (result != null) { if (result != null) {
@ -379,9 +454,13 @@ class SetupFragment : Fragment() {
} }
private fun finishSetup() { private fun finishSetup() {
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit() PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
.edit()
.putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false) .putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false)
.apply() .apply()
gamesViewModel.reloadGames(directoriesChanged = true, firstStartup = false)
mainActivity.finishSetup(binding.root.findNavController()) mainActivity.finishSetup(binding.root.findNavController())
} }
@ -405,8 +484,10 @@ class SetupFragment : Fragment() {
ViewCompat.setOnApplyWindowInsetsListener( ViewCompat.setOnApplyWindowInsetsListener(
binding.root binding.root
) { _: View, windowInsets: WindowInsetsCompat -> ) { _: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val barInsets =
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets =
windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val leftPadding = barInsets.left + cutoutInsets.left val leftPadding = barInsets.left + cutoutInsets.left
val topPadding = barInsets.top + cutoutInsets.top val topPadding = barInsets.top + cutoutInsets.top
@ -415,11 +496,22 @@ class SetupFragment : Fragment() {
if (resources.getBoolean(R.bool.small_layout)) { if (resources.getBoolean(R.bool.small_layout)) {
binding.viewPager2 binding.viewPager2
.updatePadding(left = leftPadding, top = topPadding, right = rightPadding) .updatePadding(
left = leftPadding,
top = topPadding,
right = rightPadding
)
binding.constraintButtons binding.constraintButtons
.updatePadding(left = leftPadding, right = rightPadding, bottom = bottomPadding) .updatePadding(
left = leftPadding,
right = rightPadding,
bottom = bottomPadding
)
} else { } else {
binding.viewPager2.updatePadding(top = topPadding, bottom = bottomPadding) binding.viewPager2.updatePadding(
top = topPadding,
bottom = bottomPadding
)
binding.constraintButtons binding.constraintButtons
.updatePadding( .updatePadding(
left = leftPadding, left = leftPadding,

View File

@ -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-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
@ -11,20 +14,21 @@ import android.os.Bundle
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import androidx.core.net.toUri
class SetupWarningDialogFragment : DialogFragment() { class SetupWarningDialogFragment : DialogFragment() {
private var titleId: Int = 0 private var titleIds: IntArray = intArrayOf()
private var descriptionId: Int = 0 private var descriptionIds: IntArray = intArrayOf()
private var helpLinkId: Int = 0 private var helpLinkIds: IntArray = intArrayOf()
private var page: Int = 0 private var page: Int = 0
private lateinit var setupFragment: SetupFragment private lateinit var setupFragment: SetupFragment
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
titleId = requireArguments().getInt(TITLE) titleIds = requireArguments().getIntArray(TITLES) ?: intArrayOf()
descriptionId = requireArguments().getInt(DESCRIPTION) descriptionIds = requireArguments().getIntArray(DESCRIPTIONS) ?: intArrayOf()
helpLinkId = requireArguments().getInt(HELP_LINK) helpLinkIds = requireArguments().getIntArray(HELP_LINKS) ?: intArrayOf()
page = requireArguments().getInt(PAGE) page = requireArguments().getInt(PAGE)
setupFragment = requireParentFragment() as SetupFragment setupFragment = requireParentFragment() as SetupFragment
@ -38,18 +42,24 @@ class SetupWarningDialogFragment : DialogFragment() {
} }
.setNegativeButton(R.string.warning_cancel, null) .setNegativeButton(R.string.warning_cancel, null)
if (titleId != 0) { val messageBuilder = StringBuilder()
builder.setTitle(titleId) for (i in titleIds.indices) {
} else { if (titleIds[i] != 0) {
builder.setTitle("") messageBuilder.append(getString(titleIds[i])).append("\n\n")
}
if (descriptionIds[i] != 0) {
messageBuilder.append(getString(descriptionIds[i])).append("\n\n")
}
} }
if (descriptionId != 0) {
builder.setMessage(descriptionId) builder.setTitle("Warning")
} builder.setMessage(messageBuilder.toString().trim())
if (helpLinkId != 0) {
if (helpLinkIds.any { it != 0 }) {
builder.setNeutralButton(R.string.warning_help) { _: DialogInterface?, _: Int -> builder.setNeutralButton(R.string.warning_help) { _: DialogInterface?, _: Int ->
val helpLink = resources.getString(R.string.install_prod_keys_warning_help) val helpLinkId = helpLinkIds.first { it != 0 }
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(helpLink)) val helpLink = resources.getString(helpLinkId)
val intent = Intent(Intent.ACTION_VIEW, helpLink.toUri())
startActivity(intent) startActivity(intent)
} }
} }
@ -60,27 +70,27 @@ class SetupWarningDialogFragment : DialogFragment() {
companion object { companion object {
const val TAG = "SetupWarningDialogFragment" const val TAG = "SetupWarningDialogFragment"
private const val TITLE = "Title" private const val TITLES = "Titles"
private const val DESCRIPTION = "Description" private const val DESCRIPTIONS = "Descriptions"
private const val HELP_LINK = "HelpLink" private const val HELP_LINKS = "HelpLinks"
private const val PAGE = "Page" private const val PAGE = "Page"
fun newInstance( fun newInstance(
titleId: Int, titleIds: IntArray,
descriptionId: Int, descriptionIds: IntArray,
helpLinkId: Int, helpLinkIds: IntArray,
page: Int page: Int
): SetupWarningDialogFragment { ): SetupWarningDialogFragment {
val dialog = SetupWarningDialogFragment() val dialog = SetupWarningDialogFragment()
val bundle = Bundle() val bundle = Bundle()
bundle.apply { bundle.apply {
putInt(TITLE, titleId) putIntArray(TITLES, titleIds)
putInt(DESCRIPTION, descriptionId) putIntArray(DESCRIPTIONS, descriptionIds)
putInt(HELP_LINK, helpLinkId) putIntArray(HELP_LINKS, helpLinkIds)
putInt(PAGE, page) putInt(PAGE, page)
} }
dialog.arguments = bundle dialog.arguments = bundle
return dialog return dialog
} }
} }
} }

View File

@ -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 // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.model package org.yuzu.yuzu_emu.model
@ -145,7 +145,10 @@ class GamesViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
NativeConfig.addGameDir(gameDir) 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) { if (savedFromGameFragment) {

View File

@ -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-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
@ -7,23 +10,36 @@ data class SetupPage(
val iconId: Int, val iconId: Int,
val titleId: Int, val titleId: Int,
val descriptionId: Int, val descriptionId: Int,
val buttonIconId: Int, val pageButtons: List<PageButton>? = null,
val leftAlignedIcon: Boolean, val pageSteps: () -> PageState = { PageState.COMPLETE },
val buttonTextId: Int,
)
data class PageButton(
val iconId: Int,
val titleId: Int,
val descriptionId: Int,
val buttonAction: (callback: SetupCallback) -> Unit, 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 warningTitleId: Int = 0,
val warningDescriptionId: Int = 0, val warningDescriptionId: Int = 0,
val warningHelpLinkId: Int = 0, val warningHelpLinkId: Int = 0
val stepCompleted: () -> StepState = { StepState.UNDEFINED }
) )
interface SetupCallback { interface SetupCallback {
fun onStepCompleted() fun onStepCompleted(pageButtonId: Int, pageFullyCompleted: Boolean)
} }
enum class StepState { enum class PageState {
COMPLETE, COMPLETE,
INCOMPLETE, INCOMPLETE,
UNDEFINED UNDEFINED
} }
enum class ButtonState {
BUTTON_ACTION_COMPLETE,
BUTTON_ACTION_INCOMPLETE,
BUTTON_ACTION_UNDEFINED
}

View File

@ -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>

View File

@ -27,7 +27,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/next" android:text="@string/next"
android:visibility="invisible" android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent" />

View File

@ -1,97 +1,101 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:id="@+id/left_content"
android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" app:layout_constraintStart_toStartOf="parent"
android:layout_weight="1" app:layout_constraintEnd_toStartOf="@+id/right_content"
android:gravity="center"> app:layout_constraintHorizontal_weight="2">
<ImageView <ImageView
android:id="@+id/icon" android:id="@+id/icon"
android:layout_width="260dp" android:layout_width="0dp"
android:layout_height="260dp" android:layout_height="0dp"
android:layout_gravity="center" /> android:layout_marginTop="32dp"
app:layout_constraintBottom_toTopOf="@+id/text_title"
</LinearLayout> app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="160dp"
<androidx.constraintlayout.widget.ConstraintLayout app:layout_constraintHeight_min="80dp"
android:layout_width="match_parent" app:layout_constraintStart_toStartOf="parent"
android:layout_height="match_parent" app:layout_constraintTop_toTopOf="parent"
android:layout_weight="1"> 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 <com.google.android.material.textview.MaterialTextView
android:id="@+id/text_title" android:id="@+id/text_title"
style="@style/TextAppearance.Material3.DisplaySmall" style="@style/SynthwaveText.Header"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="wrap_content"
android:gravity="center" android:textAlignment="center"
android:textColor="?attr/colorOnSurface"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/text_description" app:layout_constraintBottom_toTopOf="@+id/text_description"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toBottomOf="@+id/icon"
app:layout_constraintVertical_weight="2"
tools:text="@string/welcome" /> tools:text="@string/welcome" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/text_description" android:id="@+id/text_description"
style="@style/TextAppearance.Material3.TitleLarge" style="@style/TextAppearance.Material3.TitleLarge"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="wrap_content"
android:gravity="center"
android:textSize="20sp"
android:paddingHorizontal="16dp" 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_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_title" app:layout_constraintTop_toBottomOf="@+id/text_title"
app:layout_constraintVertical_weight="2"
app:lineHeight="30sp" app:lineHeight="30sp"
tools:text="@string/welcome_description" /> tools:text="@string/welcome_description" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/text_confirmation" android:id="@+id/text_confirmation"
style="@style/TextAppearance.Material3.TitleLarge" style="@style/SynthwaveText.Accent"
android:layout_width="0dp"
android:layout_height="0dp"
android:paddingHorizontal="16dp"
android:paddingBottom="20dp"
android:gravity="center"
android:textSize="30sp"
android:visibility="invisible"
android:text="@string/step_complete"
android:textStyle="bold"
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_width="wrap_content"
android:layout_height="56dp" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:paddingHorizontal="16dp"
android:layout_marginBottom="48dp" android:text="@string/step_complete"
android:textSize="20sp" android:textAlignment="center"
app:iconGravity="end" android:textSize="30sp"
app:iconSize="24sp" android:textStyle="bold"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_description" app:layout_constraintTop_toBottomOf="@+id/text_description"
tools:text="Get started" /> app:lineHeight="30sp" />
</androidx.constraintlayout.widget.ConstraintLayout> </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>

View File

@ -27,7 +27,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/next" android:text="@string/next"
android:visibility="invisible" android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent" />

View File

@ -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" />

View File

@ -11,7 +11,6 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="64dp" android:layout_marginTop="64dp"
android:layout_marginBottom="32dp"
app:layout_constraintBottom_toTopOf="@+id/text_title" app:layout_constraintBottom_toTopOf="@+id/text_title"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="220dp" app:layout_constraintHeight_max="220dp"
@ -45,7 +44,7 @@
android:textAlignment="center" android:textAlignment="center"
android:textSize="20sp" android:textSize="20sp"
android:paddingHorizontal="16dp" android:paddingHorizontal="16dp"
app:layout_constraintBottom_toTopOf="@+id/button_action" app:layout_constraintBottom_toTopOf="@+id/text_confirmation"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_title" app:layout_constraintTop_toBottomOf="@+id/text_title"
@ -56,8 +55,8 @@
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/text_confirmation" android:id="@+id/text_confirmation"
style="@style/SynthwaveText.Accent" style="@style/SynthwaveText.Accent"
android:layout_width="wrap_content" android:layout_width="213dp"
android:layout_height="0dp" android:layout_height="226dp"
android:paddingHorizontal="16dp" android:paddingHorizontal="16dp"
android:paddingTop="24dp" android:paddingTop="24dp"
android:textAlignment="center" android:textAlignment="center"
@ -71,20 +70,16 @@
app:layout_constraintVertical_weight="1" app:layout_constraintVertical_weight="1"
app:lineHeight="30sp" /> app:lineHeight="30sp" />
<com.google.android.material.button.MaterialButton <LinearLayout
android:id="@+id/button_action" android:id="@+id/page_button_container"
style="@style/EdenButton.Primary" android:layout_width="match_parent"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="56dp" android:orientation="vertical"
android:layout_marginTop="16dp" android:padding="16dp"
android:layout_marginBottom="48dp" android:gravity="center"
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" app:layout_constraintTop_toBottomOf="@+id/text_description"
tools:text="Get started" /> app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -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_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_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="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_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="install_firmware_warning_help">https://yuzu-mirror.github.io/help/quickstart/#guide-introduction</string>
<string name="notifications">Notifications</string> <string name="notifications">Notifications</string>