Explorar o código

Merge pull request #11273 from t895/setup-completion

android: Setup additions
bunnei %!s(int64=2) %!d(string=hai) anos
pai
achega
bbc6b08fc7

+ 20 - 2
src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt

@@ -5,13 +5,19 @@ package org.yuzu.yuzu_emu.adapters
 
 import android.text.Html
 import android.view.LayoutInflater
+import android.view.View
 import android.view.ViewGroup
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.content.res.ResourcesCompat
+import androidx.lifecycle.ViewModelProvider
 import androidx.recyclerview.widget.RecyclerView
 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.SetupCallback
 import org.yuzu.yuzu_emu.model.SetupPage
+import org.yuzu.yuzu_emu.model.StepState
+import org.yuzu.yuzu_emu.utils.ViewUtils
 
 class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) :
     RecyclerView.Adapter<SetupAdapter.SetupPageViewHolder>() {
@@ -26,7 +32,7 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>)
         holder.bind(pages[position])
 
     inner class SetupPageViewHolder(val binding: PageSetupBinding) :
-        RecyclerView.ViewHolder(binding.root) {
+        RecyclerView.ViewHolder(binding.root), SetupCallback {
         lateinit var page: SetupPage
 
         init {
@@ -35,6 +41,12 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>)
 
         fun bind(page: SetupPage) {
             this.page = page
+
+            if (page.stepCompleted.invoke() == StepState.COMPLETE) {
+                binding.buttonAction.visibility = View.INVISIBLE
+                binding.textConfirmation.visibility = View.VISIBLE
+            }
+
             binding.icon.setImageDrawable(
                 ResourcesCompat.getDrawable(
                     activity.resources,
@@ -62,9 +74,15 @@ class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>)
                         MaterialButton.ICON_GRAVITY_END
                     }
                 setOnClickListener {
-                    page.buttonAction.invoke()
+                    page.buttonAction.invoke(this@SetupPageViewHolder)
                 }
             }
         }
+
+        override fun onStepCompleted() {
+            ViewUtils.hideView(binding.buttonAction, 200)
+            ViewUtils.showView(binding.textConfirmation, 200)
+            ViewModelProvider(activity)[HomeViewModel::class.java].setShouldPageForward(true)
+        }
     }
 }

+ 96 - 49
src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt

@@ -19,6 +19,7 @@ import androidx.core.content.ContextCompat
 import androidx.core.view.ViewCompat
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.isVisible
+import androidx.core.view.updatePadding
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.activityViewModels
 import androidx.navigation.findNavController
@@ -32,10 +33,13 @@ 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.HomeViewModel
+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.ui.main.MainActivity
 import org.yuzu.yuzu_emu.utils.DirectoryInitialization
 import org.yuzu.yuzu_emu.utils.GameHelper
+import org.yuzu.yuzu_emu.utils.ViewUtils
 
 class SetupFragment : Fragment() {
     private var _binding: FragmentSetupBinding? = null
@@ -112,14 +116,22 @@ class SetupFragment : Fragment() {
                         0,
                         false,
                         R.string.give_permission,
-                        { permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) },
+                        {
+                            notificationCallback = it
+                            permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
+                        },
                         true,
                         R.string.notification_warning,
                         R.string.notification_warning_description,
                         0,
                         {
-                            NotificationManagerCompat.from(requireContext())
+                            if (NotificationManagerCompat.from(requireContext())
                                 .areNotificationsEnabled()
+                            ) {
+                                StepState.COMPLETE
+                            } else {
+                                StepState.INCOMPLETE
+                            }
                         }
                     )
                 )
@@ -133,12 +145,22 @@ class SetupFragment : Fragment() {
                     R.drawable.ic_add,
                     true,
                     R.string.select_keys,
-                    { mainActivity.getProdKey.launch(arrayOf("*/*")) },
+                    {
+                        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,
-                    { File(DirectoryInitialization.userDirectory + "/keys/prod.keys").exists() }
+                    {
+                        val file = File(DirectoryInitialization.userDirectory + "/keys/prod.keys")
+                        if (file.exists()) {
+                            StepState.COMPLETE
+                        } else {
+                            StepState.INCOMPLETE
+                        }
+                    }
                 )
             )
             add(
@@ -150,9 +172,8 @@ class SetupFragment : Fragment() {
                     true,
                     R.string.add_games,
                     {
-                        mainActivity.getGamesDirectory.launch(
-                            Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data
-                        )
+                        gamesDirCallback = it
+                        getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
                     },
                     true,
                     R.string.add_games_warning,
@@ -163,7 +184,11 @@ class SetupFragment : Fragment() {
                             PreferenceManager.getDefaultSharedPreferences(
                                 YuzuApplication.appContext
                             )
-                        preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()
+                        if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) {
+                            StepState.COMPLETE
+                        } else {
+                            StepState.INCOMPLETE
+                        }
                     }
                 )
             )
@@ -181,6 +206,13 @@ class SetupFragment : Fragment() {
             )
         }
 
+        homeViewModel.shouldPageForward.observe(viewLifecycleOwner) {
+            if (it) {
+                pageForward()
+                homeViewModel.setShouldPageForward(false)
+            }
+        }
+
         binding.viewPager2.apply {
             adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages)
             offscreenPageLimit = 2
@@ -194,15 +226,15 @@ class SetupFragment : Fragment() {
                 super.onPageSelected(position)
 
                 if (position == 1 && previousPosition == 0) {
-                    showView(binding.buttonNext)
-                    showView(binding.buttonBack)
+                    ViewUtils.showView(binding.buttonNext)
+                    ViewUtils.showView(binding.buttonBack)
                 } else if (position == 0 && previousPosition == 1) {
-                    hideView(binding.buttonBack)
-                    hideView(binding.buttonNext)
+                    ViewUtils.hideView(binding.buttonBack)
+                    ViewUtils.hideView(binding.buttonNext)
                 } else if (position == pages.size - 1 && previousPosition == pages.size - 2) {
-                    hideView(binding.buttonNext)
+                    ViewUtils.hideView(binding.buttonNext)
                 } else if (position == pages.size - 2 && previousPosition == pages.size - 1) {
-                    showView(binding.buttonNext)
+                    ViewUtils.showView(binding.buttonNext)
                 }
 
                 previousPosition = position
@@ -215,7 +247,8 @@ class SetupFragment : Fragment() {
 
             // Checks if the user has completed the task on the current page
             if (currentPage.hasWarning) {
-                if (currentPage.taskCompleted.invoke()) {
+                val stepState = currentPage.stepCompleted.invoke()
+                if (stepState != StepState.INCOMPLETE) {
                     pageForward()
                     return@setOnClickListener
                 }
@@ -264,9 +297,15 @@ class SetupFragment : Fragment() {
         _binding = null
     }
 
+    private lateinit var notificationCallback: SetupCallback
+
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     private val permissionLauncher =
         registerForActivityResult(ActivityResultContracts.RequestPermission()) {
+            if (it) {
+                notificationCallback.onStepCompleted()
+            }
+
             if (!it &&
                 !shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
             ) {
@@ -277,6 +316,27 @@ class SetupFragment : Fragment() {
             }
         }
 
+    private lateinit var keyCallback: SetupCallback
+
+    val getProdKey =
+        registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+            if (result != null) {
+                if (mainActivity.processKey(result)) {
+                    keyCallback.onStepCompleted()
+                }
+            }
+        }
+
+    private lateinit var gamesDirCallback: SetupCallback
+
+    val getGamesDirectory =
+        registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
+            if (result != null) {
+                mainActivity.processGamesDir(result)
+                gamesDirCallback.onStepCompleted()
+            }
+        }
+
     private fun finishSetup() {
         PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
             .putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false)
@@ -284,33 +344,6 @@ class SetupFragment : Fragment() {
         mainActivity.finishSetup(binding.root.findNavController())
     }
 
-    private fun showView(view: View) {
-        view.apply {
-            alpha = 0f
-            visibility = View.VISIBLE
-            isClickable = true
-        }.animate().apply {
-            duration = 300
-            alpha(1f)
-        }.start()
-    }
-
-    private fun hideView(view: View) {
-        if (view.visibility == View.INVISIBLE) {
-            return
-        }
-
-        view.apply {
-            alpha = 1f
-            isClickable = false
-        }.animate().apply {
-            duration = 300
-            alpha(0f)
-        }.withEndAction {
-            view.visibility = View.INVISIBLE
-        }
-    }
-
     fun pageForward() {
         binding.viewPager2.currentItem = binding.viewPager2.currentItem + 1
     }
@@ -326,15 +359,29 @@ class SetupFragment : Fragment() {
     private fun setInsets() =
         ViewCompat.setOnApplyWindowInsetsListener(
             binding.root
-        ) { view: View, windowInsets: WindowInsetsCompat ->
+        ) { _: View, windowInsets: WindowInsetsCompat ->
             val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
             val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
-            view.setPadding(
-                barInsets.left + cutoutInsets.left,
-                barInsets.top + cutoutInsets.top,
-                barInsets.right + cutoutInsets.right,
-                barInsets.bottom + cutoutInsets.bottom
-            )
+
+            val leftPadding = barInsets.left + cutoutInsets.left
+            val topPadding = barInsets.top + cutoutInsets.top
+            val rightPadding = barInsets.right + cutoutInsets.right
+            val bottomPadding = barInsets.bottom + cutoutInsets.bottom
+
+            if (resources.getBoolean(R.bool.small_layout)) {
+                binding.viewPager2
+                    .updatePadding(left = leftPadding, top = topPadding, right = rightPadding)
+                binding.constraintButtons
+                    .updatePadding(left = leftPadding, right = rightPadding, bottom = bottomPadding)
+            } else {
+                binding.viewPager2.updatePadding(top = topPadding, bottom = bottomPadding)
+                binding.constraintButtons
+                    .updatePadding(
+                        left = leftPadding,
+                        right = rightPadding,
+                        bottom = bottomPadding
+                    )
+            }
             windowInsets
         }
 }

+ 7 - 0
src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt

@@ -14,6 +14,9 @@ class HomeViewModel : ViewModel() {
     private val _statusBarShadeVisible = MutableLiveData(true)
     val statusBarShadeVisible: LiveData<Boolean> get() = _statusBarShadeVisible
 
+    private val _shouldPageForward = MutableLiveData(false)
+    val shouldPageForward: LiveData<Boolean> get() = _shouldPageForward
+
     var navigatedToSetup = false
 
     init {
@@ -33,4 +36,8 @@ class HomeViewModel : ViewModel() {
         }
         _statusBarShadeVisible.value = visible
     }
+
+    fun setShouldPageForward(pageForward: Boolean) {
+        _shouldPageForward.value = pageForward
+    }
 }

+ 12 - 2
src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt

@@ -10,10 +10,20 @@ data class SetupPage(
     val buttonIconId: Int,
     val leftAlignedIcon: Boolean,
     val buttonTextId: Int,
-    val buttonAction: () -> Unit,
+    val buttonAction: (callback: SetupCallback) -> Unit,
     val hasWarning: Boolean,
     val warningTitleId: Int = 0,
     val warningDescriptionId: Int = 0,
     val warningHelpLinkId: Int = 0,
-    val taskCompleted: () -> Boolean = { true }
+    val stepCompleted: () -> StepState = { StepState.UNDEFINED }
 )
+
+interface SetupCallback {
+    fun onStepCompleted()
+}
+
+enum class StepState {
+    COMPLETE,
+    INCOMPLETE,
+    UNDEFINED
+}

+ 58 - 51
src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt

@@ -266,73 +266,80 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
 
     val getGamesDirectory =
         registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
-            if (result == null) {
-                return@registerForActivityResult
+            if (result != null) {
+                processGamesDir(result)
             }
+        }
 
-            contentResolver.takePersistableUriPermission(
-                result,
-                Intent.FLAG_GRANT_READ_URI_PERMISSION
-            )
+    fun processGamesDir(result: Uri) {
+        contentResolver.takePersistableUriPermission(
+            result,
+            Intent.FLAG_GRANT_READ_URI_PERMISSION
+        )
 
-            // When a new directory is picked, we currently will reset the existing games
-            // database. This effectively means that only one game directory is supported.
-            PreferenceManager.getDefaultSharedPreferences(applicationContext).edit()
-                .putString(GameHelper.KEY_GAME_PATH, result.toString())
-                .apply()
+        // When a new directory is picked, we currently will reset the existing games
+        // database. This effectively means that only one game directory is supported.
+        PreferenceManager.getDefaultSharedPreferences(applicationContext).edit()
+            .putString(GameHelper.KEY_GAME_PATH, result.toString())
+            .apply()
 
-            Toast.makeText(
-                applicationContext,
-                R.string.games_dir_selected,
-                Toast.LENGTH_LONG
-            ).show()
+        Toast.makeText(
+            applicationContext,
+            R.string.games_dir_selected,
+            Toast.LENGTH_LONG
+        ).show()
 
-            gamesViewModel.reloadGames(true)
-        }
+        gamesViewModel.reloadGames(true)
+    }
 
     val getProdKey =
         registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
-            if (result == null) {
-                return@registerForActivityResult
+            if (result != null) {
+                processKey(result)
             }
+        }
 
-            if (FileUtil.getExtension(result) != "keys") {
-                MessageDialogFragment.newInstance(
-                    R.string.reading_keys_failure,
-                    R.string.install_prod_keys_failure_extension_description
-                ).show(supportFragmentManager, MessageDialogFragment.TAG)
-                return@registerForActivityResult
-            }
+    fun processKey(result: Uri): Boolean {
+        if (FileUtil.getExtension(result) != "keys") {
+            MessageDialogFragment.newInstance(
+                R.string.reading_keys_failure,
+                R.string.install_prod_keys_failure_extension_description
+            ).show(supportFragmentManager, MessageDialogFragment.TAG)
+            return false
+        }
 
-            contentResolver.takePersistableUriPermission(
+        contentResolver.takePersistableUriPermission(
+            result,
+            Intent.FLAG_GRANT_READ_URI_PERMISSION
+        )
+
+        val dstPath = DirectoryInitialization.userDirectory + "/keys/"
+        if (FileUtil.copyUriToInternalStorage(
+                applicationContext,
                 result,
-                Intent.FLAG_GRANT_READ_URI_PERMISSION
+                dstPath,
+                "prod.keys"
             )
-
-            val dstPath = DirectoryInitialization.userDirectory + "/keys/"
-            if (FileUtil.copyUriToInternalStorage(
+        ) {
+            if (NativeLibrary.reloadKeys()) {
+                Toast.makeText(
                     applicationContext,
-                    result,
-                    dstPath,
-                    "prod.keys"
-                )
-            ) {
-                if (NativeLibrary.reloadKeys()) {
-                    Toast.makeText(
-                        applicationContext,
-                        R.string.install_keys_success,
-                        Toast.LENGTH_SHORT
-                    ).show()
-                    gamesViewModel.reloadGames(true)
-                } else {
-                    MessageDialogFragment.newInstance(
-                        R.string.invalid_keys_error,
-                        R.string.install_keys_failure_description,
-                        R.string.dumping_keys_quickstart_link
-                    ).show(supportFragmentManager, MessageDialogFragment.TAG)
-                }
+                    R.string.install_keys_success,
+                    Toast.LENGTH_SHORT
+                ).show()
+                gamesViewModel.reloadGames(true)
+                return true
+            } else {
+                MessageDialogFragment.newInstance(
+                    R.string.invalid_keys_error,
+                    R.string.install_keys_failure_description,
+                    R.string.dumping_keys_quickstart_link
+                ).show(supportFragmentManager, MessageDialogFragment.TAG)
+                return false
             }
         }
+        return false
+    }
 
     val getFirmware =
         registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->

+ 35 - 0
src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/ViewUtils.kt

@@ -0,0 +1,35 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import android.view.View
+
+object ViewUtils {
+    fun showView(view: View, length: Long = 300) {
+        view.apply {
+            alpha = 0f
+            visibility = View.VISIBLE
+            isClickable = true
+        }.animate().apply {
+            duration = length
+            alpha(1f)
+        }.start()
+    }
+
+    fun hideView(view: View, length: Long = 300) {
+        if (view.visibility == View.INVISIBLE) {
+            return
+        }
+
+        view.apply {
+            alpha = 1f
+            isClickable = false
+        }.animate().apply {
+            duration = length
+            alpha(0f)
+        }.withEndAction {
+            view.visibility = View.INVISIBLE
+        }.start()
+    }
+}

+ 33 - 27
src/android/app/src/main/res/layout-w600dp/fragment_setup.xml

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout
+<RelativeLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/setup_root"
@@ -8,33 +8,39 @@
 
     <androidx.viewpager2.widget.ViewPager2
         android:id="@+id/viewPager2"
-        android:layout_width="0dp"
-        android:layout_height="0dp"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_alignParentTop="true"
+        android:layout_alignParentBottom="true"
+        android:clipToPadding="false" />
 
-    <com.google.android.material.button.MaterialButton
-        style="@style/Widget.Material3.Button.TextButton"
-        android:id="@+id/button_next"
-        android:layout_width="wrap_content"
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/constraint_buttons"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_margin="16dp"
-        android:text="@string/next"
-        android:visibility="invisible"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent" />
+        android:layout_alignParentBottom="true"
+        android:layout_margin="8dp">
 
-    <com.google.android.material.button.MaterialButton
-        android:id="@+id/button_back"
-        style="@style/Widget.Material3.Button.TextButton"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_margin="16dp"
-        android:text="@string/back"
-        android:visibility="invisible"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintStart_toStartOf="parent" />
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/button_next"
+            style="@style/Widget.Material3.Button.TextButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/next"
+            android:visibility="invisible"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent" />
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/button_back"
+            style="@style/Widget.Material3.Button.TextButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/back"
+            android:visibility="invisible"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
 
-</androidx.constraintlayout.widget.ConstraintLayout>
+</RelativeLayout>

+ 50 - 19
src/android/app/src/main/res/layout-w600dp/page_setup.xml

@@ -21,45 +21,76 @@
 
     </LinearLayout>
 
-    <LinearLayout
+    <androidx.constraintlayout.widget.ConstraintLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_weight="1"
-        android:orientation="vertical"
-        android:gravity="center">
+        android:layout_weight="1">
 
         <com.google.android.material.textview.MaterialTextView
-            style="@style/TextAppearance.Material3.DisplaySmall"
             android:id="@+id/text_title"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:textAlignment="center"
+            style="@style/TextAppearance.Material3.DisplaySmall"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:gravity="center"
             android:textColor="?attr/colorOnSurface"
             android:textStyle="bold"
+            app:layout_constraintBottom_toTopOf="@+id/text_description"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintVertical_weight="2"
             tools:text="@string/welcome" />
 
         <com.google.android.material.textview.MaterialTextView
-            style="@style/TextAppearance.Material3.TitleLarge"
             android:id="@+id/text_description"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="16dp"
-            android:paddingHorizontal="32dp"
-            android:textAlignment="center"
-            android:textSize="26sp"
-            app:lineHeight="40sp"
+            style="@style/TextAppearance.Material3.TitleLarge"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:gravity="center"
+            android:textSize="20sp"
+            android:paddingHorizontal="16dp"
+            app:layout_constraintBottom_toTopOf="@+id/button_action"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/text_title"
+            app:layout_constraintVertical_weight="2"
+            app:lineHeight="30sp"
             tools:text="@string/welcome_description" />
 
+        <com.google.android.material.textview.MaterialTextView
+            android:id="@+id/text_confirmation"
+            style="@style/TextAppearance.Material3.TitleLarge"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:paddingHorizontal="16dp"
+            android:paddingBottom="20dp"
+            android:gravity="center"
+            android:textSize="30sp"
+            android:visibility="invisible"
+            android:text="@string/step_complete"
+            android:textStyle="bold"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/text_description"
+            app:layout_constraintVertical_weight="1"
+            app:lineHeight="30sp" />
+
         <com.google.android.material.button.MaterialButton
             android:id="@+id/button_action"
             android:layout_width="wrap_content"
             android:layout_height="56dp"
-            android:layout_marginTop="32dp"
+            android:layout_marginTop="16dp"
+            android:layout_marginBottom="48dp"
             android:textSize="20sp"
-            app:iconSize="24sp"
             app:iconGravity="end"
+            app:iconSize="24sp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/text_description"
             tools:text="Get started" />
 
-    </LinearLayout>
+    </androidx.constraintlayout.widget.ConstraintLayout>
 
 </LinearLayout>

+ 33 - 29
src/android/app/src/main/res/layout/fragment_setup.xml

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout
+<RelativeLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/setup_root"
@@ -8,35 +8,39 @@
 
     <androidx.viewpager2.widget.ViewPager2
         android:id="@+id/viewPager2"
-        android:layout_width="0dp"
-        android:layout_height="0dp"
-        android:clipToPadding="false"
-        android:layout_marginBottom="16dp"
-        app:layout_constraintBottom_toTopOf="@+id/button_next"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
-
-    <com.google.android.material.button.MaterialButton
-        style="@style/Widget.Material3.Button.TextButton"
-        android:id="@+id/button_next"
-        android:layout_width="wrap_content"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_margin="12dp"
-        android:text="@string/next"
-        android:visibility="invisible"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent" />
+        android:layout_above="@+id/constraint_buttons"
+        android:layout_alignParentTop="true"
+        android:clipToPadding="false" />
 
-    <com.google.android.material.button.MaterialButton
-        style="@style/Widget.Material3.Button.TextButton"
-        android:id="@+id/button_back"
-        android:layout_width="wrap_content"
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/constraint_buttons"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_margin="12dp"
-        android:text="@string/back"
-        android:visibility="invisible"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintStart_toStartOf="parent" />
+        android:layout_margin="8dp"
+        android:layout_alignParentBottom="true">
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/button_next"
+            style="@style/Widget.Material3.Button.TextButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/next"
+            android:visibility="invisible"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent" />
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/button_back"
+            style="@style/Widget.Material3.Button.TextButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/back"
+            android:visibility="invisible"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintStart_toStartOf="parent" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
 
-</androidx.constraintlayout.widget.ConstraintLayout>
+</RelativeLayout>

+ 25 - 5
src/android/app/src/main/res/layout/page_setup.xml

@@ -21,11 +21,12 @@
         app:layout_constraintVertical_chainStyle="spread"
         app:layout_constraintWidth_max="220dp"
         app:layout_constraintWidth_min="110dp"
-        app:layout_constraintVertical_weight="3" />
+        app:layout_constraintVertical_weight="3"
+        tools:src="@drawable/ic_notification" />
 
     <com.google.android.material.textview.MaterialTextView
         android:id="@+id/text_title"
-        style="@style/TextAppearance.Material3.DisplayMedium"
+        style="@style/TextAppearance.Material3.DisplaySmall"
         android:layout_width="0dp"
         android:layout_height="0dp"
         android:textAlignment="center"
@@ -44,23 +45,42 @@
         android:layout_width="0dp"
         android:layout_height="0dp"
         android:textAlignment="center"
-        android:textSize="26sp"
+        android:textSize="20sp"
         android:paddingHorizontal="16dp"
         app:layout_constraintBottom_toTopOf="@+id/button_action"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toBottomOf="@+id/text_title"
         app:layout_constraintVertical_weight="2"
-        app:lineHeight="40sp"
+        app:lineHeight="30sp"
         tools:text="@string/welcome_description" />
 
+    <com.google.android.material.textview.MaterialTextView
+        android:id="@+id/text_confirmation"
+        style="@style/TextAppearance.Material3.TitleLarge"
+        android:layout_width="wrap_content"
+        android:layout_height="0dp"
+        android:paddingHorizontal="16dp"
+        android:paddingTop="24dp"
+        android:textAlignment="center"
+        android:textSize="30sp"
+        android:visibility="invisible"
+        android:text="@string/step_complete"
+        android:textStyle="bold"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/text_description"
+        app:layout_constraintVertical_weight="1"
+        app:lineHeight="30sp" />
+
     <com.google.android.material.button.MaterialButton
         android:id="@+id/button_action"
         android:layout_width="wrap_content"
         android:layout_height="56dp"
-        android:textSize="20sp"
         android:layout_marginTop="16dp"
         android:layout_marginBottom="48dp"
+        android:textSize="20sp"
         app:iconGravity="end"
         app:iconSize="24sp"
         app:layout_constraintBottom_toBottomOf="parent"

+ 1 - 0
src/android/app/src/main/res/values/strings.xml

@@ -29,6 +29,7 @@
     <string name="back">Back</string>
     <string name="add_games">Add Games</string>
     <string name="add_games_description">Select your games folder</string>
+    <string name="step_complete">Complete!</string>
 
     <!-- Home strings -->
     <string name="home_games">Games</string>