From f7bc39ef01d7e41bb427728eeea428c9629e7011 Mon Sep 17 00:00:00 2001 From: kleidis Date: Mon, 27 Oct 2025 17:26:28 +0100 Subject: [PATCH] android: Rework setup fragment to use multiple buttons per-page Adapted from https://github.com/azahar-emu/azahar/commit/f771952e62a3c7c175a99c91635509b24a8300f6#diff-e59f69380a076aef2745f7ab65072ca25fc26c598e2ed177475a15fe44121b4d --- .../yuzu/yuzu_emu/adapters/SetupAdapter.kt | 85 ++-- .../fragments/AddGameFolderDialogFragment.kt | 6 +- .../yuzu/yuzu_emu/fragments/SetupFragment.kt | 379 +++++++++++------- .../fragments/SetupWarningDialogFragment.kt | 59 +-- .../java/org/yuzu/yuzu_emu/model/SetupPage.kt | 29 +- .../src/main/res/drawable/ic_permission.xml | 9 + .../main/res/layout-w600dp/fragment_setup.xml | 2 +- .../src/main/res/layout-w600dp/page_setup.xml | 108 ++--- .../src/main/res/layout/fragment_setup.xml | 2 +- .../app/src/main/res/layout/page_button.xml | 8 + .../app/src/main/res/layout/page_setup.xml | 29 +- .../app/src/main/res/values/strings.xml | 4 + 12 files changed, 434 insertions(+), 286 deletions(-) create mode 100644 src/android/app/src/main/res/drawable/ic_permission.xml create mode 100644 src/android/app/src/main/res/layout/page_button.xml 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..d3d75ce657 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 @@ -8,16 +8,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) : AbstractListAdapter(pages) { @@ -29,9 +29,40 @@ class SetupAdapter(val activity: AppCompatActivity, pages: List) : inner class SetupPageViewHolder(val binding: PageSetupBinding) : AbstractViewHolder(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 +75,26 @@ class SetupAdapter(val activity: AppCompatActivity, pages: List) : 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(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..59e836765d 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 @@ -32,12 +32,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..3dc0015349 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,12 @@ 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.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 @@ -55,6 +56,10 @@ class SetupFragment : Fragment() { private lateinit var hasBeenWarned: BooleanArray + private lateinit var pages: MutableList + + private lateinit var pageButtonCallback: SetupCallback + companion object { const val KEY_NEXT_VISIBILITY = "NextButtonVisibility" const val KEY_BACK_VISIBILITY = "BackButtonVisibility" @@ -94,124 +99,142 @@ class SetupFragment : Fragment() { requireActivity().window.navigationBarColor = ContextCompat.getColor(requireContext(), android.R.color.transparent) - val pages = mutableListOf() + pages = mutableListOf() 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().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().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 +244,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().apply { + add( + PageButton( + R.drawable.ic_arrow_forward, + R.string.get_started, + 0, + buttonAction = { + finishSetup() + }, + buttonState = { + ButtonState.BUTTON_ACTION_UNDEFINED + }, + ) + ) + } + ) { PageState.UNDEFINED } ) } @@ -237,7 +270,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) @@ -256,11 +289,8 @@ class SetupFragment : Fragment() { ViewUtils.showView(binding.buttonBack) } else if (position == 0 && previousPosition == 1) { ViewUtils.hideView(binding.buttonBack) - 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) { - ViewUtils.showView(binding.buttonNext) } previousPosition = position @@ -271,35 +301,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>() // title, description, helpLink + + 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]) { - SetupWarningDialogFragment.newInstance( - currentPage.warningTitleId, - currentPage.warningDescriptionId, - currentPage.warningHelpLinkId, - index - ).show(childFragmentManager, SetupWarningDialogFragment.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 +365,7 @@ class SetupFragment : Fragment() { setInsets() } + override fun onStop() { super.onStop() NativeConfig.saveGlobalConfig() @@ -314,10 +373,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 +383,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 +416,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 +432,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,7 +446,8 @@ class SetupFragment : Fragment() { } private fun finishSetup() { - PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit() + PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + .edit() .putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false) .apply() mainActivity.finishSetup(binding.root.findNavController()) @@ -405,8 +473,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 +485,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..a6682fdc0a 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 @@ -11,20 +11,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 +39,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 +67,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/SetupPage.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt index 09a128ae65..427177b384 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 @@ -7,23 +7,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? = 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 @@ + + + \ 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 @@ - - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toStartOf="@+id/right_content" + app:layout_constraintHorizontal_weight="2"> - - - - + 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" /> - - - + + + + + + + \ 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 @@ + \ 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 @@ - + app:layout_constraintEnd_toEndOf="parent" /> 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 @@ Valid keys are required to emulate retail games. Only homebrew apps will function if you continue. https://yuzu-mirror.github.io/help/quickstart/#guide-introduction Skip adding firmware? + Setup Emulator Data + Keys are required in order for the emulator to work and firmware is recommended and required for using the QLaunch applet + Grant Permissions + Grant optional permissions to use specific features of the emulator Many games require access to firmware to run properly. https://yuzu-mirror.github.io/help/quickstart/#guide-introduction Notifications