diff options
| author | 2023-04-19 22:42:18 -0400 | |
|---|---|---|
| committer | 2023-06-03 00:05:52 -0700 | |
| commit | 59525ddbebc4c1a8add986eabd7802a62fedc71b (patch) | |
| tree | c06ba77c1d13a33918052ab51b01f73c57b8faac /src/android | |
| parent | android: Prevent editing unsafe settings at runtime (diff) | |
| download | yuzu-59525ddbebc4c1a8add986eabd7802a62fedc71b.tar.gz yuzu-59525ddbebc4c1a8add986eabd7802a62fedc71b.tar.xz yuzu-59525ddbebc4c1a8add986eabd7802a62fedc71b.zip | |
android: First time setup screen
Diffstat (limited to 'src/android')
19 files changed, 769 insertions, 163 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt new file mode 100644 index 000000000..481ddd5a5 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/SetupAdapter.kt | |||
| @@ -0,0 +1,70 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.adapters | ||
| 5 | |||
| 6 | import android.text.Html | ||
| 7 | import android.view.LayoutInflater | ||
| 8 | import android.view.ViewGroup | ||
| 9 | import androidx.appcompat.app.AppCompatActivity | ||
| 10 | import androidx.core.content.res.ResourcesCompat | ||
| 11 | import androidx.recyclerview.widget.RecyclerView | ||
| 12 | import com.google.android.material.button.MaterialButton | ||
| 13 | import org.yuzu.yuzu_emu.databinding.PageSetupBinding | ||
| 14 | import org.yuzu.yuzu_emu.model.SetupPage | ||
| 15 | |||
| 16 | class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) : | ||
| 17 | RecyclerView.Adapter<SetupAdapter.SetupPageViewHolder>() { | ||
| 18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetupPageViewHolder { | ||
| 19 | val binding = PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false) | ||
| 20 | return SetupPageViewHolder(binding) | ||
| 21 | } | ||
| 22 | |||
| 23 | override fun getItemCount(): Int = pages.size | ||
| 24 | |||
| 25 | override fun onBindViewHolder(holder: SetupPageViewHolder, position: Int) = | ||
| 26 | holder.bind(pages[position]) | ||
| 27 | |||
| 28 | inner class SetupPageViewHolder(val binding: PageSetupBinding) : | ||
| 29 | RecyclerView.ViewHolder(binding.root) { | ||
| 30 | lateinit var page: SetupPage | ||
| 31 | |||
| 32 | init { | ||
| 33 | itemView.tag = this | ||
| 34 | } | ||
| 35 | |||
| 36 | fun bind(page: SetupPage) { | ||
| 37 | this.page = page | ||
| 38 | binding.icon.setImageDrawable( | ||
| 39 | ResourcesCompat.getDrawable( | ||
| 40 | activity.resources, | ||
| 41 | page.iconId, | ||
| 42 | activity.theme | ||
| 43 | ) | ||
| 44 | ) | ||
| 45 | binding.textTitle.text = activity.resources.getString(page.titleId) | ||
| 46 | binding.textDescription.text = | ||
| 47 | Html.fromHtml(activity.resources.getString(page.descriptionId), 0) | ||
| 48 | |||
| 49 | binding.buttonAction.apply { | ||
| 50 | text = activity.resources.getString(page.buttonTextId) | ||
| 51 | if (page.buttonIconId != 0) { | ||
| 52 | icon = ResourcesCompat.getDrawable( | ||
| 53 | activity.resources, | ||
| 54 | page.buttonIconId, | ||
| 55 | activity.theme | ||
| 56 | ) | ||
| 57 | } | ||
| 58 | iconGravity = | ||
| 59 | if (page.leftAlignedIcon) { | ||
| 60 | MaterialButton.ICON_GRAVITY_START | ||
| 61 | } else { | ||
| 62 | MaterialButton.ICON_GRAVITY_END | ||
| 63 | } | ||
| 64 | setOnClickListener { | ||
| 65 | page.buttonAction.invoke() | ||
| 66 | } | ||
| 67 | } | ||
| 68 | } | ||
| 69 | } | ||
| 70 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt index 954e52dc6..1cf0d0f52 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/OptionsFragment.kt | |||
| @@ -10,39 +10,26 @@ import android.view.LayoutInflater | |||
| 10 | import android.view.View | 10 | import android.view.View |
| 11 | import android.view.ViewGroup | 11 | import android.view.ViewGroup |
| 12 | import android.widget.Toast | 12 | import android.widget.Toast |
| 13 | import androidx.activity.result.contract.ActivityResultContracts | ||
| 14 | import androidx.appcompat.app.AppCompatActivity | 13 | import androidx.appcompat.app.AppCompatActivity |
| 15 | import androidx.core.view.ViewCompat | 14 | import androidx.core.view.ViewCompat |
| 16 | import androidx.core.view.WindowInsetsCompat | 15 | import androidx.core.view.WindowInsetsCompat |
| 17 | import androidx.fragment.app.Fragment | 16 | import androidx.fragment.app.Fragment |
| 18 | import androidx.fragment.app.activityViewModels | ||
| 19 | import androidx.lifecycle.lifecycleScope | ||
| 20 | import androidx.preference.PreferenceManager | ||
| 21 | import androidx.recyclerview.widget.LinearLayoutManager | 17 | import androidx.recyclerview.widget.LinearLayoutManager |
| 22 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | 18 | import com.google.android.material.dialog.MaterialAlertDialogBuilder |
| 23 | import kotlinx.coroutines.Dispatchers | ||
| 24 | import kotlinx.coroutines.launch | ||
| 25 | import kotlinx.coroutines.withContext | ||
| 26 | import org.yuzu.yuzu_emu.NativeLibrary | ||
| 27 | import org.yuzu.yuzu_emu.R | 19 | import org.yuzu.yuzu_emu.R |
| 28 | import org.yuzu.yuzu_emu.adapters.HomeOptionAdapter | 20 | import org.yuzu.yuzu_emu.adapters.HomeOptionAdapter |
| 29 | import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding | ||
| 30 | import org.yuzu.yuzu_emu.databinding.FragmentOptionsBinding | 21 | import org.yuzu.yuzu_emu.databinding.FragmentOptionsBinding |
| 31 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity | 22 | import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity |
| 32 | import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile | 23 | import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile |
| 33 | import org.yuzu.yuzu_emu.model.GamesViewModel | ||
| 34 | import org.yuzu.yuzu_emu.model.HomeOption | 24 | import org.yuzu.yuzu_emu.model.HomeOption |
| 35 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization | 25 | import org.yuzu.yuzu_emu.ui.main.MainActivity |
| 36 | import org.yuzu.yuzu_emu.utils.FileUtil | ||
| 37 | import org.yuzu.yuzu_emu.utils.GameHelper | ||
| 38 | import org.yuzu.yuzu_emu.utils.GpuDriverHelper | 26 | import org.yuzu.yuzu_emu.utils.GpuDriverHelper |
| 39 | import java.io.IOException | ||
| 40 | 27 | ||
| 41 | class OptionsFragment : Fragment() { | 28 | class OptionsFragment : Fragment() { |
| 42 | private var _binding: FragmentOptionsBinding? = null | 29 | private var _binding: FragmentOptionsBinding? = null |
| 43 | private val binding get() = _binding!! | 30 | private val binding get() = _binding!! |
| 44 | 31 | ||
| 45 | private val gamesViewModel: GamesViewModel by activityViewModels() | 32 | private lateinit var mainActivity: MainActivity |
| 46 | 33 | ||
| 47 | override fun onCreateView( | 34 | override fun onCreateView( |
| 48 | inflater: LayoutInflater, | 35 | inflater: LayoutInflater, |
| @@ -54,22 +41,24 @@ class OptionsFragment : Fragment() { | |||
| 54 | } | 41 | } |
| 55 | 42 | ||
| 56 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | 43 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| 44 | mainActivity = requireActivity() as MainActivity | ||
| 45 | |||
| 57 | val optionsList: List<HomeOption> = listOf( | 46 | val optionsList: List<HomeOption> = listOf( |
| 58 | HomeOption( | 47 | HomeOption( |
| 59 | R.string.add_games, | 48 | R.string.add_games, |
| 60 | R.string.add_games_description, | 49 | R.string.add_games_description, |
| 61 | R.drawable.ic_add | 50 | R.drawable.ic_add |
| 62 | ) { getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) }, | 51 | ) { mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) }, |
| 63 | HomeOption( | 52 | HomeOption( |
| 64 | R.string.install_prod_keys, | 53 | R.string.install_prod_keys, |
| 65 | R.string.install_prod_keys_description, | 54 | R.string.install_prod_keys_description, |
| 66 | R.drawable.ic_unlock | 55 | R.drawable.ic_unlock |
| 67 | ) { getProdKey.launch(arrayOf("*/*")) }, | 56 | ) { mainActivity.getProdKey.launch(arrayOf("*/*")) }, |
| 68 | HomeOption( | 57 | HomeOption( |
| 69 | R.string.install_amiibo_keys, | 58 | R.string.install_amiibo_keys, |
| 70 | R.string.install_amiibo_keys_description, | 59 | R.string.install_amiibo_keys_description, |
| 71 | R.drawable.ic_nfc | 60 | R.drawable.ic_nfc |
| 72 | ) { getAmiiboKey.launch(arrayOf("*/*")) }, | 61 | ) { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) }, |
| 73 | HomeOption( | 62 | HomeOption( |
| 74 | R.string.install_gpu_driver, | 63 | R.string.install_gpu_driver, |
| 75 | R.string.install_gpu_driver_description, | 64 | R.string.install_gpu_driver_description, |
| @@ -115,7 +104,7 @@ class OptionsFragment : Fragment() { | |||
| 115 | ).show() | 104 | ).show() |
| 116 | } | 105 | } |
| 117 | .setNeutralButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int -> | 106 | .setNeutralButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int -> |
| 118 | getDriver.launch(arrayOf("application/zip")) | 107 | mainActivity.getDriver.launch(arrayOf("application/zip")) |
| 119 | } | 108 | } |
| 120 | .show() | 109 | .show() |
| 121 | } | 110 | } |
| @@ -131,144 +120,4 @@ class OptionsFragment : Fragment() { | |||
| 131 | ) | 120 | ) |
| 132 | windowInsets | 121 | windowInsets |
| 133 | } | 122 | } |
| 134 | |||
| 135 | private val getGamesDirectory = | ||
| 136 | registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> | ||
| 137 | if (result == null) | ||
| 138 | return@registerForActivityResult | ||
| 139 | |||
| 140 | val takeFlags = | ||
| 141 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||
| 142 | requireActivity().contentResolver.takePersistableUriPermission( | ||
| 143 | result, | ||
| 144 | takeFlags | ||
| 145 | ) | ||
| 146 | |||
| 147 | // When a new directory is picked, we currently will reset the existing games | ||
| 148 | // database. This effectively means that only one game directory is supported. | ||
| 149 | PreferenceManager.getDefaultSharedPreferences(requireContext()).edit() | ||
| 150 | .putString(GameHelper.KEY_GAME_PATH, result.toString()) | ||
| 151 | .apply() | ||
| 152 | |||
| 153 | gamesViewModel.reloadGames(true) | ||
| 154 | } | ||
| 155 | |||
| 156 | private val getProdKey = | ||
| 157 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> | ||
| 158 | if (result == null) | ||
| 159 | return@registerForActivityResult | ||
| 160 | |||
| 161 | val takeFlags = | ||
| 162 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||
| 163 | requireActivity().contentResolver.takePersistableUriPermission( | ||
| 164 | result, | ||
| 165 | takeFlags | ||
| 166 | ) | ||
| 167 | |||
| 168 | val dstPath = DirectoryInitialization.userDirectory + "/keys/" | ||
| 169 | if (FileUtil.copyUriToInternalStorage(requireContext(), result, dstPath, "prod.keys")) { | ||
| 170 | if (NativeLibrary.reloadKeys()) { | ||
| 171 | Toast.makeText( | ||
| 172 | requireContext(), | ||
| 173 | R.string.install_keys_success, | ||
| 174 | Toast.LENGTH_SHORT | ||
| 175 | ).show() | ||
| 176 | gamesViewModel.reloadGames(true) | ||
| 177 | } else { | ||
| 178 | Toast.makeText( | ||
| 179 | requireContext(), | ||
| 180 | R.string.install_keys_failure, | ||
| 181 | Toast.LENGTH_LONG | ||
| 182 | ).show() | ||
| 183 | } | ||
| 184 | } | ||
| 185 | } | ||
| 186 | |||
| 187 | private val getAmiiboKey = | ||
| 188 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> | ||
| 189 | if (result == null) | ||
| 190 | return@registerForActivityResult | ||
| 191 | |||
| 192 | val takeFlags = | ||
| 193 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||
| 194 | requireActivity().contentResolver.takePersistableUriPermission( | ||
| 195 | result, | ||
| 196 | takeFlags | ||
| 197 | ) | ||
| 198 | |||
| 199 | val dstPath = DirectoryInitialization.userDirectory + "/keys/" | ||
| 200 | if (FileUtil.copyUriToInternalStorage( | ||
| 201 | requireContext(), | ||
| 202 | result, | ||
| 203 | dstPath, | ||
| 204 | "key_retail.bin" | ||
| 205 | ) | ||
| 206 | ) { | ||
| 207 | if (NativeLibrary.reloadKeys()) { | ||
| 208 | Toast.makeText( | ||
| 209 | requireContext(), | ||
| 210 | R.string.install_keys_success, | ||
| 211 | Toast.LENGTH_SHORT | ||
| 212 | ).show() | ||
| 213 | } else { | ||
| 214 | Toast.makeText( | ||
| 215 | requireContext(), | ||
| 216 | R.string.install_amiibo_keys_failure, | ||
| 217 | Toast.LENGTH_LONG | ||
| 218 | ).show() | ||
| 219 | } | ||
| 220 | } | ||
| 221 | } | ||
| 222 | |||
| 223 | private val getDriver = | ||
| 224 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> | ||
| 225 | if (result == null) | ||
| 226 | return@registerForActivityResult | ||
| 227 | |||
| 228 | val takeFlags = | ||
| 229 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||
| 230 | requireActivity().contentResolver.takePersistableUriPermission( | ||
| 231 | result, | ||
| 232 | takeFlags | ||
| 233 | ) | ||
| 234 | |||
| 235 | val progressBinding = DialogProgressBarBinding.inflate(layoutInflater) | ||
| 236 | progressBinding.progressBar.isIndeterminate = true | ||
| 237 | val installationDialog = MaterialAlertDialogBuilder(requireContext()) | ||
| 238 | .setTitle(R.string.installing_driver) | ||
| 239 | .setView(progressBinding.root) | ||
| 240 | .show() | ||
| 241 | |||
| 242 | lifecycleScope.launch { | ||
| 243 | withContext(Dispatchers.IO) { | ||
| 244 | // Ignore file exceptions when a user selects an invalid zip | ||
| 245 | try { | ||
| 246 | GpuDriverHelper.installCustomDriver(requireContext(), result) | ||
| 247 | } catch (_: IOException) { | ||
| 248 | } | ||
| 249 | |||
| 250 | withContext(Dispatchers.Main) { | ||
| 251 | installationDialog.dismiss() | ||
| 252 | |||
| 253 | val driverName = GpuDriverHelper.customDriverName | ||
| 254 | if (driverName != null) { | ||
| 255 | Toast.makeText( | ||
| 256 | requireContext(), | ||
| 257 | getString( | ||
| 258 | R.string.select_gpu_driver_install_success, | ||
| 259 | driverName | ||
| 260 | ), | ||
| 261 | Toast.LENGTH_SHORT | ||
| 262 | ).show() | ||
| 263 | } else { | ||
| 264 | Toast.makeText( | ||
| 265 | requireContext(), | ||
| 266 | R.string.select_gpu_driver_error, | ||
| 267 | Toast.LENGTH_LONG | ||
| 268 | ).show() | ||
| 269 | } | ||
| 270 | } | ||
| 271 | } | ||
| 272 | } | ||
| 273 | } | ||
| 274 | } | 123 | } |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt new file mode 100644 index 000000000..e7d102aad --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt | |||
| @@ -0,0 +1,206 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.fragments | ||
| 5 | |||
| 6 | import android.content.Intent | ||
| 7 | import android.os.Bundle | ||
| 8 | import android.view.LayoutInflater | ||
| 9 | import android.view.View | ||
| 10 | import android.view.ViewGroup | ||
| 11 | import androidx.activity.OnBackPressedCallback | ||
| 12 | import androidx.appcompat.app.AppCompatActivity | ||
| 13 | import androidx.core.content.ContextCompat | ||
| 14 | import androidx.core.view.ViewCompat | ||
| 15 | import androidx.core.view.WindowInsetsCompat | ||
| 16 | import androidx.fragment.app.Fragment | ||
| 17 | import androidx.fragment.app.activityViewModels | ||
| 18 | import androidx.navigation.findNavController | ||
| 19 | import androidx.preference.PreferenceManager | ||
| 20 | import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback | ||
| 21 | import com.google.android.material.transition.MaterialFadeThrough | ||
| 22 | import org.yuzu.yuzu_emu.R | ||
| 23 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 24 | import org.yuzu.yuzu_emu.adapters.SetupAdapter | ||
| 25 | import org.yuzu.yuzu_emu.databinding.FragmentSetupBinding | ||
| 26 | import org.yuzu.yuzu_emu.features.settings.model.Settings | ||
| 27 | import org.yuzu.yuzu_emu.model.HomeViewModel | ||
| 28 | import org.yuzu.yuzu_emu.model.SetupPage | ||
| 29 | import org.yuzu.yuzu_emu.ui.main.MainActivity | ||
| 30 | |||
| 31 | class SetupFragment : Fragment() { | ||
| 32 | private var _binding: FragmentSetupBinding? = null | ||
| 33 | private val binding get() = _binding!! | ||
| 34 | |||
| 35 | private val homeViewModel: HomeViewModel by activityViewModels() | ||
| 36 | |||
| 37 | private lateinit var mainActivity: MainActivity | ||
| 38 | |||
| 39 | override fun onCreate(savedInstanceState: Bundle?) { | ||
| 40 | super.onCreate(savedInstanceState) | ||
| 41 | exitTransition = MaterialFadeThrough() | ||
| 42 | } | ||
| 43 | |||
| 44 | override fun onCreateView( | ||
| 45 | inflater: LayoutInflater, | ||
| 46 | container: ViewGroup?, | ||
| 47 | savedInstanceState: Bundle? | ||
| 48 | ): View { | ||
| 49 | _binding = FragmentSetupBinding.inflate(layoutInflater) | ||
| 50 | return binding.root | ||
| 51 | } | ||
| 52 | |||
| 53 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| 54 | mainActivity = requireActivity() as MainActivity | ||
| 55 | |||
| 56 | homeViewModel.setNavigationVisibility(false) | ||
| 57 | |||
| 58 | requireActivity().onBackPressedDispatcher.addCallback( | ||
| 59 | viewLifecycleOwner, | ||
| 60 | object : OnBackPressedCallback(true) { | ||
| 61 | override fun handleOnBackPressed() { | ||
| 62 | if (binding.viewPager2.currentItem > 0) { | ||
| 63 | pageBackward() | ||
| 64 | } else { | ||
| 65 | requireActivity().finish() | ||
| 66 | } | ||
| 67 | } | ||
| 68 | }) | ||
| 69 | |||
| 70 | requireActivity().window.navigationBarColor = | ||
| 71 | ContextCompat.getColor(requireContext(), android.R.color.transparent) | ||
| 72 | |||
| 73 | val pages = listOf( | ||
| 74 | SetupPage( | ||
| 75 | R.drawable.ic_yuzu_title, | ||
| 76 | R.string.welcome, | ||
| 77 | R.string.welcome_description, | ||
| 78 | 0, | ||
| 79 | true, | ||
| 80 | R.string.get_started | ||
| 81 | ) { pageForward() }, | ||
| 82 | SetupPage( | ||
| 83 | R.drawable.ic_key, | ||
| 84 | R.string.keys, | ||
| 85 | R.string.keys_description, | ||
| 86 | R.drawable.ic_add, | ||
| 87 | true, | ||
| 88 | R.string.select_keys | ||
| 89 | ) { mainActivity.getProdKey.launch(arrayOf("*/*")) }, | ||
| 90 | SetupPage( | ||
| 91 | R.drawable.ic_controller, | ||
| 92 | R.string.games, | ||
| 93 | R.string.games_description, | ||
| 94 | R.drawable.ic_add, | ||
| 95 | true, | ||
| 96 | R.string.add_games | ||
| 97 | ) { mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) }, | ||
| 98 | SetupPage( | ||
| 99 | R.drawable.ic_check, | ||
| 100 | R.string.done, | ||
| 101 | R.string.done_description, | ||
| 102 | R.drawable.ic_arrow_forward, | ||
| 103 | false, | ||
| 104 | R.string.text_continue | ||
| 105 | ) { finishSetup() } | ||
| 106 | ) | ||
| 107 | binding.viewPager2.apply { | ||
| 108 | adapter = SetupAdapter(requireActivity() as AppCompatActivity, pages) | ||
| 109 | offscreenPageLimit = 2 | ||
| 110 | } | ||
| 111 | |||
| 112 | binding.viewPager2.registerOnPageChangeCallback(object : OnPageChangeCallback() { | ||
| 113 | override fun onPageScrolled( | ||
| 114 | position: Int, | ||
| 115 | positionOffset: Float, | ||
| 116 | positionOffsetPixels: Int | ||
| 117 | ) { | ||
| 118 | super.onPageScrolled(position, positionOffset, positionOffsetPixels) | ||
| 119 | if (position == 0) { | ||
| 120 | hideView(binding.buttonBack) | ||
| 121 | } else { | ||
| 122 | showView(binding.buttonBack) | ||
| 123 | } | ||
| 124 | |||
| 125 | if (position == pages.size - 1 || position == 0) { | ||
| 126 | hideView(binding.buttonNext) | ||
| 127 | } else { | ||
| 128 | showView(binding.buttonNext) | ||
| 129 | } | ||
| 130 | } | ||
| 131 | }) | ||
| 132 | |||
| 133 | binding.buttonNext.setOnClickListener { pageForward() } | ||
| 134 | binding.buttonBack.setOnClickListener { pageBackward() } | ||
| 135 | |||
| 136 | if (binding.viewPager2.currentItem == 0) { | ||
| 137 | binding.buttonNext.visibility = View.INVISIBLE | ||
| 138 | binding.buttonBack.visibility = View.INVISIBLE | ||
| 139 | } | ||
| 140 | |||
| 141 | setInsets() | ||
| 142 | } | ||
| 143 | |||
| 144 | override fun onDestroyView() { | ||
| 145 | super.onDestroyView() | ||
| 146 | _binding = null | ||
| 147 | } | ||
| 148 | |||
| 149 | private fun finishSetup() { | ||
| 150 | PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit() | ||
| 151 | .putBoolean(Settings.PREF_FIRST_APP_LAUNCH, false) | ||
| 152 | .apply() | ||
| 153 | mainActivity.finishSetup(binding.root.findNavController()) | ||
| 154 | } | ||
| 155 | |||
| 156 | private fun showView(view: View) { | ||
| 157 | if (view.visibility == View.VISIBLE) { | ||
| 158 | return | ||
| 159 | } | ||
| 160 | |||
| 161 | view.apply { | ||
| 162 | alpha = 0f | ||
| 163 | visibility = View.VISIBLE | ||
| 164 | isClickable = true | ||
| 165 | }.animate().apply { | ||
| 166 | duration = 300 | ||
| 167 | alpha(1f) | ||
| 168 | }.start() | ||
| 169 | } | ||
| 170 | |||
| 171 | private fun hideView(view: View) { | ||
| 172 | if (view.visibility == View.GONE) { | ||
| 173 | return | ||
| 174 | } | ||
| 175 | |||
| 176 | view.apply { | ||
| 177 | alpha = 1f | ||
| 178 | isClickable = false | ||
| 179 | }.animate().apply { | ||
| 180 | duration = 300 | ||
| 181 | alpha(0f) | ||
| 182 | }.withEndAction { | ||
| 183 | view.visibility = View.INVISIBLE | ||
| 184 | } | ||
| 185 | } | ||
| 186 | |||
| 187 | private fun pageForward() { | ||
| 188 | binding.viewPager2.currentItem = binding.viewPager2.currentItem + 1 | ||
| 189 | } | ||
| 190 | |||
| 191 | private fun pageBackward() { | ||
| 192 | binding.viewPager2.currentItem = binding.viewPager2.currentItem - 1 | ||
| 193 | } | ||
| 194 | |||
| 195 | private fun setInsets() = | ||
| 196 | ViewCompat.setOnApplyWindowInsetsListener(binding.setupRoot) { view: View, windowInsets: WindowInsetsCompat -> | ||
| 197 | val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) | ||
| 198 | view.setPadding( | ||
| 199 | insets.left, | ||
| 200 | insets.top, | ||
| 201 | insets.right, | ||
| 202 | insets.bottom | ||
| 203 | ) | ||
| 204 | windowInsets | ||
| 205 | } | ||
| 206 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt index b3f4188cd..acda8663a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt | |||
| @@ -11,6 +11,8 @@ class HomeViewModel : ViewModel() { | |||
| 11 | private val _statusBarShadeVisible = MutableLiveData(true) | 11 | private val _statusBarShadeVisible = MutableLiveData(true) |
| 12 | val statusBarShadeVisible: LiveData<Boolean> get() = _statusBarShadeVisible | 12 | val statusBarShadeVisible: LiveData<Boolean> get() = _statusBarShadeVisible |
| 13 | 13 | ||
| 14 | var navigatedToSetup = false | ||
| 15 | |||
| 14 | fun setNavigationVisibility(visible: Boolean) { | 16 | fun setNavigationVisibility(visible: Boolean) { |
| 15 | if (_navigationVisible.value == visible) { | 17 | if (_navigationVisible.value == visible) { |
| 16 | return | 18 | return |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt new file mode 100644 index 000000000..a8a934552 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SetupPage.kt | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.model | ||
| 5 | |||
| 6 | data class SetupPage( | ||
| 7 | val iconId: Int, | ||
| 8 | val titleId: Int, | ||
| 9 | val descriptionId: Int, | ||
| 10 | val buttonIconId: Int, | ||
| 11 | val leftAlignedIcon: Boolean, | ||
| 12 | val buttonTextId: Int, | ||
| 13 | val buttonAction: () -> Unit | ||
| 14 | ) | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt index c6bbc3c65..759ff18fc 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt | |||
| @@ -18,6 +18,7 @@ import androidx.fragment.app.activityViewModels | |||
| 18 | import com.google.android.material.color.MaterialColors | 18 | import com.google.android.material.color.MaterialColors |
| 19 | import com.google.android.material.search.SearchView | 19 | import com.google.android.material.search.SearchView |
| 20 | import com.google.android.material.search.SearchView.TransitionState | 20 | import com.google.android.material.search.SearchView.TransitionState |
| 21 | import com.google.android.material.transition.MaterialFadeThrough | ||
| 21 | import info.debatty.java.stringsimilarity.Jaccard | 22 | import info.debatty.java.stringsimilarity.Jaccard |
| 22 | import org.yuzu.yuzu_emu.R | 23 | import org.yuzu.yuzu_emu.R |
| 23 | import org.yuzu.yuzu_emu.adapters.GameAdapter | 24 | import org.yuzu.yuzu_emu.adapters.GameAdapter |
| @@ -35,6 +36,11 @@ class GamesFragment : Fragment() { | |||
| 35 | private val gamesViewModel: GamesViewModel by activityViewModels() | 36 | private val gamesViewModel: GamesViewModel by activityViewModels() |
| 36 | private val homeViewModel: HomeViewModel by activityViewModels() | 37 | private val homeViewModel: HomeViewModel by activityViewModels() |
| 37 | 38 | ||
| 39 | override fun onCreate(savedInstanceState: Bundle?) { | ||
| 40 | super.onCreate(savedInstanceState) | ||
| 41 | enterTransition = MaterialFadeThrough() | ||
| 42 | } | ||
| 43 | |||
| 38 | override fun onCreateView( | 44 | override fun onCreateView( |
| 39 | inflater: LayoutInflater, | 45 | inflater: LayoutInflater, |
| 40 | container: ViewGroup?, | 46 | container: ViewGroup?, |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index e47866030..b455b7d35 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt | |||
| @@ -3,10 +3,13 @@ | |||
| 3 | 3 | ||
| 4 | package org.yuzu.yuzu_emu.ui.main | 4 | package org.yuzu.yuzu_emu.ui.main |
| 5 | 5 | ||
| 6 | import android.content.Intent | ||
| 6 | import android.os.Bundle | 7 | import android.os.Bundle |
| 7 | import android.view.View | 8 | import android.view.View |
| 8 | import android.view.ViewGroup.MarginLayoutParams | 9 | import android.view.ViewGroup.MarginLayoutParams |
| 9 | import android.view.animation.PathInterpolator | 10 | import android.view.animation.PathInterpolator |
| 11 | import android.widget.Toast | ||
| 12 | import androidx.activity.result.contract.ActivityResultContracts | ||
| 10 | import androidx.activity.viewModels | 13 | import androidx.activity.viewModels |
| 11 | import androidx.appcompat.app.AppCompatActivity | 14 | import androidx.appcompat.app.AppCompatActivity |
| 12 | import androidx.core.content.ContextCompat | 15 | import androidx.core.content.ContextCompat |
| @@ -14,20 +17,33 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen | |||
| 14 | import androidx.core.view.ViewCompat | 17 | import androidx.core.view.ViewCompat |
| 15 | import androidx.core.view.WindowCompat | 18 | import androidx.core.view.WindowCompat |
| 16 | import androidx.core.view.WindowInsetsCompat | 19 | import androidx.core.view.WindowInsetsCompat |
| 20 | import androidx.lifecycle.lifecycleScope | ||
| 21 | import androidx.navigation.NavController | ||
| 17 | import androidx.navigation.fragment.NavHostFragment | 22 | import androidx.navigation.fragment.NavHostFragment |
| 18 | import androidx.navigation.ui.setupWithNavController | 23 | import androidx.navigation.ui.setupWithNavController |
| 24 | import androidx.preference.PreferenceManager | ||
| 19 | import com.google.android.material.color.MaterialColors | 25 | import com.google.android.material.color.MaterialColors |
| 26 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||
| 20 | import com.google.android.material.elevation.ElevationOverlayProvider | 27 | import com.google.android.material.elevation.ElevationOverlayProvider |
| 28 | import kotlinx.coroutines.Dispatchers | ||
| 29 | import kotlinx.coroutines.launch | ||
| 30 | import kotlinx.coroutines.withContext | ||
| 31 | import org.yuzu.yuzu_emu.NativeLibrary | ||
| 21 | import org.yuzu.yuzu_emu.R | 32 | import org.yuzu.yuzu_emu.R |
| 22 | import org.yuzu.yuzu_emu.activities.EmulationActivity | 33 | import org.yuzu.yuzu_emu.activities.EmulationActivity |
| 23 | import org.yuzu.yuzu_emu.databinding.ActivityMainBinding | 34 | import org.yuzu.yuzu_emu.databinding.ActivityMainBinding |
| 35 | import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding | ||
| 36 | import org.yuzu.yuzu_emu.features.settings.model.Settings | ||
| 37 | import org.yuzu.yuzu_emu.model.GamesViewModel | ||
| 24 | import org.yuzu.yuzu_emu.model.HomeViewModel | 38 | import org.yuzu.yuzu_emu.model.HomeViewModel |
| 25 | import org.yuzu.yuzu_emu.utils.* | 39 | import org.yuzu.yuzu_emu.utils.* |
| 40 | import java.io.IOException | ||
| 26 | 41 | ||
| 27 | class MainActivity : AppCompatActivity() { | 42 | class MainActivity : AppCompatActivity() { |
| 28 | private lateinit var binding: ActivityMainBinding | 43 | private lateinit var binding: ActivityMainBinding |
| 29 | 44 | ||
| 30 | private val homeViewModel: HomeViewModel by viewModels() | 45 | private val homeViewModel: HomeViewModel by viewModels() |
| 46 | private val gamesViewModel: GamesViewModel by viewModels() | ||
| 31 | 47 | ||
| 32 | override fun onCreate(savedInstanceState: Bundle?) { | 48 | override fun onCreate(savedInstanceState: Bundle?) { |
| 33 | val splashScreen = installSplashScreen() | 49 | val splashScreen = installSplashScreen() |
| @@ -52,10 +68,9 @@ class MainActivity : AppCompatActivity() { | |||
| 52 | ) | 68 | ) |
| 53 | ) | 69 | ) |
| 54 | 70 | ||
| 55 | // Set up a central host fragment that is controlled via bottom navigation with xml navigation | ||
| 56 | val navHostFragment = | 71 | val navHostFragment = |
| 57 | supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment | 72 | supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment |
| 58 | binding.navigationBar.setupWithNavController(navHostFragment.navController) | 73 | setUpNavigation(navHostFragment.navController) |
| 59 | 74 | ||
| 60 | binding.statusBarShade.setBackgroundColor( | 75 | binding.statusBarShade.setBackgroundColor( |
| 61 | ThemeHelper.getColorWithOpacity( | 76 | ThemeHelper.getColorWithOpacity( |
| @@ -85,6 +100,32 @@ class MainActivity : AppCompatActivity() { | |||
| 85 | setInsets() | 100 | setInsets() |
| 86 | } | 101 | } |
| 87 | 102 | ||
| 103 | fun finishSetup(navController: NavController) { | ||
| 104 | navController.navigate(R.id.action_firstTimeSetupFragment_to_gamesFragment) | ||
| 105 | binding.navigationBar.setupWithNavController(navController) | ||
| 106 | showNavigation(true) | ||
| 107 | |||
| 108 | ThemeHelper.setNavigationBarColor( | ||
| 109 | this, | ||
| 110 | ElevationOverlayProvider(binding.navigationBar.context).compositeOverlay( | ||
| 111 | MaterialColors.getColor(binding.navigationBar, R.attr.colorSurface), | ||
| 112 | binding.navigationBar.elevation | ||
| 113 | ) | ||
| 114 | ) | ||
| 115 | } | ||
| 116 | |||
| 117 | private fun setUpNavigation(navController: NavController) { | ||
| 118 | val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext) | ||
| 119 | .getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true) | ||
| 120 | |||
| 121 | if (firstTimeSetup && !homeViewModel.navigatedToSetup) { | ||
| 122 | navController.navigate(R.id.firstTimeSetupFragment) | ||
| 123 | homeViewModel.navigatedToSetup = true | ||
| 124 | } else { | ||
| 125 | binding.navigationBar.setupWithNavController(navController) | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 88 | private fun showNavigation(visible: Boolean) { | 129 | private fun showNavigation(visible: Boolean) { |
| 89 | binding.navigationBar.animate().apply { | 130 | binding.navigationBar.animate().apply { |
| 90 | if (visible) { | 131 | if (visible) { |
| @@ -138,4 +179,150 @@ class MainActivity : AppCompatActivity() { | |||
| 138 | binding.statusBarShade.layoutParams = mlpShade | 179 | binding.statusBarShade.layoutParams = mlpShade |
| 139 | windowInsets | 180 | windowInsets |
| 140 | } | 181 | } |
| 182 | |||
| 183 | val getGamesDirectory = | ||
| 184 | registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> | ||
| 185 | if (result == null) | ||
| 186 | return@registerForActivityResult | ||
| 187 | |||
| 188 | val takeFlags = | ||
| 189 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||
| 190 | contentResolver.takePersistableUriPermission( | ||
| 191 | result, | ||
| 192 | takeFlags | ||
| 193 | ) | ||
| 194 | |||
| 195 | // When a new directory is picked, we currently will reset the existing games | ||
| 196 | // database. This effectively means that only one game directory is supported. | ||
| 197 | PreferenceManager.getDefaultSharedPreferences(applicationContext).edit() | ||
| 198 | .putString(GameHelper.KEY_GAME_PATH, result.toString()) | ||
| 199 | .apply() | ||
| 200 | |||
| 201 | Toast.makeText( | ||
| 202 | applicationContext, | ||
| 203 | R.string.games_dir_selected, | ||
| 204 | Toast.LENGTH_LONG | ||
| 205 | ).show() | ||
| 206 | |||
| 207 | gamesViewModel.reloadGames(true) | ||
| 208 | } | ||
| 209 | |||
| 210 | val getProdKey = | ||
| 211 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> | ||
| 212 | if (result == null) | ||
| 213 | return@registerForActivityResult | ||
| 214 | |||
| 215 | val takeFlags = | ||
| 216 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||
| 217 | contentResolver.takePersistableUriPermission( | ||
| 218 | result, | ||
| 219 | takeFlags | ||
| 220 | ) | ||
| 221 | |||
| 222 | val dstPath = DirectoryInitialization.userDirectory + "/keys/" | ||
| 223 | if (FileUtil.copyUriToInternalStorage(applicationContext, result, dstPath, "prod.keys")) { | ||
| 224 | if (NativeLibrary.reloadKeys()) { | ||
| 225 | Toast.makeText( | ||
| 226 | applicationContext, | ||
| 227 | R.string.install_keys_success, | ||
| 228 | Toast.LENGTH_SHORT | ||
| 229 | ).show() | ||
| 230 | gamesViewModel.reloadGames(true) | ||
| 231 | } else { | ||
| 232 | Toast.makeText( | ||
| 233 | applicationContext, | ||
| 234 | R.string.install_keys_failure, | ||
| 235 | Toast.LENGTH_LONG | ||
| 236 | ).show() | ||
| 237 | } | ||
| 238 | } | ||
| 239 | } | ||
| 240 | |||
| 241 | val getAmiiboKey = | ||
| 242 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> | ||
| 243 | if (result == null) | ||
| 244 | return@registerForActivityResult | ||
| 245 | |||
| 246 | val takeFlags = | ||
| 247 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||
| 248 | contentResolver.takePersistableUriPermission( | ||
| 249 | result, | ||
| 250 | takeFlags | ||
| 251 | ) | ||
| 252 | |||
| 253 | val dstPath = DirectoryInitialization.userDirectory + "/keys/" | ||
| 254 | if (FileUtil.copyUriToInternalStorage( | ||
| 255 | applicationContext, | ||
| 256 | result, | ||
| 257 | dstPath, | ||
| 258 | "key_retail.bin" | ||
| 259 | ) | ||
| 260 | ) { | ||
| 261 | if (NativeLibrary.reloadKeys()) { | ||
| 262 | Toast.makeText( | ||
| 263 | applicationContext, | ||
| 264 | R.string.install_keys_success, | ||
| 265 | Toast.LENGTH_SHORT | ||
| 266 | ).show() | ||
| 267 | } else { | ||
| 268 | Toast.makeText( | ||
| 269 | applicationContext, | ||
| 270 | R.string.install_amiibo_keys_failure, | ||
| 271 | Toast.LENGTH_LONG | ||
| 272 | ).show() | ||
| 273 | } | ||
| 274 | } | ||
| 275 | } | ||
| 276 | |||
| 277 | val getDriver = | ||
| 278 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> | ||
| 279 | if (result == null) | ||
| 280 | return@registerForActivityResult | ||
| 281 | |||
| 282 | val takeFlags = | ||
| 283 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||
| 284 | contentResolver.takePersistableUriPermission( | ||
| 285 | result, | ||
| 286 | takeFlags | ||
| 287 | ) | ||
| 288 | |||
| 289 | val progressBinding = DialogProgressBarBinding.inflate(layoutInflater) | ||
| 290 | progressBinding.progressBar.isIndeterminate = true | ||
| 291 | val installationDialog = MaterialAlertDialogBuilder(this) | ||
| 292 | .setTitle(R.string.installing_driver) | ||
| 293 | .setView(progressBinding.root) | ||
| 294 | .show() | ||
| 295 | |||
| 296 | lifecycleScope.launch { | ||
| 297 | withContext(Dispatchers.IO) { | ||
| 298 | // Ignore file exceptions when a user selects an invalid zip | ||
| 299 | try { | ||
| 300 | GpuDriverHelper.installCustomDriver(applicationContext, result) | ||
| 301 | } catch (_: IOException) { | ||
| 302 | } | ||
| 303 | |||
| 304 | withContext(Dispatchers.Main) { | ||
| 305 | installationDialog.dismiss() | ||
| 306 | |||
| 307 | val driverName = GpuDriverHelper.customDriverName | ||
| 308 | if (driverName != null) { | ||
| 309 | Toast.makeText( | ||
| 310 | applicationContext, | ||
| 311 | getString( | ||
| 312 | R.string.select_gpu_driver_install_success, | ||
| 313 | driverName | ||
| 314 | ), | ||
| 315 | Toast.LENGTH_SHORT | ||
| 316 | ).show() | ||
| 317 | } else { | ||
| 318 | Toast.makeText( | ||
| 319 | applicationContext, | ||
| 320 | R.string.select_gpu_driver_error, | ||
| 321 | Toast.LENGTH_LONG | ||
| 322 | ).show() | ||
| 323 | } | ||
| 324 | } | ||
| 325 | } | ||
| 326 | } | ||
| 327 | } | ||
| 141 | } | 328 | } |
diff --git a/src/android/app/src/main/res/drawable/ic_arrow_forward.xml b/src/android/app/src/main/res/drawable/ic_arrow_forward.xml new file mode 100644 index 000000000..3b85a3e2c --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_arrow_forward.xml | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | android:width="24dp" | ||
| 3 | android:height="24dp" | ||
| 4 | android:autoMirrored="true" | ||
| 5 | android:viewportWidth="24" | ||
| 6 | android:viewportHeight="24"> | ||
| 7 | <path | ||
| 8 | android:fillColor="?attr/colorControlNormal" | ||
| 9 | android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z" /> | ||
| 10 | </vector> | ||
diff --git a/src/android/app/src/main/res/drawable/ic_check.xml b/src/android/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 000000000..04b89abf2 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_check.xml | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | android:width="24dp" | ||
| 3 | android:height="24dp" | ||
| 4 | android:viewportWidth="24" | ||
| 5 | android:viewportHeight="24"> | ||
| 6 | <path | ||
| 7 | android:fillColor="?attr/colorOnSurface" | ||
| 8 | android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" /> | ||
| 9 | </vector> | ||
diff --git a/src/android/app/src/main/res/drawable/ic_controller.xml b/src/android/app/src/main/res/drawable/ic_controller.xml index 2359c35be..060cd9ae2 100644 --- a/src/android/app/src/main/res/drawable/ic_controller.xml +++ b/src/android/app/src/main/res/drawable/ic_controller.xml | |||
| @@ -4,6 +4,6 @@ | |||
| 4 | android:viewportHeight="24" | 4 | android:viewportHeight="24" |
| 5 | android:viewportWidth="24"> | 5 | android:viewportWidth="24"> |
| 6 | <path | 6 | <path |
| 7 | android:fillColor="?attr/colorControlNormal" | 7 | android:fillColor="?attr/colorOnSurface" |
| 8 | android:pathData="M21,6L3,6c-1.1,0 -2,0.9 -2,2v8c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,8c0,-1.1 -0.9,-2 -2,-2zM11,13L8,13v3L6,16v-3L3,13v-2h3L6,8h2v3h3v2zM15.5,15c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM19.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S18.67,9 19.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z" /> | 8 | android:pathData="M21,6L3,6c-1.1,0 -2,0.9 -2,2v8c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,8c0,-1.1 -0.9,-2 -2,-2zM11,13L8,13v3L6,16v-3L3,13v-2h3L6,8h2v3h3v2zM15.5,15c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM19.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S18.67,9 19.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z" /> |
| 9 | </vector> | 9 | </vector> |
diff --git a/src/android/app/src/main/res/drawable/ic_key.xml b/src/android/app/src/main/res/drawable/ic_key.xml new file mode 100644 index 000000000..a3943634f --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_key.xml | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | android:width="24dp" | ||
| 3 | android:height="24dp" | ||
| 4 | android:viewportWidth="24" | ||
| 5 | android:viewportHeight="24"> | ||
| 6 | <path | ||
| 7 | android:fillColor="?attr/colorOnSurface" | ||
| 8 | android:pathData="M21,10h-8.35C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H13l2,2l2,-2l2,2l4,-4.04L21,10zM7,15c-1.65,0 -3,-1.35 -3,-3c0,-1.65 1.35,-3 3,-3s3,1.35 3,3C10,13.65 8.65,15 7,15z" /> | ||
| 9 | </vector> | ||
diff --git a/src/android/app/src/main/res/drawable/ic_yuzu_title.xml b/src/android/app/src/main/res/drawable/ic_yuzu_title.xml new file mode 100644 index 000000000..b733e5248 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_yuzu_title.xml | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | android:width="340.97dp" | ||
| 3 | android:height="389.85dp" | ||
| 4 | android:viewportWidth="340.97" | ||
| 5 | android:viewportHeight="389.85"> | ||
| 6 | <path | ||
| 7 | android:fillColor="?attr/colorOnSurface" | ||
| 8 | android:pathData="M341,268.68v73c0,14.5 -2.24,25.24 -6.83,32.82 -5.92,10.15 -16.21,15.32 -30.54,15.32S279,384.61 273,374.27c-4.56,-7.64 -6.8,-18.42 -6.8,-32.92V268.68a4.52,4.52 0,0 1,4.51 -4.51H273a4.5,4.5 0,0 1,4.5 4.51v72.5c0,33.53 14.88,37.4 26.07,37.4 12.14,0 26.08,-4.17 26.08,-36.71V268.68a4.52,4.52 0,0 1,4.52 -4.51h2.27A4.5,4.5 0,0 1,341 268.68Z" /> | ||
| 9 | <path | ||
| 10 | android:fillColor="?attr/colorOnSurface" | ||
| 11 | android:pathData="M246.49,389.85H178.6c-2.35,0 -4.72,-1.88 -4.72,-6.08a8.28,8.28 0,0 1,1.33 -4.48l60.33,-104.47H186a4.51,4.51 0,0 1,-4.51 -4.51v-1.58a4.51,4.51 0,0 1,4.48 -4.51h0.8c58.69,-0.11 59.12,0 59.67,0.07a5.19,5.19 0,0 1,4 5.8,8.69 8.69,0 0,1 -1.33,3.76l-60.6,104.77h58a4.51,4.51 0,0 1,4.51 4.51v2.21A4.51,4.51 0,0 1,246.49 389.85Z" /> | ||
| 12 | <path | ||
| 13 | android:fillColor="?attr/colorOnSurface" | ||
| 14 | android:pathData="M73.6,268.68v82.06c0,26 -11.8,38.44 -37.12,39.09h-0.12a4.51,4.51 0,0 1,-4.51 -4.51V383a4.51,4.51 0,0 1,4.48 -4.5c18.49,-0.15 26,-8.23 26,-27.9v-2.37A32.34,32.34 0,0 1,59 351.46c-6.39,5.5 -14.5,8.29 -24.07,8.29C12.09,359.75 0,347.34 0,323.86V268.68a4.52,4.52 0,0 1,4.51 -4.51H6.73a4.52,4.52 0,0 1,4.5 4.51v55c0,7.6 1.82,14.22 5,18.18 3.57,4.56 9.17,6.49 18.75,6.49 10.13,0 17.32,-3.76 22,-11.5 3.61,-5.92 5.43,-13.66 5.43,-23V268.68a4.52,4.52 0,0 1,4.51 -4.51h2.22A4.52,4.52 0,0 1,73.6 268.68Z" /> | ||
| 15 | <path | ||
| 16 | android:fillColor="?attr/colorOnSurface" | ||
| 17 | android:pathData="M163.27,268.68v73c0,14.5 -2.24,25.24 -6.84,32.82 -5.92,10.15 -16.2,15.32 -30.53,15.32s-24.62,-5.23 -30.58,-15.57c-4.56,-7.64 -6.79,-18.42 -6.79,-32.92V268.68A4.51,4.51 0,0 1,93 264.17h2.28a4.51,4.51 0,0 1,4.51 4.51v72.5c0,33.53 14.88,37.4 26.07,37.4 12.14,0 26.08,-4.17 26.08,-36.71V268.68a4.51,4.51 0,0 1,4.51 -4.51h2.27A4.51,4.51 0,0 1,163.27 268.68Z" /> | ||
| 18 | <path | ||
| 19 | android:fillColor="#ff3c28" | ||
| 20 | android:pathData="M181.2,42.83V214.17a85.67,85.67 0,0 0,0 -171.34M197.93,61.6a69,69 0,0 1,0 133.8V61.6" /> | ||
| 21 | <path | ||
| 22 | android:fillColor="#0ab9e6" | ||
| 23 | android:pathData="M159.78,0a85.67,85.67 0,1 0,0 171.33ZM143.05,18.77v133.8A69,69 0,0 1,111 36.92a68.47,68.47 0,0 1,32 -18.15" /> | ||
| 24 | </vector> | ||
diff --git a/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml b/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml new file mode 100644 index 000000000..e05af9bdd --- /dev/null +++ b/src/android/app/src/main/res/layout-w600dp/fragment_setup.xml | |||
| @@ -0,0 +1,38 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <androidx.constraintlayout.widget.ConstraintLayout | ||
| 3 | xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 4 | xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 5 | android:id="@+id/setup_root" | ||
| 6 | android:layout_width="match_parent" | ||
| 7 | android:layout_height="match_parent"> | ||
| 8 | |||
| 9 | <androidx.viewpager2.widget.ViewPager2 | ||
| 10 | android:id="@+id/viewPager2" | ||
| 11 | android:layout_width="0dp" | ||
| 12 | android:layout_height="0dp" | ||
| 13 | app:layout_constraintBottom_toBottomOf="parent" | ||
| 14 | app:layout_constraintEnd_toEndOf="parent" | ||
| 15 | app:layout_constraintStart_toStartOf="parent" | ||
| 16 | app:layout_constraintTop_toTopOf="parent" /> | ||
| 17 | |||
| 18 | <com.google.android.material.button.MaterialButton | ||
| 19 | style="@style/Widget.Material3.Button.TextButton" | ||
| 20 | android:id="@+id/button_next" | ||
| 21 | android:layout_width="wrap_content" | ||
| 22 | android:layout_height="wrap_content" | ||
| 23 | android:layout_margin="16dp" | ||
| 24 | android:text="@string/next" | ||
| 25 | app:layout_constraintBottom_toBottomOf="parent" | ||
| 26 | app:layout_constraintEnd_toEndOf="parent" /> | ||
| 27 | |||
| 28 | <com.google.android.material.button.MaterialButton | ||
| 29 | android:id="@+id/button_back" | ||
| 30 | style="@style/Widget.Material3.Button.TextButton" | ||
| 31 | android:layout_width="wrap_content" | ||
| 32 | android:layout_height="wrap_content" | ||
| 33 | android:layout_margin="16dp" | ||
| 34 | android:text="@string/back" | ||
| 35 | app:layout_constraintBottom_toBottomOf="parent" | ||
| 36 | app:layout_constraintStart_toStartOf="parent" /> | ||
| 37 | |||
| 38 | </androidx.constraintlayout.widget.ConstraintLayout> | ||
diff --git a/src/android/app/src/main/res/layout-w600dp/page_setup.xml b/src/android/app/src/main/res/layout-w600dp/page_setup.xml new file mode 100644 index 000000000..e1c26b2f8 --- /dev/null +++ b/src/android/app/src/main/res/layout-w600dp/page_setup.xml | |||
| @@ -0,0 +1,65 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <LinearLayout | ||
| 3 | xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 4 | xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 5 | xmlns:tools="http://schemas.android.com/tools" | ||
| 6 | android:layout_width="match_parent" | ||
| 7 | android:layout_height="match_parent"> | ||
| 8 | |||
| 9 | <LinearLayout | ||
| 10 | android:layout_width="match_parent" | ||
| 11 | android:layout_height="match_parent" | ||
| 12 | android:orientation="vertical" | ||
| 13 | android:layout_weight="1" | ||
| 14 | android:gravity="center"> | ||
| 15 | |||
| 16 | <ImageView | ||
| 17 | android:id="@+id/icon" | ||
| 18 | android:layout_width="260dp" | ||
| 19 | android:layout_height="260dp" | ||
| 20 | android:layout_gravity="center" /> | ||
| 21 | |||
| 22 | </LinearLayout> | ||
| 23 | |||
| 24 | <LinearLayout | ||
| 25 | android:layout_width="match_parent" | ||
| 26 | android:layout_height="match_parent" | ||
| 27 | android:layout_weight="1" | ||
| 28 | android:orientation="vertical" | ||
| 29 | android:gravity="center"> | ||
| 30 | |||
| 31 | <com.google.android.material.textview.MaterialTextView | ||
| 32 | style="@style/TextAppearance.Material3.DisplaySmall" | ||
| 33 | android:id="@+id/text_title" | ||
| 34 | android:layout_width="match_parent" | ||
| 35 | android:layout_height="wrap_content" | ||
| 36 | android:textAlignment="center" | ||
| 37 | android:textColor="?attr/colorOnSurface" | ||
| 38 | android:textStyle="bold" | ||
| 39 | tools:text="@string/welcome" /> | ||
| 40 | |||
| 41 | <com.google.android.material.textview.MaterialTextView | ||
| 42 | style="@style/TextAppearance.Material3.TitleLarge" | ||
| 43 | android:id="@+id/text_description" | ||
| 44 | android:layout_width="match_parent" | ||
| 45 | android:layout_height="wrap_content" | ||
| 46 | android:layout_marginTop="16dp" | ||
| 47 | android:paddingHorizontal="32dp" | ||
| 48 | android:textAlignment="center" | ||
| 49 | android:textSize="26sp" | ||
| 50 | app:lineHeight="40sp" | ||
| 51 | tools:text="@string/welcome_description" /> | ||
| 52 | |||
| 53 | <com.google.android.material.button.MaterialButton | ||
| 54 | android:id="@+id/button_action" | ||
| 55 | android:layout_width="wrap_content" | ||
| 56 | android:layout_height="56dp" | ||
| 57 | android:layout_marginTop="32dp" | ||
| 58 | android:textSize="20sp" | ||
| 59 | app:iconSize="24sp" | ||
| 60 | app:iconGravity="end" | ||
| 61 | tools:text="Get started" /> | ||
| 62 | |||
| 63 | </LinearLayout> | ||
| 64 | |||
| 65 | </LinearLayout> | ||
diff --git a/src/android/app/src/main/res/layout/activity_main.xml b/src/android/app/src/main/res/layout/activity_main.xml index 68a3eae46..59812ab8e 100644 --- a/src/android/app/src/main/res/layout/activity_main.xml +++ b/src/android/app/src/main/res/layout/activity_main.xml | |||
| @@ -24,10 +24,12 @@ | |||
| 24 | android:id="@+id/navigation_bar" | 24 | android:id="@+id/navigation_bar" |
| 25 | android:layout_width="match_parent" | 25 | android:layout_width="match_parent" |
| 26 | android:layout_height="wrap_content" | 26 | android:layout_height="wrap_content" |
| 27 | android:visibility="invisible" | ||
| 27 | app:layout_constraintBottom_toBottomOf="parent" | 28 | app:layout_constraintBottom_toBottomOf="parent" |
| 28 | app:layout_constraintLeft_toLeftOf="parent" | 29 | app:layout_constraintLeft_toLeftOf="parent" |
| 29 | app:layout_constraintRight_toRightOf="parent" | 30 | app:layout_constraintRight_toRightOf="parent" |
| 30 | app:menu="@menu/menu_navigation" /> | 31 | app:menu="@menu/menu_navigation" |
| 32 | tools:visibility="visible" /> | ||
| 31 | 33 | ||
| 32 | <View | 34 | <View |
| 33 | android:id="@+id/status_bar_shade" | 35 | android:id="@+id/status_bar_shade" |
diff --git a/src/android/app/src/main/res/layout/fragment_setup.xml b/src/android/app/src/main/res/layout/fragment_setup.xml new file mode 100644 index 000000000..6f8993152 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_setup.xml | |||
| @@ -0,0 +1,38 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <androidx.constraintlayout.widget.ConstraintLayout | ||
| 3 | xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 4 | xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 5 | android:id="@+id/setup_root" | ||
| 6 | android:layout_width="match_parent" | ||
| 7 | android:layout_height="match_parent"> | ||
| 8 | |||
| 9 | <androidx.viewpager2.widget.ViewPager2 | ||
| 10 | android:id="@+id/viewPager2" | ||
| 11 | android:layout_width="0dp" | ||
| 12 | android:layout_height="0dp" | ||
| 13 | app:layout_constraintBottom_toBottomOf="parent" | ||
| 14 | app:layout_constraintEnd_toEndOf="parent" | ||
| 15 | app:layout_constraintStart_toStartOf="parent" | ||
| 16 | app:layout_constraintTop_toTopOf="parent" /> | ||
| 17 | |||
| 18 | <com.google.android.material.button.MaterialButton | ||
| 19 | style="@style/Widget.Material3.Button.TextButton" | ||
| 20 | android:id="@+id/button_next" | ||
| 21 | android:layout_width="wrap_content" | ||
| 22 | android:layout_height="wrap_content" | ||
| 23 | android:layout_margin="16dp" | ||
| 24 | android:text="@string/next" | ||
| 25 | app:layout_constraintBottom_toBottomOf="parent" | ||
| 26 | app:layout_constraintEnd_toEndOf="parent" /> | ||
| 27 | |||
| 28 | <com.google.android.material.button.MaterialButton | ||
| 29 | style="@style/Widget.Material3.Button.TextButton" | ||
| 30 | android:id="@+id/button_back" | ||
| 31 | android:layout_width="wrap_content" | ||
| 32 | android:layout_height="wrap_content" | ||
| 33 | android:layout_margin="16dp" | ||
| 34 | android:text="@string/back" | ||
| 35 | app:layout_constraintBottom_toBottomOf="parent" | ||
| 36 | app:layout_constraintStart_toStartOf="parent" /> | ||
| 37 | |||
| 38 | </androidx.constraintlayout.widget.ConstraintLayout> | ||
diff --git a/src/android/app/src/main/res/layout/page_setup.xml b/src/android/app/src/main/res/layout/page_setup.xml new file mode 100644 index 000000000..965019cdb --- /dev/null +++ b/src/android/app/src/main/res/layout/page_setup.xml | |||
| @@ -0,0 +1,52 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <androidx.appcompat.widget.LinearLayoutCompat | ||
| 3 | xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 4 | xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 5 | xmlns:tools="http://schemas.android.com/tools" | ||
| 6 | android:layout_width="match_parent" | ||
| 7 | android:layout_height="match_parent" | ||
| 8 | android:orientation="vertical" | ||
| 9 | android:paddingBottom="64dp"> | ||
| 10 | |||
| 11 | <ImageView | ||
| 12 | android:id="@+id/icon" | ||
| 13 | android:layout_width="220dp" | ||
| 14 | android:layout_height="220dp" | ||
| 15 | android:layout_marginTop="64dp" | ||
| 16 | android:layout_gravity="center" /> | ||
| 17 | |||
| 18 | <com.google.android.material.textview.MaterialTextView | ||
| 19 | style="@style/TextAppearance.Material3.DisplayMedium" | ||
| 20 | android:id="@+id/text_title" | ||
| 21 | android:layout_width="match_parent" | ||
| 22 | android:layout_height="wrap_content" | ||
| 23 | android:layout_marginTop="64dp" | ||
| 24 | android:textAlignment="center" | ||
| 25 | android:textColor="?attr/colorOnSurface" | ||
| 26 | android:textStyle="bold" | ||
| 27 | tools:text="@string/welcome" /> | ||
| 28 | |||
| 29 | <com.google.android.material.textview.MaterialTextView | ||
| 30 | style="@style/TextAppearance.Material3.TitleLarge" | ||
| 31 | android:id="@+id/text_description" | ||
| 32 | android:layout_width="match_parent" | ||
| 33 | android:layout_height="wrap_content" | ||
| 34 | android:layout_marginTop="24dp" | ||
| 35 | android:paddingHorizontal="32dp" | ||
| 36 | android:textAlignment="center" | ||
| 37 | android:textSize="26sp" | ||
| 38 | app:lineHeight="40sp" | ||
| 39 | tools:text="@string/welcome_description" /> | ||
| 40 | |||
| 41 | <com.google.android.material.button.MaterialButton | ||
| 42 | android:id="@+id/button_action" | ||
| 43 | android:layout_width="wrap_content" | ||
| 44 | android:layout_height="56dp" | ||
| 45 | android:layout_marginTop="96dp" | ||
| 46 | android:layout_gravity="center" | ||
| 47 | android:textSize="20sp" | ||
| 48 | app:iconSize="24sp" | ||
| 49 | app:iconGravity="end" | ||
| 50 | tools:text="Get started" /> | ||
| 51 | |||
| 52 | </androidx.appcompat.widget.LinearLayoutCompat> | ||
diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml index e85e24a85..5afa901c2 100644 --- a/src/android/app/src/main/res/navigation/home_navigation.xml +++ b/src/android/app/src/main/res/navigation/home_navigation.xml | |||
| @@ -14,4 +14,13 @@ | |||
| 14 | android:name="org.yuzu.yuzu_emu.fragments.OptionsFragment" | 14 | android:name="org.yuzu.yuzu_emu.fragments.OptionsFragment" |
| 15 | android:label="OptionsFragment" /> | 15 | android:label="OptionsFragment" /> |
| 16 | 16 | ||
| 17 | <fragment | ||
| 18 | android:id="@+id/firstTimeSetupFragment" | ||
| 19 | android:name="org.yuzu.yuzu_emu.fragments.SetupFragment" | ||
| 20 | android:label="FirstTimeSetupFragment" > | ||
| 21 | <action | ||
| 22 | android:id="@+id/action_firstTimeSetupFragment_to_gamesFragment" | ||
| 23 | app:destination="@id/gamesFragment" /> | ||
| 24 | </fragment> | ||
| 25 | |||
| 17 | </navigation> | 26 | </navigation> |
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 564bad081..916f516c0 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml | |||
| @@ -9,12 +9,28 @@ | |||
| 9 | <string name="app_notification_channel_description">yuzu Switch emulator notifications</string> | 9 | <string name="app_notification_channel_description">yuzu Switch emulator notifications</string> |
| 10 | <string name="app_notification_running">yuzu is running</string> | 10 | <string name="app_notification_running">yuzu is running</string> |
| 11 | 11 | ||
| 12 | <!-- Setup strings --> | ||
| 13 | <string name="welcome">Welcome!</string> | ||
| 14 | <string name="welcome_description">Learn how to setup <b>yuzu</b> and jump into emulation.</string> | ||
| 15 | <string name="get_started">Get started</string> | ||
| 16 | <string name="keys">Keys</string> | ||
| 17 | <string name="keys_description">Select your <b>prod.keys</b> file with the button below.</string> | ||
| 18 | <string name="select_keys">Select Keys</string> | ||
| 19 | <string name="games">Games</string> | ||
| 20 | <string name="games_description">Select your <b>Games</b> folder with the button below.</string> | ||
| 21 | <string name="done">Done</string> | ||
| 22 | <string name="done_description">You\'re all set.\nEnjoy your games!</string> | ||
| 23 | <string name="text_continue">Continue</string> | ||
| 24 | <string name="next">Next</string> | ||
| 25 | <string name="back">Back</string> | ||
| 26 | |||
| 12 | <!-- Home strings --> | 27 | <!-- Home strings --> |
| 13 | <string name="home_games">Games</string> | 28 | <string name="home_games">Games</string> |
| 14 | <string name="home_options">Options</string> | 29 | <string name="home_options">Options</string> |
| 15 | <string name="add_games">Add Games</string> | 30 | <string name="add_games">Add Games</string> |
| 16 | <string name="add_games_description">Select your games folder</string> | 31 | <string name="add_games_description">Select your games folder</string> |
| 17 | <string name="home_search_games">Search Games</string> | 32 | <string name="home_search_games">Search Games</string> |
| 33 | <string name="games_dir_selected">Games directory selected</string> | ||
| 18 | <string name="install_prod_keys">Install Prod.keys</string> | 34 | <string name="install_prod_keys">Install Prod.keys</string> |
| 19 | <string name="install_prod_keys_description">Required to decrypt retail games</string> | 35 | <string name="install_prod_keys_description">Required to decrypt retail games</string> |
| 20 | <string name="install_amiibo_keys">Install Amiibo Keys</string> | 36 | <string name="install_amiibo_keys">Install Amiibo Keys</string> |