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>
This commit is contained in:
OpenSauce 2025-05-27 17:41:27 +00:00 committed by GitHub
parent 6df92285e1
commit f771952e62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 403 additions and 262 deletions

View File

@ -1,9 +1,10 @@
// Copyright 2023 Citra Emulator Project // Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
package org.citra.citra_emu.adapters package org.citra.citra_emu.adapters
import android.content.res.ColorStateList
import android.text.Html import android.text.Html
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.view.LayoutInflater import android.view.LayoutInflater
@ -14,9 +15,11 @@ import androidx.core.content.res.ResourcesCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import org.citra.citra_emu.databinding.PageSetupBinding 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.SetupCallback
import org.citra.citra_emu.model.SetupPage 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 import org.citra.citra_emu.utils.ViewUtils
class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) : class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) :
@ -42,8 +45,40 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>)
fun bind(page: SetupPage) { fun bind(page: SetupPage) {
this.page = page this.page = page
if (page.stepCompleted.invoke() == StepState.STEP_COMPLETE) { if (page.pageSteps.invoke() == PageState.PAGE_STEPS_COMPLETE) {
onStepCompleted() 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( binding.icon.setImageDrawable(
@ -57,31 +92,26 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>)
binding.textDescription.text = binding.textDescription.text =
Html.fromHtml(activity.resources.getString(page.descriptionId), 0) Html.fromHtml(activity.resources.getString(page.descriptionId), 0)
binding.textDescription.movementMethod = LinkMovementMethod.getInstance() 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() { override fun onStepCompleted(pageButtonId: Int, pageFullyCompleted: Boolean) {
ViewUtils.hideView(binding.buttonAction, 200) val button = binding.pageButtonContainer.findViewById<MaterialButton>(pageButtonId)
if (pageFullyCompleted) {
ViewUtils.hideView(binding.pageButtonContainer, 200)
ViewUtils.showView(binding.textConfirmation, 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,4 +1,4 @@
// Copyright 2023 Citra Emulator Project // Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
@ -141,7 +141,7 @@ class CopyDirProgressDialog : DialogFragment() {
override fun onComplete() { override fun onComplete() {
CitraDirectoryHelper.initializeCitraDirectory(path) CitraDirectoryHelper.initializeCitraDirectory(path)
callback?.onStepCompleted() callback?.onStepCompleted(0, false)
viewModel.setCopyComplete(true) viewModel.setCopyComplete(true)
} }
}) })

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project // Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // 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.adapters.SetupAdapter
import org.citra.citra_emu.databinding.FragmentSetupBinding import org.citra.citra_emu.databinding.FragmentSetupBinding
import org.citra.citra_emu.features.settings.model.Settings 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.SetupCallback
import org.citra.citra_emu.model.SetupPage 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.ui.main.MainActivity
import org.citra.citra_emu.utils.CitraDirectoryHelper import org.citra.citra_emu.utils.CitraDirectoryHelper
import org.citra.citra_emu.utils.GameHelper import org.citra.citra_emu.utils.GameHelper
@ -113,151 +115,195 @@ class SetupFragment : Fragment() {
0, 0,
true, true,
R.string.get_started, R.string.get_started,
{ pageForward() } pageButtons = mutableListOf<PageButton>().apply {
add(
PageButton(
R.drawable.ic_arrow_forward,
R.string.get_started,
0,
buttonAction = {
pageForward()
},
buttonState = {
ButtonState.BUTTON_ACTION_UNDEFINED
}
)
)
}
) )
) )
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
add( add(
SetupPage( SetupPage(
R.drawable.ic_permission,
R.string.permissions,
R.string.permissions_description,
0,
false,
0,
pageButtons = mutableListOf<PageButton>().apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
add(
PageButton(
R.drawable.ic_notification, R.drawable.ic_notification,
R.string.notifications, R.string.notifications,
R.string.notifications_description, R.string.notifications_description,
0, buttonAction = {
false, pageButtonCallback = it
R.string.give_permission,
{
notificationCallback = it
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}, },
false, buttonState = {
true,
{
if (NotificationManagerCompat.from(requireContext()) if (NotificationManagerCompat.from(requireContext())
.areNotificationsEnabled() .areNotificationsEnabled()
) { ) {
StepState.STEP_COMPLETE ButtonState.BUTTON_ACTION_COMPLETE
} else { } else {
StepState.STEP_INCOMPLETE ButtonState.BUTTON_ACTION_INCOMPLETE
} }
}, },
isUnskippable = false,
hasWarning = true,
R.string.notification_warning, R.string.notification_warning,
R.string.notification_warning_description, R.string.notification_warning_description,
0
) )
) )
} }
add( add(
SetupPage( PageButton(
R.drawable.ic_microphone, R.drawable.ic_microphone,
R.string.microphone_permission, R.string.microphone_permission,
R.string.microphone_permission_description, R.string.microphone_permission_description,
0, buttonAction = {
false, pageButtonCallback = it
R.string.give_permission,
{
microphoneCallback = it
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO) permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}, },
false, buttonState = {
false, if (ContextCompat.checkSelfPermission(
{
if (
ContextCompat.checkSelfPermission(
requireContext(), requireContext(),
Manifest.permission.RECORD_AUDIO Manifest.permission.RECORD_AUDIO
) == PackageManager.PERMISSION_GRANTED ) == PackageManager.PERMISSION_GRANTED
) { ) {
StepState.STEP_COMPLETE ButtonState.BUTTON_ACTION_COMPLETE
} else { } else {
StepState.STEP_INCOMPLETE ButtonState.BUTTON_ACTION_INCOMPLETE
}
} }
},
) )
) )
add( add(
SetupPage( PageButton(
R.drawable.ic_camera, R.drawable.ic_camera,
R.string.camera_permission, R.string.camera_permission,
R.string.camera_permission_description, R.string.camera_permission_description,
0, buttonAction = {
false, pageButtonCallback = it
R.string.give_permission,
{
cameraCallback = it
permissionLauncher.launch(Manifest.permission.CAMERA) permissionLauncher.launch(Manifest.permission.CAMERA)
}, },
false, buttonState = {
false, if (ContextCompat.checkSelfPermission(
{
if (
ContextCompat.checkSelfPermission(
requireContext(), requireContext(),
Manifest.permission.CAMERA Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED ) == PackageManager.PERMISSION_GRANTED
) { ) {
StepState.STEP_COMPLETE ButtonState.BUTTON_ACTION_COMPLETE
} else { } else {
StepState.STEP_INCOMPLETE ButtonState.BUTTON_ACTION_INCOMPLETE
}
},
)
)
},
) {
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( add(
SetupPage( 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<PageButton>().apply {
add(
PageButton(
R.drawable.ic_home, R.drawable.ic_home,
R.string.select_citra_user_folder, R.string.select_citra_user_folder,
R.string.select_citra_user_folder_description, R.string.select_citra_user_folder_description,
0, buttonAction = {
true, pageButtonCallback = it
R.string.select,
{
userDirCallback = it
openCitraDirectory.launch(null) openCitraDirectory.launch(null)
}, },
true, buttonState = {
true,
{
if (PermissionsHandler.hasWriteAccess(requireContext())) { if (PermissionsHandler.hasWriteAccess(requireContext())) {
StepState.STEP_COMPLETE ButtonState.BUTTON_ACTION_COMPLETE
} else { } else {
StepState.STEP_INCOMPLETE ButtonState.BUTTON_ACTION_INCOMPLETE
} }
}, },
isUnskippable = true,
hasWarning = false,
R.string.cannot_skip, R.string.cannot_skip,
R.string.cannot_skip_directory_description, R.string.cannot_skip_directory_description,
R.string.cannot_skip_directory_help R.string.cannot_skip_directory_help
) )
) )
add( add(
SetupPage( PageButton(
R.drawable.ic_controller, R.drawable.ic_controller,
R.string.games, R.string.games,
R.string.games_description, R.string.games_description,
0, buttonAction = {
true, pageButtonCallback = it
R.string.select,
{
gamesDirCallback = it
getGamesDirectory.launch( getGamesDirectory.launch(
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data
) )
}, },
false, buttonState = {
true,
{
if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) { if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) {
StepState.STEP_COMPLETE ButtonState.BUTTON_ACTION_COMPLETE
} else { } else {
StepState.STEP_INCOMPLETE ButtonState.BUTTON_ACTION_INCOMPLETE
} }
}, },
isUnskippable = false,
hasWarning = true,
R.string.add_games_warning, R.string.add_games_warning,
R.string.add_games_warning_description, R.string.add_games_warning_description,
R.string.add_games_warning_help
) )
) )
},
) {
if (
PermissionsHandler.hasWriteAccess(requireContext()) &&
preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()
) {
PageState.PAGE_STEPS_COMPLETE
} else {
PageState.PAGE_STEPS_INCOMPLETE
}
}
)
add( add(
SetupPage( SetupPage(
R.drawable.ic_check, R.drawable.ic_check,
@ -266,7 +312,21 @@ class SetupFragment : Fragment() {
R.drawable.ic_arrow_forward, R.drawable.ic_arrow_forward,
false, false,
R.string.text_continue, R.string.text_continue,
{ finishSetup() } pageButtons = mutableListOf<PageButton>().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 index = binding.viewPager2.currentItem
val currentPage = pages[index] val currentPage = pages[index]
// Checks if the user has completed the task on the current page // This allows multiple sets of warning messages to be displayed on the same dialog if necessary
if (currentPage.hasWarning || currentPage.isUnskippable) { val warningMessages =
val stepState = currentPage.stepCompleted.invoke() mutableListOf<Triple<Int, Int, Int>>() // title, description, helpLink
if (stepState == StepState.STEP_COMPLETE ||
stepState == StepState.STEP_UNDEFINED currentPage.pageButtons?.forEach { button ->
) { if (button.hasWarning || button.isUnskippable) {
pageForward() val buttonState = button.buttonState()
return@setOnClickListener if (buttonState == ButtonState.BUTTON_ACTION_COMPLETE) {
return@forEach
} }
if (currentPage.isUnskippable) { if (button.isUnskippable) {
MessageDialogFragment.newInstance( MessageDialogFragment.newInstance(
currentPage.warningTitleId, button.warningTitleId,
currentPage.warningDescriptionId, button.warningDescriptionId,
currentPage.warningHelpLinkId button.warningHelpLinkId
).show(childFragmentManager, MessageDialogFragment.TAG) ).show(childFragmentManager, MessageDialogFragment.TAG)
return@setOnClickListener return@setOnClickListener
} }
if (!hasBeenWarned[index]) { if (!hasBeenWarned[index]) {
warningMessages.add(
Triple(
button.warningTitleId,
button.warningDescriptionId,
button.warningHelpLinkId
)
)
}
}
}
if (warningMessages.isNotEmpty()) {
SetupWarningDialogFragment.newInstance( SetupWarningDialogFragment.newInstance(
currentPage.warningTitleId, warningMessages.map { it.first }.toIntArray(),
currentPage.warningDescriptionId, warningMessages.map { it.second }.toIntArray(),
currentPage.warningHelpLinkId, warningMessages.map { it.third }.toIntArray(),
index index
).show(childFragmentManager, SetupWarningDialogFragment.TAG) ).show(childFragmentManager, SetupWarningDialogFragment.TAG)
return@setOnClickListener return@setOnClickListener
} }
}
pageForward() pageForward()
} }
binding.buttonBack.setOnClickListener { pageBackward() } binding.buttonBack.setOnClickListener { pageBackward() }
@ -366,19 +438,24 @@ class SetupFragment : Fragment() {
_binding = null _binding = null
} }
private lateinit var notificationCallback: SetupCallback private lateinit var pageButtonCallback: SetupCallback
private lateinit var microphoneCallback: SetupCallback private val checkForButtonState: () -> Unit = {
private lateinit var cameraCallback: SetupCallback 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 = private val permissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) { if (isGranted) {
val page = pages[binding.viewPager2.currentItem] checkForButtonState.invoke()
when (page.titleId) {
R.string.notifications -> notificationCallback.onStepCompleted()
R.string.microphone_permission -> microphoneCallback.onStepCompleted()
R.string.camera_permission -> cameraCallback.onStepCompleted()
}
return@registerForActivityResult return@registerForActivityResult
} }
@ -394,8 +471,6 @@ class SetupFragment : Fragment() {
.show() .show()
} }
private lateinit var userDirCallback: SetupCallback
private val openCitraDirectory = registerForActivityResult<Uri, Uri>( private val openCitraDirectory = registerForActivityResult<Uri, Uri>(
ActivityResultContracts.OpenDocumentTree() ActivityResultContracts.OpenDocumentTree()
) { result: Uri? -> ) { result: Uri? ->
@ -403,11 +478,9 @@ class SetupFragment : Fragment() {
return@registerForActivityResult return@registerForActivityResult
} }
CitraDirectoryHelper(requireActivity()).showCitraDirectoryDialog(result, userDirCallback) CitraDirectoryHelper(requireActivity()).showCitraDirectoryDialog(result, pageButtonCallback, checkForButtonState)
} }
private lateinit var gamesDirCallback: SetupCallback
private val getGamesDirectory = private val getGamesDirectory =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
if (result == null) { if (result == null) {
@ -427,7 +500,7 @@ class SetupFragment : Fragment() {
homeViewModel.setGamesDir(requireActivity(), result.path!!) homeViewModel.setGamesDir(requireActivity(), result.path!!)
gamesDirCallback.onStepCompleted() checkForButtonState.invoke()
} }
private fun finishSetup() { private fun finishSetup() {

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project // Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
@ -14,18 +14,18 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.citra.citra_emu.R import org.citra.citra_emu.R
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
@ -39,16 +39,23 @@ class SetupWarningDialogFragment : DialogFragment() {
} }
.setNegativeButton(R.string.warning_cancel, null) .setNegativeButton(R.string.warning_cancel, null)
if (titleId != 0) { // Message builder to build multiple strings into one
builder.setTitle(titleId) val messageBuilder = StringBuilder()
} else { for (i in titleIds.indices) {
builder.setTitle("") if (titleIds[i] != 0) {
messageBuilder.append(getString(titleIds[i])).append("\n\n")
} }
if (descriptionId != 0) { if (descriptionIds[i] != 0) {
builder.setMessage(descriptionId) 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 -> builder.setNeutralButton(R.string.warning_help) { _: DialogInterface?, _: Int ->
val helpLinkId = helpLinkIds.first { it != 0 }
val helpLink = resources.getString(helpLinkId) val helpLink = resources.getString(helpLinkId)
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(helpLink)) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(helpLink))
startActivity(intent) startActivity(intent)
@ -61,23 +68,23 @@ 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

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project // Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
@ -11,21 +11,35 @@ data class SetupPage(
val buttonIconId: Int, val buttonIconId: Int,
val leftAlignedIcon: Boolean, val leftAlignedIcon: Boolean,
val buttonTextId: Int, val buttonTextId: Int,
val pageButtons: List<PageButton>? = 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 buttonAction: (callback: SetupCallback) -> Unit,
val buttonState: () -> ButtonState = { ButtonState.BUTTON_ACTION_UNDEFINED },
val isUnskippable: Boolean = false, val isUnskippable: Boolean = false,
val hasWarning: Boolean = false, val hasWarning: Boolean = false,
val stepCompleted: () -> StepState = { StepState.STEP_UNDEFINED },
val warningTitleId: Int = 0, val warningTitleId: Int = 0,
val warningDescriptionId: Int = 0, val warningDescriptionId: Int = 0,
val warningHelpLinkId: Int = 0 val warningHelpLinkId: Int = 0
) )
interface SetupCallback { interface SetupCallback {
fun onStepCompleted() fun onStepCompleted(pageButtonId : Int, pageFullyCompleted: Boolean)
} }
enum class StepState { enum class PageState {
STEP_COMPLETE, PAGE_STEPS_COMPLETE,
STEP_INCOMPLETE, PAGE_STEPS_INCOMPLETE,
STEP_UNDEFINED PAGE_STEPS_UNDEFINED
}
enum class ButtonState {
BUTTON_ACTION_COMPLETE,
BUTTON_ACTION_INCOMPLETE,
BUTTON_ACTION_UNDEFINED
} }

View File

@ -314,7 +314,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
return@registerForActivityResult return@registerForActivityResult
} }
CitraDirectoryHelper(this@MainActivity).showCitraDirectoryDialog(result) CitraDirectoryHelper(this@MainActivity).showCitraDirectoryDialog(result, buttonState = {})
} }
val ciaFileInstaller = registerForActivityResult( val ciaFileInstaller = registerForActivityResult(

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project // Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
@ -17,7 +17,7 @@ import org.citra.citra_emu.viewmodel.HomeViewModel
* Citra directory initialization ui flow controller. * Citra directory initialization ui flow controller.
*/ */
class CitraDirectoryHelper(private val fragmentActivity: FragmentActivity) { 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( val citraDirectoryDialog = CitraDirectoryDialogFragment.newInstance(
fragmentActivity, fragmentActivity,
result.toString(), result.toString(),
@ -36,7 +36,7 @@ class CitraDirectoryHelper(private val fragmentActivity: FragmentActivity) {
) )
if (!moveData || previous.toString().isEmpty()) { if (!moveData || previous.toString().isEmpty()) {
initializeCitraDirectory(path) initializeCitraDirectory(path)
callback?.onStepCompleted() buttonState()
val viewModel = ViewModelProvider(fragmentActivity)[HomeViewModel::class.java] val viewModel = ViewModelProvider(fragmentActivity)[HomeViewModel::class.java]
viewModel.setUserDir(fragmentActivity, path.path!!) viewModel.setUserDir(fragmentActivity, path.path!!)
viewModel.setPickingUserDir(false) viewModel.setPickingUserDir(false)

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

@ -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"
@ -43,11 +42,11 @@
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="78dp"
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"
@ -58,15 +57,15 @@
<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/TextAppearance.Material3.TitleLarge"
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:text="@string/step_complete"
android:textAlignment="center" android:textAlignment="center"
android:textSize="30sp" android:textSize="30sp"
android:visibility="invisible"
android:text="@string/step_complete"
android:textStyle="bold" 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"
@ -74,19 +73,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"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="56dp" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:orientation="vertical"
android:layout_marginBottom="48dp" android:padding="16dp"
android:textSize="20sp" android:gravity="center"
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

@ -84,6 +84,10 @@
<string name="add_games_warning">Skip selecting applications folder?</string> <string name="add_games_warning">Skip selecting applications folder?</string>
<string name="add_games_warning_description">Software won\'t be displayed in the Applications list if a folder isn\'t selected.</string> <string name="add_games_warning_description">Software won\'t be displayed in the Applications list if a folder isn\'t selected.</string>
<string name="add_games_warning_help" translatable="false">https://web.archive.org/web/20240304210021/https://citra-emu.org/wiki/dumping-game-cartridges/</string> <string name="add_games_warning_help" translatable="false">https://web.archive.org/web/20240304210021/https://citra-emu.org/wiki/dumping-game-cartridges/</string>
<string name="permissions">Permissions</string>
<string name="select_emulator_data_folders">Data Folders</string>
<string name="select_emulator_data_folders_description"><![CDATA[Select data folders<br/>(User folder is required)]]></string>
<string name="permissions_description">Grant optional permissions to use specific features of the emulator</string>
<string name="warning_help">Help</string> <string name="warning_help">Help</string>
<string name="warning_skip">Skip</string> <string name="warning_skip">Skip</string>
<string name="warning_cancel">Cancel</string> <string name="warning_cancel">Cancel</string>
@ -93,7 +97,7 @@
<string name="keep_current_azahar_directory">Keep Current Azahar Directory</string> <string name="keep_current_azahar_directory">Keep Current Azahar Directory</string>
<string name="use_prior_lime3ds_directory">Use Prior Lime3DS Directory</string> <string name="use_prior_lime3ds_directory">Use Prior Lime3DS Directory</string>
<string name="select">Select</string> <string name="select">Select</string>
<string name="cannot_skip">You can\'t skip this step</string> <string name="cannot_skip">You can\'t skip setting up the user folder</string>
<string name="cannot_skip_directory_description">This step is required to allow Azahar to work. Please select a directory and then you can continue.</string> <string name="cannot_skip_directory_description">This step is required to allow Azahar to work. Please select a directory and then you can continue.</string>
<string name="selecting_user_directory_without_write_permissions">You have lost write permissions on your <a href="https://web.archive.org/web/20240304193549/https://github.com/citra-emu/citra/wiki/Citra-Android-user-data-and-storage">user data</a> 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.</string> <string name="selecting_user_directory_without_write_permissions">You have lost write permissions on your <a href="https://web.archive.org/web/20240304193549/https://github.com/citra-emu/citra/wiki/Citra-Android-user-data-and-storage">user data</a> 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.</string>
<string name="cannot_skip_directory_help" translatable="false">https://web.archive.org/web/20240304193549/https://github.com/citra-emu/citra/wiki/Citra-Android-user-data-and-storage</string> <string name="cannot_skip_directory_help" translatable="false">https://web.archive.org/web/20240304193549/https://github.com/citra-emu/citra/wiki/Citra-Android-user-data-and-storage</string>