From f771952e62a3c7c175a99c91635509b24a8300f6 Mon Sep 17 00:00:00 2001 From: OpenSauce Date: Tue, 27 May 2025 17:41:27 +0000 Subject: [PATCH] android: Reorganize setup process to use multiple buttons per-page (#820) * Refactor SetupFragment to support multiple buttons in one page * Add new `PageButton` data class * Programmatic button creation && button disabling in setUpAdapter * Refactor SetupWarningDialogFragment to support multiple titles, descriptions, and help links * Rework CitraDirectoryHelper to support button step state * Update warning message for user folder selection step * Updated license headers * Code cleanup * "skip setting the user folder" --> "skip setting up the user folder" * Fixed typos in string names * Break `select_emulator_data_folder_description` string over two lines * `select_emulator_data_folder` --> `select_emulator_data_folders` * Code cleanup #2 * Removed seemingly accidentally duplicated block of code * Removed stray newlines --------- Co-authored-by: Kleidis <167202775+kleidis@users.noreply.github.com> --- .../citra/citra_emu/adapters/SetupAdapter.kt | 84 ++-- .../CopyDirProgressDialogFragment.kt | 4 +- .../citra_emu/fragments/SetupFragment.kt | 427 ++++++++++-------- .../fragments/SetupWarningDialogFragment.kt | 55 ++- .../org/citra/citra_emu/model/SetupPage.kt | 28 +- .../citra/citra_emu/ui/main/MainActivity.kt | 2 +- .../citra_emu/utils/CitraDirectoryHelper.kt | 6 +- .../src/main/res/drawable/ic_permission.xml | 9 + .../app/src/main/res/layout/page_button.xml | 8 + .../app/src/main/res/layout/page_setup.xml | 36 +- .../app/src/main/res/values/strings.xml | 6 +- 11 files changed, 403 insertions(+), 262 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/citra/citra_emu/adapters/SetupAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/adapters/SetupAdapter.kt index b6917b072..24761519c 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/adapters/SetupAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/adapters/SetupAdapter.kt @@ -1,9 +1,10 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. package org.citra.citra_emu.adapters +import android.content.res.ColorStateList import android.text.Html import android.text.method.LinkMovementMethod import android.view.LayoutInflater @@ -14,9 +15,11 @@ import androidx.core.content.res.ResourcesCompat import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton import org.citra.citra_emu.databinding.PageSetupBinding +import org.citra.citra_emu.model.ButtonState +import org.citra.citra_emu.model.PageState import org.citra.citra_emu.model.SetupCallback import org.citra.citra_emu.model.SetupPage -import org.citra.citra_emu.model.StepState +import org.citra.citra_emu.R import org.citra.citra_emu.utils.ViewUtils class SetupAdapter(val activity: AppCompatActivity, val pages: List) : @@ -42,8 +45,40 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List) fun bind(page: SetupPage) { this.page = page - if (page.stepCompleted.invoke() == StepState.STEP_COMPLETE) { - onStepCompleted() + if (page.pageSteps.invoke() == PageState.PAGE_STEPS_COMPLETE) { + onStepCompleted(0, pageFullyCompleted = true) + } + + if (page.pageButtons != null && page.pageSteps.invoke() != PageState.PAGE_STEPS_COMPLETE) { + for (pageButton in page.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( @@ -57,31 +92,26 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List) binding.textDescription.text = Html.fromHtml(activity.resources.getString(page.descriptionId), 0) binding.textDescription.movementMethod = LinkMovementMethod.getInstance() - - binding.buttonAction.apply { - text = activity.resources.getString(page.buttonTextId) - if (page.buttonIconId != 0) { - icon = ResourcesCompat.getDrawable( - activity.resources, - page.buttonIconId, - activity.theme - ) - } - iconGravity = - if (page.leftAlignedIcon) { - MaterialButton.ICON_GRAVITY_START - } else { - MaterialButton.ICON_GRAVITY_END - } - setOnClickListener { - page.buttonAction.invoke(this@SetupPageViewHolder) - } - } } - override fun onStepCompleted() { - ViewUtils.hideView(binding.buttonAction, 200) - ViewUtils.showView(binding.textConfirmation, 200) + 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) + } + + 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/citra/citra_emu/fragments/CopyDirProgressDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/CopyDirProgressDialogFragment.kt index 7fd92f979..14111de51 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/CopyDirProgressDialogFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/CopyDirProgressDialogFragment.kt @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -141,7 +141,7 @@ class CopyDirProgressDialog : DialogFragment() { override fun onComplete() { CitraDirectoryHelper.initializeCitraDirectory(path) - callback?.onStepCompleted() + callback?.onStepCompleted(0, false) viewModel.setCopyComplete(true) } }) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SetupFragment.kt index d7a3b0f2b..eecf3e4f9 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SetupFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SetupFragment.kt @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -35,9 +35,11 @@ import org.citra.citra_emu.R import org.citra.citra_emu.adapters.SetupAdapter import org.citra.citra_emu.databinding.FragmentSetupBinding import org.citra.citra_emu.features.settings.model.Settings +import org.citra.citra_emu.model.ButtonState +import org.citra.citra_emu.model.PageButton +import org.citra.citra_emu.model.PageState import org.citra.citra_emu.model.SetupCallback import org.citra.citra_emu.model.SetupPage -import org.citra.citra_emu.model.StepState import org.citra.citra_emu.ui.main.MainActivity import org.citra.citra_emu.utils.CitraDirectoryHelper import org.citra.citra_emu.utils.GameHelper @@ -113,151 +115,195 @@ class SetupFragment : Fragment() { 0, true, R.string.get_started, - { pageForward() } - ) - ) - - 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) - }, - false, - true, - { - if (NotificationManagerCompat.from(requireContext()) - .areNotificationsEnabled() - ) { - StepState.STEP_COMPLETE - } else { - StepState.STEP_INCOMPLETE - } - }, - R.string.notification_warning, - R.string.notification_warning_description, - 0 - ) - ) - } - - add( - SetupPage( - R.drawable.ic_microphone, - R.string.microphone_permission, - R.string.microphone_permission_description, - 0, - false, - R.string.give_permission, - { - microphoneCallback = it - permissionLauncher.launch(Manifest.permission.RECORD_AUDIO) - }, - false, - false, - { - if ( - ContextCompat.checkSelfPermission( - requireContext(), - Manifest.permission.RECORD_AUDIO - ) == PackageManager.PERMISSION_GRANTED - ) { - StepState.STEP_COMPLETE - } else { - StepState.STEP_INCOMPLETE - } + pageButtons = mutableListOf().apply { + add( + PageButton( + R.drawable.ic_arrow_forward, + R.string.get_started, + 0, + buttonAction = { + pageForward() + }, + buttonState = { + ButtonState.BUTTON_ACTION_UNDEFINED + } + ) + ) } ) ) + add( SetupPage( - R.drawable.ic_camera, - R.string.camera_permission, - R.string.camera_permission_description, + R.drawable.ic_permission, + R.string.permissions, + R.string.permissions_description, 0, false, - R.string.give_permission, - { - cameraCallback = it - permissionLauncher.launch(Manifest.permission.CAMERA) - }, - false, - false, - { - if ( - ContextCompat.checkSelfPermission( - requireContext(), - Manifest.permission.CAMERA - ) == PackageManager.PERMISSION_GRANTED - ) { - StepState.STEP_COMPLETE - } else { - StepState.STEP_INCOMPLETE + 0, + pageButtons = mutableListOf().apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + add( + PageButton( + R.drawable.ic_notification, + R.string.notifications, + R.string.notifications_description, + buttonAction = { + pageButtonCallback = it + permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + }, + buttonState = { + if (NotificationManagerCompat.from(requireContext()) + .areNotificationsEnabled() + ) { + ButtonState.BUTTON_ACTION_COMPLETE + } else { + ButtonState.BUTTON_ACTION_INCOMPLETE + } + }, + isUnskippable = false, + hasWarning = true, + R.string.notification_warning, + R.string.notification_warning_description, + ) + ) } - } - ) - ) - add( - SetupPage( - R.drawable.ic_home, - R.string.select_citra_user_folder, - R.string.select_citra_user_folder_description, - 0, - true, - R.string.select, - { - userDirCallback = it - openCitraDirectory.launch(null) - }, - true, - true, - { - if (PermissionsHandler.hasWriteAccess(requireContext())) { - StepState.STEP_COMPLETE - } else { - StepState.STEP_INCOMPLETE - } - }, - R.string.cannot_skip, - R.string.cannot_skip_directory_description, - R.string.cannot_skip_directory_help - ) - ) - add( - SetupPage( - R.drawable.ic_controller, - R.string.games, - R.string.games_description, - 0, - true, - R.string.select, - { - gamesDirCallback = it - getGamesDirectory.launch( - Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data + add( + PageButton( + R.drawable.ic_microphone, + R.string.microphone_permission, + R.string.microphone_permission_description, + buttonAction = { + pageButtonCallback = it + permissionLauncher.launch(Manifest.permission.RECORD_AUDIO) + }, + buttonState = { + if (ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.RECORD_AUDIO + ) == PackageManager.PERMISSION_GRANTED + ) { + ButtonState.BUTTON_ACTION_COMPLETE + } else { + ButtonState.BUTTON_ACTION_INCOMPLETE + } + }, + ) + ) + add( + PageButton( + R.drawable.ic_camera, + R.string.camera_permission, + R.string.camera_permission_description, + buttonAction = { + pageButtonCallback = it + permissionLauncher.launch(Manifest.permission.CAMERA) + }, + buttonState = { + if (ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + ) { + ButtonState.BUTTON_ACTION_COMPLETE + } else { + ButtonState.BUTTON_ACTION_INCOMPLETE + } + }, + ) ) }, - false, - true, - { - if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) { - StepState.STEP_COMPLETE - } else { - StepState.STEP_INCOMPLETE - } - }, - R.string.add_games_warning, - R.string.add_games_warning_description, - R.string.add_games_warning_help - ) + ) { + if ( + ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.RECORD_AUDIO + ) == PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED && + NotificationManagerCompat.from(requireContext()) + .areNotificationsEnabled() + ) { + PageState.PAGE_STEPS_COMPLETE + } else { + PageState.PAGE_STEPS_INCOMPLETE + } + } ) + + add( + SetupPage( + R.drawable.ic_folder, + R.string.select_emulator_data_folders, + R.string.select_emulator_data_folders_description, + 0, + true, + R.string.select, + pageButtons = mutableListOf().apply { + add( + PageButton( + R.drawable.ic_home, + R.string.select_citra_user_folder, + R.string.select_citra_user_folder_description, + buttonAction = { + pageButtonCallback = it + openCitraDirectory.launch(null) + }, + buttonState = { + if (PermissionsHandler.hasWriteAccess(requireContext())) { + ButtonState.BUTTON_ACTION_COMPLETE + } else { + ButtonState.BUTTON_ACTION_INCOMPLETE + } + }, + isUnskippable = true, + hasWarning = false, + R.string.cannot_skip, + R.string.cannot_skip_directory_description, + R.string.cannot_skip_directory_help + + ) + ) + add( + PageButton( + R.drawable.ic_controller, + R.string.games, + R.string.games_description, + buttonAction = { + pageButtonCallback = it + getGamesDirectory.launch( + Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data + ) + }, + buttonState = { + if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) { + ButtonState.BUTTON_ACTION_COMPLETE + } else { + ButtonState.BUTTON_ACTION_INCOMPLETE + } + }, + isUnskippable = false, + hasWarning = true, + R.string.add_games_warning, + R.string.add_games_warning_description, + ) + ) + }, + ) { + if ( + PermissionsHandler.hasWriteAccess(requireContext()) && + preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty() + ) { + PageState.PAGE_STEPS_COMPLETE + + } else { + PageState.PAGE_STEPS_INCOMPLETE + } + } + ) + add( SetupPage( R.drawable.ic_check, @@ -266,7 +312,21 @@ class SetupFragment : Fragment() { R.drawable.ic_arrow_forward, false, R.string.text_continue, - { finishSetup() } + pageButtons = mutableListOf().apply { + add( + PageButton( + R.drawable.ic_arrow_forward, + R.string.text_continue, + 0, + buttonAction = { + finishSetup() + }, + buttonState = { + ButtonState.BUTTON_ACTION_UNDEFINED + } + ) + ) + } ) ) } @@ -303,35 +363,47 @@ 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 || currentPage.isUnskippable) { - val stepState = currentPage.stepCompleted.invoke() - if (stepState == StepState.STEP_COMPLETE || - stepState == StepState.STEP_UNDEFINED - ) { - pageForward() - return@setOnClickListener - } + // This allows multiple sets of warning messages to be displayed on the same dialog if necessary + val warningMessages = + mutableListOf>() // title, description, helpLink - if (currentPage.isUnskippable) { - MessageDialogFragment.newInstance( - currentPage.warningTitleId, - currentPage.warningDescriptionId, - currentPage.warningHelpLinkId - ).show(childFragmentManager, MessageDialogFragment.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 (!hasBeenWarned[index]) { - SetupWarningDialogFragment.newInstance( - currentPage.warningTitleId, - currentPage.warningDescriptionId, - currentPage.warningHelpLinkId, - index - ).show(childFragmentManager, SetupWarningDialogFragment.TAG) - return@setOnClickListener + if (button.isUnskippable) { + MessageDialogFragment.newInstance( + button.warningTitleId, + button.warningDescriptionId, + 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() } @@ -366,19 +438,24 @@ class SetupFragment : Fragment() { _binding = null } - private lateinit var notificationCallback: SetupCallback - private lateinit var microphoneCallback: SetupCallback - private lateinit var cameraCallback: SetupCallback + private lateinit var pageButtonCallback: 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.PAGE_STEPS_COMPLETE) { + pageButtonCallback.onStepCompleted(0, pageFullyCompleted = true) + } + } + } private val permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> if (isGranted) { - val page = pages[binding.viewPager2.currentItem] - when (page.titleId) { - R.string.notifications -> notificationCallback.onStepCompleted() - R.string.microphone_permission -> microphoneCallback.onStepCompleted() - R.string.camera_permission -> cameraCallback.onStepCompleted() - } + checkForButtonState.invoke() return@registerForActivityResult } @@ -394,8 +471,6 @@ class SetupFragment : Fragment() { .show() } - private lateinit var userDirCallback: SetupCallback - private val openCitraDirectory = registerForActivityResult( ActivityResultContracts.OpenDocumentTree() ) { result: Uri? -> @@ -403,11 +478,9 @@ class SetupFragment : Fragment() { return@registerForActivityResult } - CitraDirectoryHelper(requireActivity()).showCitraDirectoryDialog(result, userDirCallback) + CitraDirectoryHelper(requireActivity()).showCitraDirectoryDialog(result, pageButtonCallback, checkForButtonState) } - private lateinit var gamesDirCallback: SetupCallback - private val getGamesDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> if (result == null) { @@ -427,7 +500,7 @@ class SetupFragment : Fragment() { homeViewModel.setGamesDir(requireActivity(), result.path!!) - gamesDirCallback.onStepCompleted() + checkForButtonState.invoke() } private fun finishSetup() { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SetupWarningDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SetupWarningDialogFragment.kt index 20a1e0baf..055d9bb9d 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SetupWarningDialogFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SetupWarningDialogFragment.kt @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -14,18 +14,18 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.citra.citra_emu.R 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 @@ -39,16 +39,23 @@ class SetupWarningDialogFragment : DialogFragment() { } .setNegativeButton(R.string.warning_cancel, null) - if (titleId != 0) { - builder.setTitle(titleId) - } else { - builder.setTitle("") + // Message builder to build multiple strings into one + 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 (descriptionId != 0) { - builder.setMessage(descriptionId) - } - 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 helpLinkId = helpLinkIds.first { it != 0 } val helpLink = resources.getString(helpLinkId) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(helpLink)) startActivity(intent) @@ -61,23 +68,23 @@ 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 diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/SetupPage.kt b/src/android/app/src/main/java/org/citra/citra_emu/model/SetupPage.kt index c45f05cf8..9302c4662 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/model/SetupPage.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/model/SetupPage.kt @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -11,21 +11,35 @@ data class SetupPage( val buttonIconId: Int, val leftAlignedIcon: Boolean, val buttonTextId: Int, + val pageButtons: List? = null, + val pageSteps: () -> PageState = { PageState.PAGE_STEPS_UNDEFINED }, +) + +data class PageButton( + val iconId: Int, + val titleId: Int, + val descriptionId: Int, val buttonAction: (callback: SetupCallback) -> Unit, + val buttonState: () -> ButtonState = { ButtonState.BUTTON_ACTION_UNDEFINED }, val isUnskippable: Boolean = false, val hasWarning: Boolean = false, - val stepCompleted: () -> StepState = { StepState.STEP_UNDEFINED }, val warningTitleId: Int = 0, val warningDescriptionId: Int = 0, val warningHelpLinkId: Int = 0 ) interface SetupCallback { - fun onStepCompleted() + fun onStepCompleted(pageButtonId : Int, pageFullyCompleted: Boolean) } -enum class StepState { - STEP_COMPLETE, - STEP_INCOMPLETE, - STEP_UNDEFINED +enum class PageState { + PAGE_STEPS_COMPLETE, + PAGE_STEPS_INCOMPLETE, + PAGE_STEPS_UNDEFINED +} + +enum class ButtonState { + BUTTON_ACTION_COMPLETE, + BUTTON_ACTION_INCOMPLETE, + BUTTON_ACTION_UNDEFINED } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt index 651c325b3..d88d61103 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt @@ -314,7 +314,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { return@registerForActivityResult } - CitraDirectoryHelper(this@MainActivity).showCitraDirectoryDialog(result) + CitraDirectoryHelper(this@MainActivity).showCitraDirectoryDialog(result, buttonState = {}) } val ciaFileInstaller = registerForActivityResult( diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/CitraDirectoryHelper.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/CitraDirectoryHelper.kt index 0b6c91a98..fa492a7bf 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/CitraDirectoryHelper.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/CitraDirectoryHelper.kt @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -17,7 +17,7 @@ import org.citra.citra_emu.viewmodel.HomeViewModel * Citra directory initialization ui flow controller. */ class CitraDirectoryHelper(private val fragmentActivity: FragmentActivity) { - fun showCitraDirectoryDialog(result: Uri, callback: SetupCallback? = null) { + fun showCitraDirectoryDialog(result: Uri, callback: SetupCallback? = null, buttonState: () -> Unit) { val citraDirectoryDialog = CitraDirectoryDialogFragment.newInstance( fragmentActivity, result.toString(), @@ -36,7 +36,7 @@ class CitraDirectoryHelper(private val fragmentActivity: FragmentActivity) { ) if (!moveData || previous.toString().isEmpty()) { initializeCitraDirectory(path) - callback?.onStepCompleted() + buttonState() val viewModel = ViewModelProvider(fragmentActivity)[HomeViewModel::class.java] viewModel.setUserDir(fragmentActivity, path.path!!) viewModel.setPickingUserDir(false) 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 000000000..b28cb1384 --- /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/page_button.xml b/src/android/app/src/main/res/layout/page_button.xml new file mode 100644 index 000000000..36e176ab0 --- /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 535abcf02..408e3b4dc 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" @@ -43,11 +42,11 @@ android:id="@+id/text_description" style="@style/TextAppearance.Material3.TitleLarge" android:layout_width="0dp" - android:layout_height="0dp" + android:layout_height="78dp" 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" @@ -58,15 +57,15 @@ - + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + 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 9dab4eaee..41a01a2c7 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -84,6 +84,10 @@ Skip selecting applications folder? Software won\'t be displayed in the Applications list if a folder isn\'t selected. https://web.archive.org/web/20240304210021/https://citra-emu.org/wiki/dumping-game-cartridges/ + Permissions + Data Folders + (User folder is required)]]> + Grant optional permissions to use specific features of the emulator Help Skip Cancel @@ -93,7 +97,7 @@ Keep Current Azahar Directory Use Prior Lime3DS Directory Select - You can\'t skip this step + You can\'t skip setting up the user folder This step is required to allow Azahar to work. Please select a directory and then you can continue. You have lost write permissions on your user data directory, where saves and other information are stored. This can happen after some app or Android updates. Please re-select the directory to regain permissions so you can continue. https://web.archive.org/web/20240304193549/https://github.com/citra-emu/citra/wiki/Citra-Android-user-data-and-storage