[android,ui] chore: settings subscreens transition and other minor conformances (#3699)
Some checks are pending
tx-src / sources (push) Waiting to run
Check Strings / check-strings (push) Waiting to run

- Fix black screen in transition animations

- Adjustments to about fragment
made about text more label and less button like, header transparency, spacing adjustments, word Contributors replaced by People in Contributors field for de-duplication.

- installable actions code de-duplication
Extracted install/update/import firmware/user data flows into InstallableActions.kt and reused it from MainActivity and InstallableFragment, reducing duplicated logic, ensuring single source of truth.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3699
Reviewed-by: DraVee <chimera@dravee.dev>
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Co-authored-by: xbzk <xbzk@eden-emu.dev>
Co-committed-by: xbzk <xbzk@eden-emu.dev>
This commit is contained in:
xbzk 2026-03-11 22:47:16 +01:00 committed by crueter
parent 0dad29698e
commit 2896fa3835
No known key found for this signature in database
GPG key ID: 425ACD2D4830EBC6
24 changed files with 1064 additions and 476 deletions

View file

@ -73,6 +73,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
android:theme="@style/Theme.Yuzu.Main"
android:label="@string/preferences_settings"/>
<activity
android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsSubscreenActivity"
android:theme="@style/Theme.Yuzu.Main"
android:label="@string/preferences_settings"/>
<activity
android:name="org.yuzu.yuzu_emu.activities.EmulationActivity"
android:theme="@style/Theme.Yuzu.Main"

View file

@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@ -6,10 +9,10 @@ package org.yuzu.yuzu_emu.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.FragmentActivity
import androidx.navigation.findNavController
import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
@ -67,8 +70,13 @@ class AppletAdapter(val activity: FragmentActivity, applets: List<Applet>) :
title = YuzuApplication.appContext.getString(applet.titleId),
path = appletPath
)
val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame)
binding.root.findNavController().navigate(action)
binding.root.findNavController().navigate(
R.id.action_global_emulationActivity,
bundleOf(
"game" to appletGame,
"custom" to false
)
)
}
}
}

View file

@ -111,18 +111,10 @@ class SettingsActivity : AppCompatActivity() {
if (navHostFragment.childFragmentManager.backStackEntryCount > 0) {
navHostFragment.navController.popBackStack()
} else {
finishWithFragmentLikeAnimation()
finish()
}
}
private fun finishWithFragmentLikeAnimation() {
finish()
overridePendingTransition(
androidx.navigation.ui.R.anim.nav_default_pop_enter_anim,
androidx.navigation.ui.R.anim.nav_default_pop_exit_anim
)
}
override fun onStart() {
super.onStart()
if (!DirectoryInitialization.areDirectoriesReady) {
@ -178,7 +170,7 @@ class SettingsActivity : AppCompatActivity() {
getString(R.string.settings_reset),
Toast.LENGTH_LONG
).show()
finishWithFragmentLikeAnimation()
finish()
}
private fun setInsets() {

View file

@ -0,0 +1,152 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
import android.content.Context
import android.os.Bundle
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.navArgs
import com.google.android.material.color.MaterialColors
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.InsetsHelper
import org.yuzu.yuzu_emu.utils.ThemeHelper
enum class SettingsSubscreen {
PROFILE_MANAGER,
DRIVER_MANAGER,
DRIVER_FETCHER,
FREEDRENO_SETTINGS,
APPLET_LAUNCHER,
INSTALLABLE,
GAME_FOLDERS,
ABOUT,
LICENSES,
GAME_INFO,
ADDONS,
}
class SettingsSubscreenActivity : AppCompatActivity() {
private lateinit var binding: ActivitySettingsBinding
private val args by navArgs<SettingsSubscreenActivityArgs>()
override fun attachBaseContext(base: Context) {
super.attachBaseContext(YuzuApplication.applyLanguage(base))
}
override fun onCreate(savedInstanceState: Bundle?) {
ThemeHelper.setTheme(this)
super.onCreate(savedInstanceState)
binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
if (savedInstanceState == null) {
val navController = navHostFragment.navController
val navGraph = navController.navInflater.inflate(
R.navigation.settings_subscreen_navigation
)
navGraph.setStartDestination(resolveStartDestination())
navController.setGraph(navGraph, createStartDestinationArgs())
}
WindowCompat.setDecorFitsSystemWindows(window, false)
if (InsetsHelper.getSystemGestureType(applicationContext) !=
InsetsHelper.GESTURE_NAVIGATION
) {
binding.navigationBarShade.setBackgroundColor(
ThemeHelper.getColorWithOpacity(
MaterialColors.getColor(
binding.navigationBarShade,
com.google.android.material.R.attr.colorSurface
),
ThemeHelper.SYSTEM_BAR_ALPHA
)
)
}
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() = navigateBack()
}
)
setInsets()
}
override fun onStart() {
super.onStart()
if (!DirectoryInitialization.areDirectoriesReady) {
DirectoryInitialization.start()
}
}
fun navigateBack() {
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
if (!navHostFragment.navController.popBackStack()) {
finish()
}
}
private fun resolveStartDestination(): Int =
when (args.destination) {
SettingsSubscreen.PROFILE_MANAGER -> R.id.profileManagerFragment
SettingsSubscreen.DRIVER_MANAGER -> R.id.driverManagerFragment
SettingsSubscreen.DRIVER_FETCHER -> R.id.driverFetcherFragment
SettingsSubscreen.FREEDRENO_SETTINGS -> R.id.freedrenoSettingsFragment
SettingsSubscreen.APPLET_LAUNCHER -> R.id.appletLauncherFragment
SettingsSubscreen.INSTALLABLE -> R.id.installableFragment
SettingsSubscreen.GAME_FOLDERS -> R.id.gameFoldersFragment
SettingsSubscreen.ABOUT -> R.id.aboutFragment
SettingsSubscreen.LICENSES -> R.id.licensesFragment
SettingsSubscreen.GAME_INFO -> R.id.gameInfoFragment
SettingsSubscreen.ADDONS -> R.id.addonsFragment
}
private fun createStartDestinationArgs(): Bundle =
when (args.destination) {
SettingsSubscreen.DRIVER_MANAGER,
SettingsSubscreen.FREEDRENO_SETTINGS -> bundleOf("game" to args.game)
SettingsSubscreen.GAME_INFO,
SettingsSubscreen.ADDONS -> bundleOf(
"game" to requireNotNull(args.game) {
"Game is required for ${args.destination}"
}
)
else -> Bundle()
}
private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(
binding.navigationBarShade
) { _: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val mlpNavShade = binding.navigationBarShade.layoutParams as MarginLayoutParams
mlpNavShade.height = barInsets.bottom
binding.navigationBarShade.layoutParams = mlpNavShade
windowInsets
}
}
}

View file

@ -21,9 +21,10 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import com.google.android.material.transition.MaterialSharedAxis
import org.yuzu.yuzu_emu.BuildConfig
import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding
import org.yuzu.yuzu_emu.features.settings.ui.SettingsSubscreen
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import org.yuzu.yuzu_emu.NativeLibrary
@ -54,7 +55,7 @@ class AboutFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarAbout.setNavigationOnClickListener {
binding.root.findNavController().popBackStack()
requireActivity().onBackPressedDispatcher.onBackPressed()
}
binding.imageLogo.setOnLongClickListener {
@ -72,8 +73,11 @@ class AboutFragment : Fragment() {
)
}
binding.buttonLicenses.setOnClickListener {
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
binding.root.findNavController().navigate(R.id.action_aboutFragment_to_licensesFragment)
val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
SettingsSubscreen.LICENSES,
null
)
binding.root.findNavController().navigate(action)
}
val buildName = getString(R.string.app_name_suffixed)

View file

@ -15,7 +15,6 @@ import androidx.core.view.updatePadding
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
@ -61,7 +60,7 @@ class AddonsFragment : Fragment() {
homeViewModel.setStatusBarShadeVisibility(false)
binding.toolbarAddons.setNavigationOnClickListener {
binding.root.findNavController().popBackStack()
requireActivity().onBackPressedDispatcher.onBackPressed()
}
binding.toolbarAddons.title = getString(R.string.addons_game, args.game.title)

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@ -12,7 +12,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
import org.yuzu.yuzu_emu.R
@ -50,7 +49,7 @@ class AppletLauncherFragment : Fragment() {
homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarApplets.setNavigationOnClickListener {
binding.root.findNavController().popBackStack()
requireActivity().onBackPressedDispatcher.onBackPressed()
}
val applets = listOf(

View file

@ -13,7 +13,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
@ -142,7 +141,7 @@ class DriverFetcherFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarDrivers.setNavigationOnClickListener {
binding.root.findNavController().popBackStack()
requireActivity().onBackPressedDispatcher.onBackPressed()
}
binding.listDrivers.layoutManager = LinearLayoutManager(context)

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@ -19,6 +19,7 @@ import androidx.navigation.fragment.navArgs
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
import org.yuzu.yuzu_emu.HomeNavigationDirections
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -27,6 +28,7 @@ import org.yuzu.yuzu_emu.adapters.DriverAdapter
import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.features.settings.ui.SettingsSubscreen
import org.yuzu.yuzu_emu.model.Driver.Companion.toDriver
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
@ -105,7 +107,7 @@ class DriverManagerFragment : Fragment() {
}
binding.toolbarDrivers.setNavigationOnClickListener {
binding.root.findNavController().popBackStack()
requireActivity().onBackPressedDispatcher.onBackPressed()
}
binding.buttonInstall.setOnClickListener {
@ -113,9 +115,11 @@ class DriverManagerFragment : Fragment() {
}
binding.buttonFetch.setOnClickListener {
binding.root.findNavController().navigate(
R.id.action_driverManagerFragment_to_driverFetcherFragment
val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
SettingsSubscreen.DRIVER_FETCHER,
null
)
binding.root.findNavController().navigate(action)
}
binding.listDrivers.apply {

View file

@ -4,20 +4,21 @@
package org.yuzu.yuzu_emu.fragments
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.transition.MaterialSharedAxis
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.adapters.FolderAdapter
import org.yuzu.yuzu_emu.databinding.FragmentFoldersBinding
@ -25,7 +26,6 @@ import org.yuzu.yuzu_emu.model.DirectoryType
import org.yuzu.yuzu_emu.model.GameDir
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import org.yuzu.yuzu_emu.utils.collect
@ -36,6 +36,20 @@ class GameFoldersFragment : Fragment() {
private val homeViewModel: HomeViewModel by activityViewModels()
private val gamesViewModel: GamesViewModel by activityViewModels()
private val getGamesDirectory =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
if (result != null) {
processGamesDir(result)
}
}
private val getExternalContentDirectory =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
if (result != null) {
processExternalContentDir(result)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
@ -59,7 +73,7 @@ class GameFoldersFragment : Fragment() {
homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarFolders.setNavigationOnClickListener {
binding.root.findNavController().popBackStack()
requireActivity().onBackPressedDispatcher.onBackPressed()
}
binding.listFolders.apply {
@ -74,7 +88,6 @@ class GameFoldersFragment : Fragment() {
(binding.listFolders.adapter as FolderAdapter).submitList(it)
}
val mainActivity = requireActivity() as MainActivity
binding.buttonAdd.setOnClickListener {
// Show a model to choose between Game and External Content
val options = arrayOf(
@ -87,10 +100,10 @@ class GameFoldersFragment : Fragment() {
.setItems(options) { _, which ->
when (which) {
0 -> { // Game Folder
mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
}
1 -> { // External Content Folder
mainActivity.getExternalContentDirectory.launch(null)
getExternalContentDirectory.launch(null)
}
}
}
@ -105,6 +118,50 @@ class GameFoldersFragment : Fragment() {
gamesViewModel.onCloseGameFoldersFragment()
}
private fun processGamesDir(result: Uri) {
requireContext().contentResolver.takePersistableUriPermission(
result,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
val uriString = result.toString()
val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString }
if (folder != null) {
Toast.makeText(
requireContext().applicationContext,
R.string.folder_already_added,
Toast.LENGTH_SHORT
).show()
return
}
AddGameFolderDialogFragment.newInstance(uriString, calledFromGameFragment = false)
.show(parentFragmentManager, AddGameFolderDialogFragment.TAG)
}
private fun processExternalContentDir(result: Uri) {
requireContext().contentResolver.takePersistableUriPermission(
result,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
val uriString = result.toString()
val folder = gamesViewModel.folders.value.firstOrNull {
it.uriString == uriString && it.type == DirectoryType.EXTERNAL_CONTENT
}
if (folder != null) {
Toast.makeText(
requireContext().applicationContext,
R.string.folder_already_added,
Toast.LENGTH_SHORT
).show()
return
}
val externalContentDir = GameDir(uriString, deepScan = false, DirectoryType.EXTERNAL_CONTENT)
gamesViewModel.addFolder(externalContentDir, savedFromGameFragment = false)
}
private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener(
binding.root

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@ -18,7 +18,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.transition.MaterialSharedAxis
import org.yuzu.yuzu_emu.NativeLibrary
@ -64,7 +63,7 @@ class GameInfoFragment : Fragment() {
binding.apply {
toolbarInfo.title = args.game.title
toolbarInfo.setNavigationOnClickListener {
view.findNavController().popBackStack()
requireActivity().onBackPressedDispatcher.onBackPressed()
}
val pathString = Uri.parse(args.game.path).path ?: ""

View file

@ -35,6 +35,7 @@ import org.yuzu.yuzu_emu.adapters.GamePropertiesAdapter
import org.yuzu.yuzu_emu.databinding.FragmentGamePropertiesBinding
import org.yuzu.yuzu_emu.features.DocumentProvider
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.ui.SettingsSubscreen
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.GameProperty
import org.yuzu.yuzu_emu.model.GamesViewModel
@ -250,8 +251,10 @@ class GamePropertiesFragment : Fragment() {
R.string.info_description,
R.drawable.ic_info_outline,
action = {
val action = GamePropertiesFragmentDirections
.actionPerGamePropertiesFragmentToGameInfoFragment(args.game)
val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
SettingsSubscreen.GAME_INFO,
args.game
)
binding.root.findNavController().navigate(action)
}
)
@ -317,8 +320,11 @@ class GamePropertiesFragment : Fragment() {
R.string.add_ons_description,
R.drawable.ic_edit,
action = {
val action = GamePropertiesFragmentDirections
.actionPerGamePropertiesFragmentToAddonsFragment(args.game)
val action =
HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
SettingsSubscreen.ADDONS,
args.game
)
binding.root.findNavController().navigate(action)
}
)
@ -333,8 +339,11 @@ class GamePropertiesFragment : Fragment() {
R.drawable.ic_build,
detailsFlow = driverViewModel.selectedDriverTitle,
action = {
val action = GamePropertiesFragmentDirections
.actionPerGamePropertiesFragmentToDriverManagerFragment(args.game)
val action =
HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
SettingsSubscreen.DRIVER_MANAGER,
args.game
)
binding.root.findNavController().navigate(action)
}
)
@ -347,8 +356,11 @@ class GamePropertiesFragment : Fragment() {
R.string.freedreno_per_game_description,
R.drawable.ic_graphics,
action = {
val action = GamePropertiesFragmentDirections
.actionPerGamePropertiesFragmentToFreedrenoSettingsFragment(args.game)
val action =
HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
SettingsSubscreen.FREEDRENO_SETTINGS,
args.game
)
binding.root.findNavController().navigate(action)
}
)

View file

@ -36,6 +36,7 @@ import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding
import org.yuzu.yuzu_emu.features.DocumentProvider
import org.yuzu.yuzu_emu.features.fetcher.SpacingItemDecoration
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.ui.SettingsSubscreen
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.HomeSetting
import org.yuzu.yuzu_emu.model.HomeViewModel
@ -126,8 +127,11 @@ class HomeSettingsFragment : Fragment() {
R.string.profile_manager_description,
R.drawable.ic_account_circle,
{
binding.root.findNavController()
.navigate(R.id.action_homeSettingsFragment_to_profileManagerFragment)
val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
SettingsSubscreen.PROFILE_MANAGER,
null
)
binding.root.findNavController().navigate(action)
}
)
)
@ -137,8 +141,10 @@ class HomeSettingsFragment : Fragment() {
R.string.install_gpu_driver_description,
R.drawable.ic_build,
{
val action = HomeSettingsFragmentDirections
.actionHomeSettingsFragmentToDriverManagerFragment(null)
val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
SettingsSubscreen.DRIVER_MANAGER,
null
)
binding.root.findNavController().navigate(action)
},
{ true },
@ -154,7 +160,12 @@ class HomeSettingsFragment : Fragment() {
R.string.gpu_driver_settings,
R.drawable.ic_graphics,
{
binding.root.findNavController().navigate(R.id.freedrenoSettingsFragment)
val action =
HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
SettingsSubscreen.FREEDRENO_SETTINGS,
null
)
binding.root.findNavController().navigate(action)
}
)
)
@ -175,8 +186,11 @@ class HomeSettingsFragment : Fragment() {
R.string.applets_description,
R.drawable.ic_applet,
{
binding.root.findNavController()
.navigate(R.id.action_homeSettingsFragment_to_appletLauncherFragment)
val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
SettingsSubscreen.APPLET_LAUNCHER,
null
)
binding.root.findNavController().navigate(action)
},
{ NativeLibrary.isFirmwareAvailable() },
R.string.applets_error_firmware,
@ -189,8 +203,11 @@ class HomeSettingsFragment : Fragment() {
R.string.manage_yuzu_data_description,
R.drawable.ic_install,
{
binding.root.findNavController()
.navigate(R.id.action_homeSettingsFragment_to_installableFragment)
val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
SettingsSubscreen.INSTALLABLE,
null
)
binding.root.findNavController().navigate(action)
}
)
)
@ -200,8 +217,11 @@ class HomeSettingsFragment : Fragment() {
R.string.select_games_folder_description,
R.drawable.ic_add,
{
binding.root.findNavController()
.navigate(R.id.action_homeSettingsFragment_to_gameFoldersFragment)
val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
SettingsSubscreen.GAME_FOLDERS,
null
)
binding.root.findNavController().navigate(action)
}
)
)
@ -284,9 +304,11 @@ class HomeSettingsFragment : Fragment() {
R.string.about_description,
R.drawable.ic_info_outline,
{
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
parentFragmentManager.primaryNavigationFragment?.findNavController()
?.navigate(R.id.action_homeSettingsFragment_to_aboutFragment)
val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
SettingsSubscreen.ABOUT,
null
)
binding.root.findNavController().navigate(action)
}
)
)

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@ -14,23 +14,23 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.adapters.InstallableAdapter
import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding
import org.yuzu.yuzu_emu.model.AddonViewModel
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.Installable
import org.yuzu.yuzu_emu.model.TaskState
import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.FileUtil
import org.yuzu.yuzu_emu.utils.InstallableActions
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import org.yuzu.yuzu_emu.utils.collect
@ -45,6 +45,9 @@ class InstallableFragment : Fragment() {
private val binding get() = _binding!!
private val homeViewModel: HomeViewModel by activityViewModels()
private val gamesViewModel: GamesViewModel by activityViewModels()
private val addonViewModel: AddonViewModel by activityViewModels()
private val driverViewModel: DriverViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -65,12 +68,10 @@ class InstallableFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val mainActivity = requireActivity() as MainActivity
homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarInstallables.setNavigationOnClickListener {
binding.root.findNavController().popBackStack()
requireActivity().onBackPressedDispatcher.onBackPressed()
}
homeViewModel.openImportSaves.collect(viewLifecycleOwner) {
@ -84,8 +85,8 @@ class InstallableFragment : Fragment() {
Installable(
R.string.user_data,
R.string.user_data_description,
install = { mainActivity.importUserData.launch(arrayOf("application/zip")) },
export = { mainActivity.exportUserData.launch("export.zip") }
install = { importUserDataLauncher.launch(arrayOf("application/zip")) },
export = { exportUserDataLauncher.launch("export.zip") }
),
Installable(
R.string.manage_save_data,
@ -127,27 +128,33 @@ class InstallableFragment : Fragment() {
Installable(
R.string.install_game_content,
R.string.install_game_content_description,
install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) }
install = { installGameUpdateLauncher.launch(arrayOf("*/*")) }
),
Installable(
R.string.install_firmware,
R.string.install_firmware_description,
install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) }
install = { getFirmwareLauncher.launch(arrayOf("application/zip")) }
),
Installable(
R.string.uninstall_firmware,
R.string.uninstall_firmware_description,
install = { mainActivity.uninstallFirmware() }
install = {
InstallableActions.uninstallFirmware(
activity = requireActivity(),
fragmentManager = parentFragmentManager,
homeViewModel = homeViewModel
)
}
),
Installable(
R.string.install_prod_keys,
R.string.install_prod_keys_description,
install = { mainActivity.getProdKey.launch(arrayOf("*/*")) }
install = { getProdKeyLauncher.launch(arrayOf("*/*")) }
),
Installable(
R.string.install_amiibo_keys,
R.string.install_amiibo_keys_description,
install = { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) }
install = { getAmiiboKeyLauncher.launch(arrayOf("*/*")) }
)
)
@ -180,6 +187,132 @@ class InstallableFragment : Fragment() {
windowInsets
}
private val getProdKeyLauncher =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result != null) {
InstallableActions.processKey(
activity = requireActivity(),
fragmentManager = parentFragmentManager,
gamesViewModel = gamesViewModel,
result = result,
extension = "keys"
)
}
}
private val getAmiiboKeyLauncher =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result != null) {
InstallableActions.processKey(
activity = requireActivity(),
fragmentManager = parentFragmentManager,
gamesViewModel = gamesViewModel,
result = result,
extension = "bin"
)
}
}
private val getFirmwareLauncher =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result != null) {
InstallableActions.processFirmware(
activity = requireActivity(),
fragmentManager = parentFragmentManager,
homeViewModel = homeViewModel,
result = result
)
}
}
private val installGameUpdateLauncher =
registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { documents ->
if (documents.isEmpty()) {
return@registerForActivityResult
}
if (addonViewModel.game == null) {
InstallableActions.installContent(
activity = requireActivity(),
fragmentManager = parentFragmentManager,
addonViewModel = addonViewModel,
documents = documents
)
return@registerForActivityResult
}
ProgressDialogFragment.newInstance(
requireActivity(),
R.string.verifying_content,
false
) { _, _ ->
var updatesMatchProgram = true
for (document in documents) {
val valid = NativeLibrary.doesUpdateMatchProgram(
addonViewModel.game!!.programId,
document.toString()
)
if (!valid) {
updatesMatchProgram = false
break
}
}
if (updatesMatchProgram) {
requireActivity().runOnUiThread {
InstallableActions.installContent(
activity = requireActivity(),
fragmentManager = parentFragmentManager,
addonViewModel = addonViewModel,
documents = documents
)
}
} else {
requireActivity().runOnUiThread {
MessageDialogFragment.newInstance(
requireActivity(),
titleId = R.string.content_install_notice,
descriptionId = R.string.content_install_notice_description,
positiveAction = {
InstallableActions.installContent(
activity = requireActivity(),
fragmentManager = parentFragmentManager,
addonViewModel = addonViewModel,
documents = documents
)
},
negativeAction = {}
).show(parentFragmentManager, MessageDialogFragment.TAG)
}
}
return@newInstance Any()
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
}
private val importUserDataLauncher =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result != null) {
InstallableActions.importUserData(
activity = requireActivity(),
fragmentManager = parentFragmentManager,
gamesViewModel = gamesViewModel,
driverViewModel = driverViewModel,
result = result
)
}
}
private val exportUserDataLauncher =
registerForActivityResult(ActivityResultContracts.CreateDocument("application/zip")) { result ->
if (result != null) {
InstallableActions.exportUserData(
activity = requireActivity(),
fragmentManager = parentFragmentManager,
result = result
)
}
}
private val importSaves =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result == null) {

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@ -13,7 +13,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
import org.yuzu.yuzu_emu.R
@ -48,7 +47,7 @@ class LicensesFragment : Fragment() {
homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarLicenses.setNavigationOnClickListener {
binding.root.findNavController().popBackStack()
requireActivity().onBackPressedDispatcher.onBackPressed()
}
val licenses = listOf(

View file

@ -51,7 +51,7 @@ class ProfileManagerFragment : Fragment() {
homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarProfiles.setNavigationOnClickListener {
findNavController().popBackStack()
requireActivity().onBackPressedDispatcher.onBackPressed()
}
setupRecyclerView()

View file

@ -26,7 +26,6 @@ import androidx.preference.PreferenceManager
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.File
import java.io.FilenameFilter
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
@ -39,16 +38,10 @@ import org.yuzu.yuzu_emu.model.AddonViewModel
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.InstallResult
import android.os.Build
import org.yuzu.yuzu_emu.model.TaskState
import org.yuzu.yuzu_emu.model.TaskViewModel
import org.yuzu.yuzu_emu.utils.*
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import androidx.core.content.edit
import org.yuzu.yuzu_emu.activities.EmulationActivity
import kotlin.text.compareTo
@ -453,35 +446,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
fun processKey(result: Uri, extension: String = "keys") {
contentResolver.takePersistableUriPermission(
result,
Intent.FLAG_GRANT_READ_URI_PERMISSION
InstallableActions.processKey(
activity = this,
fragmentManager = supportFragmentManager,
gamesViewModel = gamesViewModel,
result = result,
extension = extension
)
val resultCode: Int = NativeLibrary.installKeys(result.toString(), extension)
if (resultCode == 0) {
// TODO(crueter): It may be worth it to switch some of these Toasts to snackbars,
// since most of it is foreground-only anyways.
Toast.makeText(
applicationContext,
R.string.keys_install_success,
Toast.LENGTH_SHORT
).show()
gamesViewModel.reloadGames(true)
return
}
val resultString: String =
resources.getStringArray(R.array.installKeysResults)[resultCode]
MessageDialogFragment.newInstance(
titleId = R.string.keys_failed,
descriptionString = resultString,
helpLinkId = R.string.keys_missing_help
).show(supportFragmentManager, MessageDialogFragment.TAG)
}
val getFirmware = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
@ -491,75 +462,21 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
fun processFirmware(result: Uri, onComplete: (() -> Unit)? = null) {
val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") }
val firmwarePath =
File(NativeConfig.getNandDir() + "/system/Contents/registered/")
val cacheFirmwareDir = File("${cacheDir.path}/registered/")
ProgressDialogFragment.newInstance(
this,
R.string.firmware_installing
) { progressCallback, _ ->
var messageToShow: Any
try {
FileUtil.unzipToInternalStorage(
result.toString(),
cacheFirmwareDir,
progressCallback
)
val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
MessageDialogFragment.newInstance(
this,
titleId = R.string.firmware_installed_failure,
descriptionId = R.string.firmware_installed_failure_description
)
} else {
firmwarePath.deleteRecursively()
cacheFirmwareDir.copyRecursively(firmwarePath, true)
NativeLibrary.initializeSystem(true)
homeViewModel.setCheckKeys(true)
getString(R.string.save_file_imported_success)
}
} catch (e: Exception) {
Log.error("[MainActivity] Firmware install failed - ${e.message}")
messageToShow = getString(R.string.fatal_error)
} finally {
cacheFirmwareDir.deleteRecursively()
}
messageToShow
}.apply {
onDialogComplete = onComplete
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
InstallableActions.processFirmware(
activity = this,
fragmentManager = supportFragmentManager,
homeViewModel = homeViewModel,
result = result,
onComplete = onComplete
)
}
fun uninstallFirmware() {
val firmwarePath =
File(NativeConfig.getNandDir() + "/system/Contents/registered/")
ProgressDialogFragment.newInstance(
this,
R.string.firmware_uninstalling
) { progressCallback, _ ->
var messageToShow: Any
try {
// Ensure the firmware directory exists before attempting to delete
if (firmwarePath.exists()) {
firmwarePath.deleteRecursively()
// Optionally reinitialize the system or perform other necessary steps
NativeLibrary.initializeSystem(true)
homeViewModel.setCheckKeys(true)
messageToShow = getString(R.string.firmware_uninstalled_success)
} else {
messageToShow = getString(R.string.firmware_uninstalled_failure)
}
} catch (e: Exception) {
Log.error("[MainActivity] Firmware uninstall failed - ${e.message}")
messageToShow = getString(R.string.fatal_error)
}
messageToShow
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
InstallableActions.uninstallFirmware(
activity = this,
fragmentManager = supportFragmentManager,
homeViewModel = homeViewModel
)
}
val installGameUpdate = registerForActivityResult(
@ -606,101 +523,12 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
private fun installContent(documents: List<Uri>) {
ProgressDialogFragment.newInstance(
this@MainActivity,
R.string.installing_game_content
) { progressCallback, messageCallback ->
var installSuccess = 0
var installOverwrite = 0
var errorBaseGame = 0
var error = 0
documents.forEach {
messageCallback.invoke(FileUtil.getFilename(it))
when (
InstallResult.from(
NativeLibrary.installFileToNand(
it.toString(),
progressCallback
)
)
) {
InstallResult.Success -> {
installSuccess += 1
}
InstallResult.Overwrite -> {
installOverwrite += 1
}
InstallResult.BaseInstallAttempted -> {
errorBaseGame += 1
}
InstallResult.Failure -> {
error += 1
}
}
}
addonViewModel.refreshAddons(force = true)
val separator = System.lineSeparator() ?: "\n"
val installResult = StringBuilder()
if (installSuccess > 0) {
installResult.append(
getString(
R.string.install_game_content_success_install,
installSuccess
)
)
installResult.append(separator)
}
if (installOverwrite > 0) {
installResult.append(
getString(
R.string.install_game_content_success_overwrite,
installOverwrite
)
)
installResult.append(separator)
}
val errorTotal: Int = errorBaseGame + error
if (errorTotal > 0) {
installResult.append(separator)
installResult.append(
getString(
R.string.install_game_content_failed_count,
errorTotal
)
)
installResult.append(separator)
if (errorBaseGame > 0) {
installResult.append(separator)
installResult.append(
getString(R.string.install_game_content_failure_base)
)
installResult.append(separator)
}
if (error > 0) {
installResult.append(
getString(R.string.install_game_content_failure_description)
)
installResult.append(separator)
}
return@newInstance MessageDialogFragment.newInstance(
this,
titleId = R.string.install_game_content_failure,
descriptionString = installResult.toString().trim(),
helpLinkId = R.string.install_game_content_help_link
)
} else {
return@newInstance MessageDialogFragment.newInstance(
this,
titleId = R.string.install_game_content_success,
descriptionString = installResult.toString().trim()
)
}
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
InstallableActions.installContent(
activity = this,
fragmentManager = supportFragmentManager,
addonViewModel = addonViewModel,
documents = documents
)
}
val exportUserData = registerForActivityResult(
@ -709,25 +537,11 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
if (result == null) {
return@registerForActivityResult
}
ProgressDialogFragment.newInstance(
this,
R.string.exporting_user_data,
true
) { progressCallback, _ ->
val zipResult = FileUtil.zipFromInternalStorage(
File(DirectoryInitialization.userDirectory!!),
DirectoryInitialization.userDirectory!!,
BufferedOutputStream(contentResolver.openOutputStream(result)),
progressCallback,
compression = false
)
return@newInstance when (zipResult) {
TaskState.Completed -> getString(R.string.user_data_export_success)
TaskState.Failed -> R.string.export_failed
TaskState.Cancelled -> R.string.user_data_export_cancelled
}
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
InstallableActions.exportUserData(
activity = this,
fragmentManager = supportFragmentManager,
result = result
)
}
val importUserData =
@ -735,58 +549,12 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
if (result == null) {
return@registerForActivityResult
}
ProgressDialogFragment.newInstance(
this,
R.string.importing_user_data
) { progressCallback, _ ->
val checkStream =
ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result)))
var isYuzuBackup = false
checkStream.use { stream ->
var ze: ZipEntry? = null
while (stream.nextEntry?.also { ze = it } != null) {
val itemName = ze!!.name.trim()
if (itemName == "/config/config.ini" || itemName == "config/config.ini") {
isYuzuBackup = true
return@use
}
}
}
if (!isYuzuBackup) {
return@newInstance MessageDialogFragment.newInstance(
this,
titleId = R.string.invalid_yuzu_backup,
descriptionId = R.string.user_data_import_failed_description
)
}
// Clear existing user data
NativeConfig.unloadGlobalConfig()
File(DirectoryInitialization.userDirectory!!).deleteRecursively()
// Copy archive to internal storage
try {
FileUtil.unzipToInternalStorage(
result.toString(),
File(DirectoryInitialization.userDirectory!!),
progressCallback
)
} catch (e: Exception) {
return@newInstance MessageDialogFragment.newInstance(
this,
titleId = R.string.import_failed,
descriptionId = R.string.user_data_import_failed_description
)
}
// Reinitialize relevant data
NativeLibrary.initializeSystem(true)
NativeConfig.initializeGlobalConfig()
gamesViewModel.reloadGames(false)
driverViewModel.reloadDriverData()
return@newInstance getString(R.string.user_data_import_success)
}.show(supportFragmentManager, ProgressDialogFragment.TAG)
InstallableActions.importUserData(
activity = this,
fragmentManager = supportFragmentManager,
gamesViewModel = gamesViewModel,
driverViewModel = driverViewModel,
result = result
)
}
}

View file

@ -0,0 +1,327 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.fragments.ProgressDialogFragment
import org.yuzu.yuzu_emu.model.AddonViewModel
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.InstallResult
import org.yuzu.yuzu_emu.model.TaskState
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.File
import java.io.FilenameFilter
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
object InstallableActions {
fun processKey(
activity: FragmentActivity,
fragmentManager: FragmentManager,
gamesViewModel: GamesViewModel,
result: Uri,
extension: String = "keys"
) {
activity.contentResolver.takePersistableUriPermission(
result,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
val resultCode = NativeLibrary.installKeys(result.toString(), extension)
if (resultCode == 0) {
Toast.makeText(
activity.applicationContext,
R.string.keys_install_success,
Toast.LENGTH_SHORT
).show()
gamesViewModel.reloadGames(true)
return
}
val resultString = activity.resources.getStringArray(R.array.installKeysResults)[resultCode]
MessageDialogFragment.newInstance(
titleId = R.string.keys_failed,
descriptionString = resultString,
helpLinkId = R.string.keys_missing_help
).show(fragmentManager, MessageDialogFragment.TAG)
}
fun processFirmware(
activity: FragmentActivity,
fragmentManager: FragmentManager,
homeViewModel: HomeViewModel,
result: Uri,
onComplete: (() -> Unit)? = null
) {
val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") }
val firmwarePath = File(NativeConfig.getNandDir() + "/system/Contents/registered/")
val cacheFirmwareDir = File("${activity.cacheDir.path}/registered/")
ProgressDialogFragment.newInstance(
activity,
R.string.firmware_installing
) { progressCallback, _ ->
var messageToShow: Any
try {
FileUtil.unzipToInternalStorage(
result.toString(),
cacheFirmwareDir,
progressCallback
)
val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
MessageDialogFragment.newInstance(
activity,
titleId = R.string.firmware_installed_failure,
descriptionId = R.string.firmware_installed_failure_description
)
} else {
firmwarePath.deleteRecursively()
cacheFirmwareDir.copyRecursively(firmwarePath, overwrite = true)
NativeLibrary.initializeSystem(true)
homeViewModel.setCheckKeys(true)
activity.getString(R.string.save_file_imported_success)
}
} catch (_: Exception) {
messageToShow = activity.getString(R.string.fatal_error)
} finally {
cacheFirmwareDir.deleteRecursively()
}
messageToShow
}.apply {
onDialogComplete = onComplete
}.show(fragmentManager, ProgressDialogFragment.TAG)
}
fun uninstallFirmware(
activity: FragmentActivity,
fragmentManager: FragmentManager,
homeViewModel: HomeViewModel
) {
val firmwarePath = File(NativeConfig.getNandDir() + "/system/Contents/registered/")
ProgressDialogFragment.newInstance(
activity,
R.string.firmware_uninstalling
) { _, _ ->
val messageToShow: Any = try {
if (firmwarePath.exists()) {
firmwarePath.deleteRecursively()
NativeLibrary.initializeSystem(true)
homeViewModel.setCheckKeys(true)
activity.getString(R.string.firmware_uninstalled_success)
} else {
activity.getString(R.string.firmware_uninstalled_failure)
}
} catch (_: Exception) {
activity.getString(R.string.fatal_error)
}
messageToShow
}.show(fragmentManager, ProgressDialogFragment.TAG)
}
fun installContent(
activity: FragmentActivity,
fragmentManager: FragmentManager,
addonViewModel: AddonViewModel,
documents: List<Uri>
) {
ProgressDialogFragment.newInstance(
activity,
R.string.installing_game_content
) { progressCallback, messageCallback ->
var installSuccess = 0
var installOverwrite = 0
var errorBaseGame = 0
var error = 0
documents.forEach {
messageCallback.invoke(FileUtil.getFilename(it))
when (
InstallResult.from(
NativeLibrary.installFileToNand(
it.toString(),
progressCallback
)
)
) {
InstallResult.Success -> installSuccess += 1
InstallResult.Overwrite -> installOverwrite += 1
InstallResult.BaseInstallAttempted -> errorBaseGame += 1
InstallResult.Failure -> error += 1
}
}
addonViewModel.refreshAddons(force = true)
val separator = System.lineSeparator() ?: "\n"
val installResult = StringBuilder()
if (installSuccess > 0) {
installResult.append(
activity.getString(
R.string.install_game_content_success_install,
installSuccess
)
)
installResult.append(separator)
}
if (installOverwrite > 0) {
installResult.append(
activity.getString(
R.string.install_game_content_success_overwrite,
installOverwrite
)
)
installResult.append(separator)
}
val errorTotal = errorBaseGame + error
if (errorTotal > 0) {
installResult.append(separator)
installResult.append(
activity.getString(
R.string.install_game_content_failed_count,
errorTotal
)
)
installResult.append(separator)
if (errorBaseGame > 0) {
installResult.append(separator)
installResult.append(activity.getString(R.string.install_game_content_failure_base))
installResult.append(separator)
}
if (error > 0) {
installResult.append(
activity.getString(R.string.install_game_content_failure_description)
)
installResult.append(separator)
}
return@newInstance MessageDialogFragment.newInstance(
activity,
titleId = R.string.install_game_content_failure,
descriptionString = installResult.toString().trim(),
helpLinkId = R.string.install_game_content_help_link
)
} else {
return@newInstance MessageDialogFragment.newInstance(
activity,
titleId = R.string.install_game_content_success,
descriptionString = installResult.toString().trim()
)
}
}.show(fragmentManager, ProgressDialogFragment.TAG)
}
fun exportUserData(
activity: FragmentActivity,
fragmentManager: FragmentManager,
result: Uri
) {
val userDirectory = DirectoryInitialization.userDirectory
if (userDirectory == null) {
Toast.makeText(
activity.applicationContext,
R.string.fatal_error,
Toast.LENGTH_SHORT
).show()
return
}
ProgressDialogFragment.newInstance(
activity,
R.string.exporting_user_data,
true
) { progressCallback, _ ->
val zipResult = FileUtil.zipFromInternalStorage(
File(userDirectory),
userDirectory,
BufferedOutputStream(activity.contentResolver.openOutputStream(result)),
progressCallback,
compression = false
)
return@newInstance when (zipResult) {
TaskState.Completed -> activity.getString(R.string.user_data_export_success)
TaskState.Failed -> R.string.export_failed
TaskState.Cancelled -> R.string.user_data_export_cancelled
}
}.show(fragmentManager, ProgressDialogFragment.TAG)
}
fun importUserData(
activity: FragmentActivity,
fragmentManager: FragmentManager,
gamesViewModel: GamesViewModel,
driverViewModel: DriverViewModel,
result: Uri
) {
val userDirectory = DirectoryInitialization.userDirectory
if (userDirectory == null) {
Toast.makeText(
activity.applicationContext,
R.string.fatal_error,
Toast.LENGTH_SHORT
).show()
return
}
ProgressDialogFragment.newInstance(
activity,
R.string.importing_user_data
) { progressCallback, _ ->
val checkStream = ZipInputStream(
BufferedInputStream(activity.contentResolver.openInputStream(result))
)
var isYuzuBackup = false
checkStream.use { stream ->
var ze: ZipEntry? = null
while (stream.nextEntry?.also { ze = it } != null) {
val itemName = ze!!.name.trim()
if (itemName == "/config/config.ini" || itemName == "config/config.ini") {
isYuzuBackup = true
return@use
}
}
}
if (!isYuzuBackup) {
return@newInstance MessageDialogFragment.newInstance(
activity,
titleId = R.string.invalid_yuzu_backup,
descriptionId = R.string.user_data_import_failed_description
)
}
NativeConfig.unloadGlobalConfig()
File(userDirectory).deleteRecursively()
try {
FileUtil.unzipToInternalStorage(
result.toString(),
File(userDirectory),
progressCallback
)
} catch (_: Exception) {
return@newInstance MessageDialogFragment.newInstance(
activity,
titleId = R.string.import_failed,
descriptionId = R.string.user_data_import_failed_description
)
}
NativeLibrary.initializeSystem(true)
NativeConfig.initializeGlobalConfig()
gamesViewModel.reloadGames(false)
driverViewModel.reloadDriverData()
return@newInstance activity.getString(R.string.user_data_import_success)
}.show(fragmentManager, ProgressDialogFragment.TAG)
}
}

View file

@ -10,12 +10,11 @@
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_about"
style="@style/Widget.Eden.TransparentTopAppBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:touchscreenBlocksFocus="false"
android:background="@android:color/transparent"
app:elevation="0dp">
android:touchscreenBlocksFocus="false">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_about"
@ -41,15 +40,41 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:padding="24dp">
android:paddingBottom="24dp"
android:paddingStart="24dp"
android:paddingTop="0dp"
android:paddingEnd="24dp">
<ImageView
android:id="@+id/image_logo"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center_vertical"
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:src="@drawable/ic_yuzu" />
android:gravity="center_horizontal"
android:orientation="vertical">
<ImageView
android:id="@+id/image_logo"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginBottom="8dp"
android:src="@drawable/ic_yuzu" />
<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textAlignment="center" />
<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="220dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/about_app_description"
android:textAlignment="center" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
@ -57,39 +82,6 @@
android:layout_weight="1"
android:orientation="vertical">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="16dp"
app:cardBackgroundColor="?attr/colorSurface"
app:strokeColor="?attr/colorOutline"
app:strokeWidth="1dp"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="24dp"
android:paddingVertical="20dp">
<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/about"
android:textAlignment="viewStart" />
<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/about_app_description"
android:textAlignment="viewStart" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/button_contributors"
android:layout_width="match_parent"
@ -205,7 +197,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginTop="12dp"
android:gravity="start"
android:orientation="horizontal">

View file

@ -10,12 +10,11 @@
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_about"
style="@style/Widget.Eden.TransparentTopAppBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:touchscreenBlocksFocus="false"
android:background="@android:color/transparent"
app:elevation="0dp">
android:touchscreenBlocksFocus="false">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_about"
@ -43,48 +42,35 @@
android:orientation="vertical"
android:paddingBottom="24dp">
<ImageView
android:id="@+id/image_logo"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_marginVertical="24dp"
android:layout_gravity="center_horizontal"
android:src="@drawable/ic_yuzu" />
<com.google.android.material.card.MaterialCardView
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
app:cardBackgroundColor="@android:color/transparent"
app:cardCornerRadius="16dp"
app:cardElevation="0dp"
app:strokeColor="?attr/colorOutline"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal">
<ImageView
android:id="@+id/image_logo"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_marginBottom="8dp"
android:src="@drawable/ic_yuzu" />
<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="20dp"
android:paddingHorizontal="20dp"
android:orientation="vertical">
android:text="@string/app_name"
android:textAlignment="center" />
<com.google.android.material.textview.MaterialTextView
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:text="@string/about" />
<com.google.android.material.textview.MaterialTextView
style="@style/SynthwaveText.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textAlignment="center"
android:text="@string/about_app_description" />
<com.google.android.material.textview.MaterialTextView
style="@style/SynthwaveText.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:textAlignment="viewStart"
android:text="@string/about_app_description" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<com.google.android.material.card.MaterialCardView
android:id="@+id/button_contributors"
@ -206,7 +192,7 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_horizontal"
android:layout_marginTop="24dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="16dp"
android:layout_marginHorizontal="40dp">
@ -220,7 +206,9 @@
app:icon="@drawable/ic_discord"
app:iconSize="24dp"
app:iconGravity="textStart"
app:iconPadding="0dp" />
app:iconPadding="0dp"
app:strokeColor="?attr/colorOutline"
app:strokeWidth="1dp" />
<com.google.android.material.button.MaterialButton
style="@style/EdenButton.Secondary"
@ -232,7 +220,9 @@
app:icon="@drawable/ic_stoat"
app:iconSize="24dp"
app:iconGravity="textStart"
app:iconPadding="0dp" />
app:iconPadding="0dp"
app:strokeColor="?attr/colorOutline"
app:strokeWidth="1dp" />
<com.google.android.material.button.MaterialButton
style="@style/EdenButton.Secondary"
@ -244,7 +234,9 @@
app:icon="@drawable/ic_x"
app:iconSize="24dp"
app:iconGravity="textStart"
app:iconPadding="0dp" />
app:iconPadding="0dp"
app:strokeColor="?attr/colorOutline"
app:strokeWidth="1dp" />
<com.google.android.material.button.MaterialButton
style="@style/EdenButton.Secondary"
@ -256,7 +248,9 @@
app:icon="@drawable/ic_website"
app:iconSize="24dp"
app:iconGravity="textStart"
app:iconPadding="0dp" />
app:iconPadding="0dp"
app:strokeColor="?attr/colorOutline"
app:strokeWidth="1dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_github"
@ -268,7 +262,9 @@
app:icon="@drawable/ic_github"
app:iconSize="24dp"
app:iconGravity="textStart"
app:iconPadding="0dp" />
app:iconPadding="0dp"
app:strokeColor="?attr/colorOutline"
app:strokeWidth="1dp" />
</LinearLayout>

View file

@ -40,10 +40,6 @@
<action
android:id="@+id/action_global_settingsActivity"
app:destination="@id/settingsActivity"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
app:destination="@id/settingsActivity" />
</navigation>

View file

@ -20,26 +20,7 @@
<fragment
android:id="@+id/homeSettingsFragment"
android:name="org.yuzu.yuzu_emu.fragments.HomeSettingsFragment"
android:label="HomeSettingsFragment" >
<action
android:id="@+id/action_homeSettingsFragment_to_aboutFragment"
app:destination="@id/aboutFragment" />
<action
android:id="@+id/action_homeSettingsFragment_to_installableFragment"
app:destination="@id/installableFragment" />
<action
android:id="@+id/action_homeSettingsFragment_to_driverManagerFragment"
app:destination="@id/driverManagerFragment" />
<action
android:id="@+id/action_homeSettingsFragment_to_appletLauncherFragment"
app:destination="@id/appletLauncherFragment" />
<action
android:id="@+id/action_homeSettingsFragment_to_gameFoldersFragment"
app:destination="@id/gameFoldersFragment" />
<action
android:id="@+id/action_homeSettingsFragment_to_profileManagerFragment"
app:destination="@id/profileManagerFragment" />
</fragment>
android:label="HomeSettingsFragment" />
<fragment
android:id="@+id/firstTimeSetupFragment"
@ -55,11 +36,7 @@
<fragment
android:id="@+id/aboutFragment"
android:name="org.yuzu.yuzu_emu.fragments.AboutFragment"
android:label="AboutFragment" >
<action
android:id="@+id/action_aboutFragment_to_licensesFragment"
app:destination="@id/licensesFragment" />
</fragment>
android:label="AboutFragment" />
<fragment
android:id="@+id/licensesFragment"
@ -101,11 +78,23 @@
<action
android:id="@+id/action_global_settingsActivity"
app:destination="@id/settingsActivity"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
app:destination="@id/settingsActivity" />
<activity
android:id="@+id/settingsSubscreenActivity"
android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsSubscreenActivity"
android:label="SettingsSubscreenActivity">
<argument
android:name="destination"
app:argType="org.yuzu.yuzu_emu.features.settings.ui.SettingsSubscreen" />
<argument
android:name="game"
app:argType="org.yuzu.yuzu_emu.model.Game"
app:nullable="true"
android:defaultValue="@null" />
</activity>
<action
android:id="@+id/action_global_settingsSubscreenActivity"
app:destination="@id/settingsSubscreenActivity" />
<fragment
android:id="@+id/installableFragment"
android:name="org.yuzu.yuzu_emu.fragments.InstallableFragment"
@ -119,9 +108,6 @@
app:argType="org.yuzu.yuzu_emu.model.Game"
app:nullable="true"
android:defaultValue="@null" />
<action
android:id="@+id/action_driverManagerFragment_to_driverFetcherFragment"
app:destination="@id/driverFetcherFragment" />
</fragment>
<fragment
android:id="@+id/appletLauncherFragment"

View file

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/settings_subscreen_navigation"
app:startDestination="@id/profileManagerFragment">
<fragment
android:id="@+id/profileManagerFragment"
android:name="org.yuzu.yuzu_emu.fragments.ProfileManagerFragment"
android:label="ProfileManagerFragment">
<action
android:id="@+id/action_profileManagerFragment_to_newUserDialog"
app:destination="@id/newUserDialogFragment" />
</fragment>
<fragment
android:id="@+id/newUserDialogFragment"
android:name="org.yuzu.yuzu_emu.fragments.EditUserDialogFragment"
android:label="NewUserDialogFragment" />
<fragment
android:id="@+id/driverManagerFragment"
android:name="org.yuzu.yuzu_emu.fragments.DriverManagerFragment"
android:label="DriverManagerFragment">
<argument
android:name="game"
app:argType="org.yuzu.yuzu_emu.model.Game"
app:nullable="true"
android:defaultValue="@null" />
</fragment>
<fragment
android:id="@+id/driverFetcherFragment"
android:name="org.yuzu.yuzu_emu.fragments.DriverFetcherFragment"
android:label="fragment_driver_fetcher"
tools:layout="@layout/fragment_driver_fetcher" />
<fragment
android:id="@+id/freedrenoSettingsFragment"
android:name="org.yuzu.yuzu_emu.fragments.FreedrenoSettingsFragment"
android:label="@string/freedreno_settings_title">
<argument
android:name="game"
app:argType="org.yuzu.yuzu_emu.model.Game"
app:nullable="true"
android:defaultValue="@null" />
</fragment>
<fragment
android:id="@+id/appletLauncherFragment"
android:name="org.yuzu.yuzu_emu.fragments.AppletLauncherFragment"
android:label="AppletLauncherFragment">
<action
android:id="@+id/action_appletLauncherFragment_to_cabinetLauncherDialogFragment"
app:destination="@id/cabinetLauncherDialogFragment" />
</fragment>
<dialog
android:id="@+id/cabinetLauncherDialogFragment"
android:name="org.yuzu.yuzu_emu.fragments.CabinetLauncherDialogFragment"
android:label="CabinetLauncherDialogFragment" />
<fragment
android:id="@+id/aboutFragment"
android:name="org.yuzu.yuzu_emu.fragments.AboutFragment"
android:label="AboutFragment" />
<fragment
android:id="@+id/licensesFragment"
android:name="org.yuzu.yuzu_emu.fragments.LicensesFragment"
android:label="LicensesFragment" />
<fragment
android:id="@+id/gameInfoFragment"
android:name="org.yuzu.yuzu_emu.fragments.GameInfoFragment"
android:label="GameInfoFragment">
<argument
android:name="game"
app:argType="org.yuzu.yuzu_emu.model.Game" />
</fragment>
<fragment
android:id="@+id/addonsFragment"
android:name="org.yuzu.yuzu_emu.fragments.AddonsFragment"
android:label="AddonsFragment">
<argument
android:name="game"
app:argType="org.yuzu.yuzu_emu.model.Game" />
</fragment>
<fragment
android:id="@+id/installableFragment"
android:name="org.yuzu.yuzu_emu.fragments.InstallableFragment"
android:label="InstallableFragment" />
<fragment
android:id="@+id/gameFoldersFragment"
android:name="org.yuzu.yuzu_emu.fragments.GameFoldersFragment"
android:label="GameFoldersFragment" />
<activity
android:id="@+id/emulationActivity"
android:name="org.yuzu.yuzu_emu.activities.EmulationActivity"
android:label="EmulationActivity">
<argument
android:name="game"
app:argType="org.yuzu.yuzu_emu.model.Game"
app:nullable="true"
android:defaultValue="@null" />
<argument
android:name="custom"
app:argType="boolean"
android:defaultValue="false" />
</activity>
<action
android:id="@+id/action_global_emulationActivity"
app:destination="@id/emulationActivity"
app:launchSingleTop="true" />
<activity
android:id="@+id/settingsSubscreenActivity"
android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsSubscreenActivity"
android:label="SettingsSubscreenActivity">
<argument
android:name="destination"
app:argType="org.yuzu.yuzu_emu.features.settings.ui.SettingsSubscreen" />
<argument
android:name="game"
app:argType="org.yuzu.yuzu_emu.model.Game"
app:nullable="true"
android:defaultValue="@null" />
</activity>
<action
android:id="@+id/action_global_settingsSubscreenActivity"
app:destination="@id/settingsSubscreenActivity" />
</navigation>

View file

@ -400,7 +400,7 @@
<string name="copied_to_clipboard">Copied to clipboard</string>
<string name="about_app_description">An open-source Switch emulator</string>
<string name="contributors">Contributors</string>
<string name="contributors_description">Contributors who made Eden for Android possible</string>
<string name="contributors_description">People who made Eden for Android possible</string>
<string name="contributors_link" translatable="false">https://git.eden-emu.dev/eden-emu/eden/activity/contributors</string>
<string name="licenses_description">Projects that make Eden for Android possible</string>
<string name="build">Build</string>