Driver Fetcher (#130)

Lets the user download & install drivers from various repositories.

Co-authored-by: swurl <swurl@swurl.xyz>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/130
Co-authored-by: Aleksandr Popovich <alekpopo@pm.me>
Co-committed-by: Aleksandr Popovich <alekpopo@pm.me>
This commit is contained in:
Aleksandr Popovich 2025-06-05 18:56:14 +00:00 committed by crueter
parent 810df8d01c
commit 972576d2c5
45 changed files with 1131 additions and 254 deletions

View file

@ -225,11 +225,14 @@ dependencies {
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("io.coil-kt:coil:2.2.2")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.2")
implementation("androidx.window:window:1.3.0")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("org.commonmark:commonmark:0.22.0")
implementation("androidx.navigation:navigation-fragment-ktx:2.8.9")
implementation("androidx.navigation:navigation-ui-ktx:2.8.9")
implementation("info.debatty:java-string-similarity:2.0.0")

View file

@ -0,0 +1,85 @@
package org.yuzu.yuzu_emu.features.fetcher
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.yuzu.yuzu_emu.databinding.ItemDriverGroupBinding
import org.yuzu.yuzu_emu.fragments.DriverFetcherFragment.DriverGroup
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.transition.AutoTransition
import androidx.transition.ChangeBounds
import androidx.transition.Fade
import androidx.transition.TransitionManager
import androidx.transition.TransitionSet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.model.DriverViewModel
class DriverGroupAdapter(
private val activity: FragmentActivity,
private val driverViewModel: DriverViewModel
) : RecyclerView.Adapter<DriverGroupAdapter.DriverGroupViewHolder>() {
private var driverGroups: List<DriverGroup> = emptyList()
inner class DriverGroupViewHolder(
private val binding: ItemDriverGroupBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(group: DriverGroup) {
val onClick = {
TransitionManager.beginDelayedTransition(
binding.root,
TransitionSet().addTransition(Fade()).addTransition(ChangeBounds())
.setDuration(200)
)
val isVisible = binding.recyclerReleases.isVisible
binding.recyclerReleases.visibility = if (isVisible) View.GONE else View.VISIBLE
binding.imageDropdownArrow.rotation = if (isVisible) 0f else 180f
if (!isVisible && binding.recyclerReleases.adapter == null) {
CoroutineScope(Dispatchers.Main).launch {
binding.recyclerReleases.layoutManager =
LinearLayoutManager(binding.root.context)
binding.recyclerReleases.adapter =
ReleaseAdapter(group.releases, activity, driverViewModel)
binding.recyclerReleases.addItemDecoration(
SpacingItemDecoration(
(activity.resources.displayMetrics.density * 8).toInt()
)
)
}
}
}
binding.textGroupName.text = group.name
binding.textGroupName.setOnClickListener { onClick() }
binding.imageDropdownArrow.setOnClickListener { onClick() }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverGroupViewHolder {
val binding = ItemDriverGroupBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return DriverGroupViewHolder(binding)
}
override fun onBindViewHolder(holder: DriverGroupViewHolder, position: Int) {
holder.bind(driverGroups[position])
}
override fun getItemCount(): Int = driverGroups.size
@SuppressLint("NotifyDataSetChanged")
fun updateDriverGroups(newDriverGroups: List<DriverGroup>) {
driverGroups = newDriverGroups
notifyDataSetChanged()
}
}

View file

@ -0,0 +1,267 @@
package org.yuzu.yuzu_emu.features.fetcher
import android.animation.LayoutTransition
import android.content.res.ColorStateList
import android.text.Html
import android.text.Html.FROM_HTML_MODE_COMPACT
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ItemReleaseBinding
import org.yuzu.yuzu_emu.fragments.DriverFetcherFragment.Release
import androidx.core.net.toUri
import androidx.transition.ChangeBounds
import androidx.transition.Fade
import androidx.transition.TransitionManager
import androidx.transition.TransitionSet
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import org.yuzu.yuzu_emu.databinding.DialogProgressBinding
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.utils.FileUtil
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
class ReleaseAdapter(
private val releases: List<Release>,
private val activity: FragmentActivity,
private val driverViewModel: DriverViewModel
) : RecyclerView.Adapter<ReleaseAdapter.ReleaseViewHolder>() {
inner class ReleaseViewHolder(
private val binding: ItemReleaseBinding
) : RecyclerView.ViewHolder(binding.root) {
private var isPreview: Boolean = true
private val client = OkHttpClient()
private val markdownParser = Parser.builder().build()
private val htmlRenderer = HtmlRenderer.builder().build()
init {
binding.root.let { root ->
val layoutTransition = root.layoutTransition ?: LayoutTransition().apply {
enableTransitionType(LayoutTransition.CHANGING)
setDuration(125)
}
root.layoutTransition = layoutTransition
}
(binding.textBody.parent as ViewGroup).isTransitionGroup = false
binding.containerDownloads.isTransitionGroup = false
}
fun bind(release: Release) {
binding.textReleaseName.text = release.title
binding.badgeLatest.isVisible = release.latest
// truncates to 150 chars so it does not take up too much space.
var bodyPreview = release.body.take(150)
bodyPreview = bodyPreview.replace("#", "").removeSurrounding(" ");
val body =
bodyPreview.replace("\\r\\n", "\n").replace("\\n", "\n").replace("\n", "<br>")
binding.textBody.text = Html.fromHtml(body, FROM_HTML_MODE_COMPACT)
binding.textBody.setOnClickListener {
TransitionManager.beginDelayedTransition(
binding.root,
TransitionSet().addTransition(Fade()).addTransition(ChangeBounds())
.setDuration(100)
)
isPreview = !isPreview
if (isPreview) {
val body = bodyPreview.replace("\\r\\n", "\n").replace("\\n", "\n")
.replace("\n", "<br>")
binding.textBody.text = Html.fromHtml(body, FROM_HTML_MODE_COMPACT)
binding.textBody.maxLines = 3
binding.textBody.ellipsize = TextUtils.TruncateAt.END
} else {
val body = release.body.replace("\\r\\n", "\n\n").replace("\\n", "\n\n")
try {
val doc = markdownParser.parse(body)
val html = htmlRenderer.render(doc)
binding.textBody.text = Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT)
} catch (e: Exception) {
e.printStackTrace()
binding.textBody.text = body
}
binding.textBody.maxLines = Integer.MAX_VALUE
binding.textBody.ellipsize = null
}
}
val onDownloadsClick = {
val isVisible = binding.containerDownloads.isVisible
TransitionManager.beginDelayedTransition(
binding.root,
TransitionSet().addTransition(Fade()).addTransition(ChangeBounds())
.setDuration(100)
)
binding.containerDownloads.isVisible = !isVisible
binding.imageDownloadsArrow.rotation = if (isVisible) 0f else 180f
binding.buttonToggleDownloads.text =
if (isVisible) activity.getString(R.string.show_downloads)
else activity.getString(R.string.hide_downloads)
}
binding.buttonToggleDownloads.setOnClickListener {
onDownloadsClick()
}
binding.imageDownloadsArrow.setOnClickListener {
onDownloadsClick()
}
binding.containerDownloads.removeAllViews()
release.artifacts.forEach { artifact ->
val button = MaterialButton(binding.root.context).apply {
text = artifact.name
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_LabelLarge)
textAlignment = MaterialButton.TEXT_ALIGNMENT_VIEW_START
setBackgroundColor(context.getColor(com.google.android.material.R.color.m3_button_background_color_selector))
setIconResource(R.drawable.ic_import)
iconTint = ColorStateList.valueOf(
MaterialColors.getColor(
this,
com.google.android.material.R.attr.colorPrimary
)
)
elevation = 6f
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
setOnClickListener {
val dialogBinding =
DialogProgressBinding.inflate(LayoutInflater.from(context))
dialogBinding.progressBar.isIndeterminate = true
dialogBinding.title.text = context.getString(R.string.installing_driver)
dialogBinding.status.text = context.getString(R.string.downloading)
val progressDialog = MaterialAlertDialogBuilder(context)
.setView(dialogBinding.root)
.setCancelable(false)
.create()
progressDialog.show()
CoroutineScope(Dispatchers.Main).launch {
try {
val request = Request.Builder()
.url(artifact.url)
.header("Accept", "application/octet-stream")
.build()
val cacheDir = context.externalCacheDir ?: throw IOException(
context.getString(R.string.failed_cache_dir)
)
cacheDir.mkdirs()
val file = File(cacheDir, artifact.name)
withContext(Dispatchers.IO) {
client.newBuilder()
.followRedirects(true)
.followSslRedirects(true)
.build()
.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw IOException("${response.code}")
}
response.body?.byteStream()?.use { input ->
FileOutputStream(file).use { output ->
input.copyTo(output)
}
}
?: throw IOException(context.getString(R.string.empty_response_body))
}
}
if (file.length() == 0L) {
throw IOException(context.getString(R.string.driver_empty))
}
dialogBinding.status.text = context.getString(R.string.installing)
val driverData = GpuDriverHelper.getMetadataFromZip(file)
val driverPath =
"${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename(file.toUri())}"
if (GpuDriverHelper.copyDriverToInternalStorage(file.toUri())) {
driverViewModel.onDriverAdded(Pair(driverPath, driverData))
progressDialog.dismiss()
Toast.makeText(
context,
context.getString(
R.string.successfully_installed,
driverData.name
),
Toast.LENGTH_SHORT
).show()
} else {
throw IOException(
context.getString(
R.string.failed_install_driver,
artifact.name
)
)
}
} catch (e: Exception) {
progressDialog.dismiss()
MaterialAlertDialogBuilder(context)
.setTitle(context.getString(R.string.driver_failed_title))
.setMessage(e.message)
.setPositiveButton(R.string.ok) { dialog, _ ->
dialog.cancel()
}
.show()
}
}
}
}
binding.containerDownloads.addView(button)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReleaseViewHolder {
val binding = ItemReleaseBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ReleaseViewHolder(binding)
}
override fun onBindViewHolder(holder: ReleaseViewHolder, position: Int) {
holder.bind(releases[position])
}
override fun getItemCount(): Int = releases.size
}

View file

@ -0,0 +1,19 @@
package org.yuzu.yuzu_emu.features.fetcher
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class SpacingItemDecoration(private val spacing: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
outRect.bottom = spacing
if (parent.getChildAdapterPosition(view) == 0) {
outRect.top = spacing
}
}
}

View file

@ -33,6 +33,7 @@ object Settings {
YuzuApplication.appContext.getString(R.string.preferences_player, player)
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
const val PREF_SHOULD_SHOW_DRIVER_WARNING = "ShouldShowDriverWarning"
const val PREF_SHOULD_SHOW_PRE_ALPHA_WARNING = "ShouldShowPreAlphaWarning"
const val PREF_SHOULD_SHOW_PRE_ALPHA_BANNER = "ShouldShowPreAlphaBanner"
const val PREF_SHOULD_SHOW_EDENS_VEIL_DIALOG = "ShouldShowEdensVeilDialog"

View file

@ -0,0 +1,307 @@
package org.yuzu.yuzu_emu.fragments
import android.app.AlertDialog
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
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
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.transition.MaterialSharedAxis
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.FragmentDriverFetcherBinding
import org.yuzu.yuzu_emu.features.fetcher.DriverGroupAdapter
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import java.io.IOException
import java.net.URL
import kotlin.getValue
class DriverFetcherFragment : Fragment() {
private var _binding: FragmentDriverFetcherBinding? = null
private val binding get() = _binding!!
private val client = OkHttpClient()
private val gpuModel: String?
get() = GpuDriverHelper.getGpuModel()
private val adrenoModel: Int
get() = parseAdrenoModel()
private val recommendedDriver: String
get() = driverMap.firstOrNull { adrenoModel in it.first }?.second ?: "Unsupported"
private data class DriverRepo(
val name: String = "",
val path: String = "",
val sort: Int = 0,
val useTagName: Boolean = false
)
private val repoList: List<DriverRepo> = listOf(
DriverRepo("Mr. Purple Turnip", "MrPurple666/purple-turnip", 0),
DriverRepo("GameHub Adreno 8xx", "crueter/GameHub-8Elite-Drivers", 1),
DriverRepo("KIMCHI Turnip", "K11MCH1/AdrenoToolsDrivers", 2, true),
DriverRepo("Weab-Chan Freedreno", "Weab-chan/freedreno_turnip-CI", 3),
)
private val driverMap = listOf(
IntRange(Integer.MIN_VALUE, 9) to "Unsupported",
IntRange(10, 99) to "KIMCHI Latest",
IntRange(100, 599) to "Unsupported",
IntRange(600, 639) to "Mr. Purple EOL-24.3.4",
IntRange(640, 699) to "Mr. Purple T19",
IntRange(700, 799) to "Mr. Purple T20", // TODO: Await T21 and update accordingly
IntRange(800, 899) to "GameHub Adreno 8xx",
IntRange(900, Int.MAX_VALUE) to "Unsupported",
)
private lateinit var driverGroupAdapter: DriverGroupAdapter
private val driverViewModel: DriverViewModel by activityViewModels()
fun parseAdrenoModel(): Int {
if (gpuModel == null) {
return 0
}
val modelList = gpuModel!!.split(" ")
// format: Adreno (TM) <ModelNumber>
if (modelList.size < 3 || modelList[0] != "Adreno") {
return 0
}
val model = modelList[2]
try {
// special case for Axx GPUs (e.g. AYANEO Pocket S2)
// driverMap has specific ranges for this
// needs to be fixed
if (model.startsWith("A")) {
return model.substring(1).toInt()
}
return model.toInt()
} catch (e: Exception) {
// Model parse error, just say unsupported
e.printStackTrace()
return 0
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentDriverFetcherBinding.inflate(inflater)
binding.badgeRecommendedDriver.text = recommendedDriver
binding.badgeGpuModel.text = gpuModel
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbarDrivers.setNavigationOnClickListener {
binding.root.findNavController().popBackStack()
}
binding.listDrivers.layoutManager = LinearLayoutManager(context)
driverGroupAdapter = DriverGroupAdapter(requireActivity(), driverViewModel)
binding.listDrivers.adapter = driverGroupAdapter
setInsets()
fetchDrivers()
}
private fun fetchDrivers() {
binding.loadingIndicator.isVisible = true
val driverGroups = arrayListOf<DriverGroup>()
repoList.forEach { driver ->
val name = driver.name
val path = driver.path
val useTagName = driver.useTagName
val sort = driver.sort
CoroutineScope(Dispatchers.Main).launch {
val request = Request.Builder()
.url("https://api.github.com/repos/$path/releases")
.build()
withContext(Dispatchers.IO) {
var releases: ArrayList<Release>
try {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw IOException(response.body.toString())
}
val body = response.body?.string() ?: return@withContext
releases = Release.fromJsonArray(body, useTagName)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
MaterialAlertDialogBuilder(requireActivity().applicationContext)
.setTitle(getString(R.string.error_during_fetch))
.setMessage("${getString(R.string.failed_to_fetch)} ${name}:\n${e.message}")
.setPositiveButton(getString(R.string.ok)) { dialog, _ -> dialog.cancel() }
.show()
releases = ArrayList<Release>()
}
}
val driver = DriverGroup(
name,
releases,
sort
)
synchronized(driverGroups) {
driverGroups.add(driver)
driverGroups.sortBy {
it.sort
}
}
withContext(Dispatchers.Main) {
driverGroupAdapter.updateDriverGroups(driverGroups)
if (driverGroups.size >= repoList.size) {
binding.loadingIndicator.isVisible = false
}
}
}
}
}
}
private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { _: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right
binding.toolbarDrivers.updateMargins(left = leftInsets, right = rightInsets)
binding.listDrivers.updateMargins(left = leftInsets, right = rightInsets)
binding.listDrivers.updatePadding(
bottom = barInsets.bottom +
resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
)
windowInsets
}
data class Artifact(val url: URL, val name: String)
data class Release(
var tagName: String = "",
var title: String = "",
var body: String = "",
var artifacts: List<Artifact> = ArrayList<Artifact>(),
var prerelease: Boolean = false,
var latest: Boolean = false
) {
companion object {
fun fromJsonArray(jsonString: String, useTagName: Boolean): ArrayList<Release> {
val mapper = jacksonObjectMapper()
try {
val rootNode = mapper.readTree(jsonString)
val releases = ArrayList<Release>()
var latestRelease: Release? = null
if (rootNode.isArray) {
rootNode.forEach { node ->
val release = fromJson(node, useTagName)
if (latestRelease == null && !release.prerelease) {
latestRelease = release
release.latest = true
}
releases.add(release)
}
}
return releases
} catch (e: Exception) {
e.printStackTrace()
return ArrayList<Release>()
}
}
fun fromJson(node: JsonNode, useTagName: Boolean): Release {
try {
val tagName = node.get("tag_name").toString().removeSurrounding("\"")
val body = node.get("body").toString().removeSurrounding("\"")
val prerelease = node.get("prerelease").toString().toBoolean()
val title = if (useTagName) tagName else node.get("name").toString().removeSurrounding("\"")
val assets = node.get("assets")
val artifacts = ArrayList<Artifact>()
if (assets?.isArray == true) {
assets.forEach { node ->
val urlStr =
node.get("browser_download_url").toString().removeSurrounding("\"")
val url = URL(urlStr)
val name = node.get("name").toString().removeSurrounding("\"")
val artifact = Artifact(url, name)
artifacts.add(artifact)
}
}
return Release(tagName, title, body, artifacts, prerelease)
} catch (e: Exception) {
// TODO: handle malformed input.
e.printStackTrace()
}
return Release()
}
}
}
data class DriverGroup(
val name: String,
val releases: ArrayList<Release>,
val sort: Int,
)
}

View file

@ -8,6 +8,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.edit
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
@ -15,6 +16,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
import kotlinx.coroutines.Dispatchers
@ -23,6 +25,8 @@ import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.adapters.DriverAdapter
import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.model.Driver.Companion.toDriver
import org.yuzu.yuzu_emu.model.DriverViewModel
@ -103,6 +107,10 @@ class DriverManagerFragment : Fragment() {
getDriver.launch(arrayOf("application/zip"))
}
binding.buttonFetch.setOnClickListener {
binding.root.findNavController().navigate(R.id.action_driverManagerFragment_to_driverFetcherFragment)
}
binding.listDrivers.apply {
layoutManager = GridLayoutManager(
requireContext(),
@ -112,6 +120,10 @@ class DriverManagerFragment : Fragment() {
}
setInsets()
if (!GpuDriverHelper.supportsCustomDriverLoading()) {
showDriverWarningDialog()
}
}
override fun onDestroy() {
@ -139,6 +151,12 @@ class DriverManagerFragment : Fragment() {
bottom = barInsets.bottom + fabSpacing
)
binding.buttonFetch.updateMargins(
left = leftInsets + fabSpacing,
right = rightInsets + fabSpacing,
bottom = barInsets.bottom + fabSpacing
)
binding.listDrivers.updatePadding(
bottom = barInsets.bottom +
resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
@ -195,4 +213,26 @@ class DriverManagerFragment : Fragment() {
return@newInstance Any()
}.show(childFragmentManager, ProgressDialogFragment.TAG)
}
fun showDriverWarningDialog() {
val shouldDisplayGpuWarning =
PreferenceManager.getDefaultSharedPreferences(requireContext())
.getBoolean(Settings.PREF_SHOULD_SHOW_DRIVER_WARNING, true)
if (shouldDisplayGpuWarning) {
MessageDialogFragment.newInstance(
activity,
titleId = R.string.unsupported_gpu,
descriptionId = R.string.unsupported_gpu_warning,
positiveButtonTitleId = R.string.dont_show_again,
negativeButtonTitleId = R.string.close,
showNegativeButton = true,
positiveAction = {
PreferenceManager.getDefaultSharedPreferences(requireContext())
.edit() {
putBoolean(Settings.PREF_SHOULD_SHOW_DRIVER_WARNING, false)
}
}
).show(requireActivity().supportFragmentManager, MessageDialogFragment.TAG)
}
}
}

View file

@ -116,7 +116,7 @@ class HomeSettingsFragment : Fragment() {
.actionHomeSettingsFragmentToDriverManagerFragment(null)
binding.root.findNavController().navigate(action)
},
{ GpuDriverHelper.supportsCustomDriverLoading() },
{true},
R.string.custom_driver_not_supported,
R.string.custom_driver_not_supported_description,
driverViewModel.selectedDriverTitle

View file

@ -455,7 +455,7 @@ class GamesFragment : Fragment() {
val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right
val mlpSwipe = binding.swipeRefresh.layoutParams as ViewGroup.MarginLayoutParams
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR) {
mlpSwipe.leftMargin = leftInsets
mlpSwipe.rightMargin = rightInsets
} else {

View file

@ -25,13 +25,10 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
import com.google.android.material.color.MaterialColors
import com.google.android.material.navigation.NavigationBarView
import java.io.File
import java.io.FilenameFilter
import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding

View file

@ -24,6 +24,7 @@ import java.nio.charset.StandardCharsets
import java.util.zip.Deflater
import java.util.zip.ZipOutputStream
import kotlin.IllegalStateException
import androidx.core.net.toUri
object FileUtil {
const val PATH_TREE = "tree"
@ -195,6 +196,10 @@ object FileUtil {
* @return String display name
*/
fun getFilename(uri: Uri): String {
if (uri.scheme == "file") {
return uri.lastPathSegment?.takeIf { it.isNotEmpty() } ?: throw IOException("Invalid file URI: $uri")
}
val resolver = YuzuApplication.appContext.contentResolver
val columns = arrayOf(
DocumentsContract.Document.COLUMN_DISPLAY_NAME
@ -236,7 +241,7 @@ object FileUtil {
var size: Long = 0
var c: Cursor? = null
try {
val mUri = Uri.parse(path)
val mUri = path.toUri()
c = resolver.query(mUri, columns, null, null, null)
c!!.moveToNext()
size = c.getLong(0)

View file

@ -202,6 +202,11 @@ object GpuDriverHelper {
hookLibPath: String = GpuDriverHelper.hookLibPath!!
): Array<String>?
external fun getGpuModel(
surface: Surface = Surface(SurfaceTexture(true)),
hookLibPath: String = GpuDriverHelper.hookLibPath!!
): String?
// Parse the custom driver metadata to retrieve the name.
val installedCustomDriverData: GpuDriverMetadata
get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME))

View file

@ -567,6 +567,30 @@ jobjectArray Java_org_yuzu_yuzu_1emu_utils_GpuDriverHelper_getSystemDriverInfo(
return j_driver_info;
}
jstring Java_org_yuzu_yuzu_1emu_utils_GpuDriverHelper_getGpuModel(JNIEnv *env, jobject j_obj, jobject j_surf, jstring j_hook_lib_dir) {
const char* file_redirect_dir_{};
int featureFlags{};
std::string hook_lib_dir = Common::Android::GetJString(env, j_hook_lib_dir);
auto handle = adrenotools_open_libvulkan(RTLD_NOW, featureFlags, nullptr, hook_lib_dir.c_str(),
nullptr, nullptr, file_redirect_dir_, nullptr);
auto driver_library = std::make_shared<Common::DynamicLibrary>(handle);
InputCommon::InputSubsystem input_subsystem;
auto window =
std::make_unique<EmuWindow_Android>(ANativeWindow_fromSurface(env, j_surf), driver_library);
Vulkan::vk::InstanceDispatch dld;
Vulkan::vk::Instance vk_instance = Vulkan::CreateInstance(
*driver_library, dld, VK_API_VERSION_1_1, Core::Frontend::WindowSystemType::Android);
auto surface = Vulkan::CreateSurface(vk_instance, window->GetWindowInfo());
auto device = Vulkan::CreateDevice(vk_instance, dld, *surface);
const std::string model_name{device.GetModelName()};
return Common::Android::ToJString(env, model_name);
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_reloadKeys(JNIEnv* env, jclass clazz) {
Core::Crypto::KeyManager::Instance().ReloadKeys();
return static_cast<jboolean>(Core::Crypto::KeyManager::Instance().AreKeysLoaded());

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M7,10l5,5 5,-5" />
</vector>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?attr/colorSurface" />
<stroke
android:width="1dp"
android:color="?attr/colorOutline" />
<corners android:radius="8dp" />
<padding
android:left="12dp"
android:top="12dp"
android:right="12dp"
android:bottom="12dp" />
</shape>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?attr/colorSurfaceVariant" />
<corners android:radius="8dp" />
<padding
android:left="8dp"
android:right="8dp"
android:top="0dp"
android:bottom="0dp" />
</shape>

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dialog_progress_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:textAlignment="center"
android:textAppearance="?attr/textAppearanceHeadline6"
android:textColor="?attr/colorPrimary" />
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textAppearance="?attr/textAppearanceBody1"
android:textColor="?attr/colorOnSurface" />
</LinearLayout>

View file

@ -0,0 +1,116 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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/coordinator_licenses"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_drivers"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:touchscreenBlocksFocus="false"
app:layout_constraintTop_toTopOf="parent"
app:liftOnScrollTargetViewId="@id/list_drivers">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_drivers"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:touchscreenBlocksFocus="false"
app:navigationIcon="@drawable/ic_back"
app:title="@string/gpu_driver_fetcher" />
</com.google.android.material.appbar.AppBarLayout>
<TextView
android:id="@+id/label_gpu_model"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:padding="8dp"
android:text="@string/gpu_model"
android:textSize="16sp"
android:textStyle="bold"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar_drivers" />
<TextView
android:id="@+id/badge_gpu_model"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:background="@drawable/item_release_latest_badge_background"
android:textColor="?attr/colorPrimary"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="@id/label_gpu_model"
app:layout_constraintStart_toEndOf="@id/label_gpu_model"
app:layout_constraintTop_toTopOf="@id/label_gpu_model" />
<TextView
android:id="@+id/label_recommended_driver"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:padding="8dp"
android:text="@string/recommended_driver"
android:textSize="16sp"
android:textStyle="bold"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_gpu_model" />
<TextView
android:id="@+id/badge_recommended_driver"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:background="@drawable/item_release_latest_badge_background"
android:textColor="?attr/colorPrimary"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="@id/label_recommended_driver"
app:layout_constraintStart_toEndOf="@id/label_recommended_driver"
app:layout_constraintTop_toTopOf="@id/label_recommended_driver" />
<View
android:id="@+id/divider"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:background="?attr/colorOutline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_recommended_driver" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_drivers"
android:layout_height="0dp"
android:layout_width="match_parent"
android:layout_marginTop="8dp"
android:clipToPadding="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintTop_toBottomOf="@id/divider"
app:layout_constraintBottom_toBottomOf="parent" />
<ProgressBar
android:id="@+id/loadingIndicator"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="@id/list_drivers"
app:layout_constraintStart_toStartOf="@id/list_drivers"
app:layout_constraintTop_toTopOf="@id/list_drivers" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -7,6 +7,7 @@
android:background="?attr/colorSurface">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -47,4 +48,14 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/button_fetch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:text="@string/fetch"
app:icon="@drawable/ic_import"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/text_group_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="?attr/colorControlNormal"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/image_dropdown_arrow" />
<ImageView
android:id="@+id/image_dropdown_arrow"
android:layout_width="32dp"
android:layout_height="24dp"
android:src="@drawable/ic_dropdown_arrow"
app:tint="?attr/colorControlNormal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="@string/show_releases" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_releases"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/text_group_name"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="8dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/item_release_box"
android:padding="8dp"
android:paddingStart="12dp"
tools:ignore="RtlSymmetry">
<TextView
android:id="@+id/text_release_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/badge_latest"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/latest"
android:textColor="?attr/colorPrimary"
android:textSize="14sp"
android:background="@drawable/item_release_latest_badge_background"
app:layout_constraintStart_toEndOf="@id/text_release_name"
app:layout_constraintTop_toTopOf="@id/text_release_name"
app:layout_constraintBottom_toBottomOf="@id/text_release_name"
android:layout_marginStart="8dp" />
<TextView
android:id="@+id/text_body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:maxLines="3"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_release_name" />
<TextView
android:id="@+id/button_toggle_downloads"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/show_downloads"
android:textStyle="bold"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_body"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp"/>
<ImageView
android:id="@+id/image_downloads_arrow"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="8dp"
android:contentDescription="@string/show_downloads"
android:src="@drawable/ic_dropdown_arrow"
app:layout_constraintBottom_toBottomOf="@id/button_toggle_downloads"
app:layout_constraintStart_toEndOf="@id/button_toggle_downloads"
app:layout_constraintTop_toTopOf="@id/button_toggle_downloads"
app:tint="?attr/colorControlNormal" />
<LinearLayout
android:id="@+id/container_downloads"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/button_toggle_downloads"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="8dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,6 +1,7 @@
<?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/home_navigation"
app:startDestination="@id/gamesFragment">
@ -111,6 +112,9 @@
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"
@ -164,5 +168,10 @@
android:name="game"
app:argType="org.yuzu.yuzu_emu.model.Game" />
</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" />
</navigation>

View file

@ -365,17 +365,6 @@
<string name="user_data_import_success">تم استيراد بيانات المستخدم بنجاح</string>
<string name="user_data_export_cancelled">تم إلغاء التصدير</string>
<!-- Early access upgrade strings -->
<string name="early_access">الوصول المبكر</string>
<string name="early_access_benefits">مزايا الوصول المبكر</string>
<string name="cutting_edge_features">ميزات متطورة</string>
<string name="early_access_updates">الوصول المبكر إلى التحديثات</string>
<string name="no_manual_installation">لا يوجد التثبيت اليدوي</string>
<string name="prioritized_support">الدعم ذو الأولوية</string>
<string name="helping_game_preservation">المساعدة في الحفاظ على اللعبة</string>
<string name="our_eternal_gratitude">امتناننا الأبدي</string>
<string name="are_you_interested">هل انت مهتم؟</string>
<!-- General settings strings -->
<string name="frame_limit_enable">الحد من السرعة</string>
<string name="frame_limit_enable_description">يحد من سرعة المحاكاة بنسبة محددة من السرعة العادية</string>

View file

@ -334,17 +334,6 @@
<string name="licenses_description">ئەو پڕۆژانەی کە یوزوی بۆ ئەندرۆید ڕەخساند</string>
<string name="build">بونیات</string>
<!-- Early access upgrade strings -->
<string name="early_access">بەزوویی دەسپێگەشتن</string>
<string name="early_access_benefits">سوودەکانی بەزوویی دەسپێگەشتن</string>
<string name="cutting_edge_features">تایبەتمەندییە پێشکەوتووەکان</string>
<string name="early_access_updates">زوو دەستگەیشتن بە نوێکارییەکان</string>
<string name="no_manual_installation">چیتر دامەزراندنی دەستی نییە</string>
<string name="prioritized_support">پشتگیری لە پێشینە</string>
<string name="helping_game_preservation">یارمەتیدانی پاراستنی یارییەکان</string>
<string name="our_eternal_gratitude">سوپاس و پێزانینی هەمیشەییمان</string>
<string name="are_you_interested">ئایا تۆ خوازیاریت؟</string>
<!-- General settings strings -->
<string name="frame_limit_enable">سنووردارکردنی خێرایی</string>
<string name="frame_limit_enable_description">خێرایی ئیمولەیشن سنووردار دەکات بۆ ڕێژەیەکی دیاریکراو لە خێرایی ئاسایی.</string>

View file

@ -326,10 +326,6 @@
<string name="user_data_import_success">Uživatelská data byla úspěšně importována.</string>
<string name="user_data_export_cancelled">Export zrušen</string>
<string name="no_manual_installation">Žádná manuální instalace</string>
<string name="prioritized_support">Prioritní podpora</string>
<string name="our_eternal_gratitude">Naše věčná vděčnost</string>
<string name="are_you_interested">Máte zájem?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">Omezit rychlost</string>

View file

@ -358,17 +358,6 @@ Wirklich fortfahren?</string>
<string name="user_data_import_success">Nutzerdaten erfolgreich importiert</string>
<string name="user_data_export_cancelled">Export abgebrochen</string>
<!-- Early access upgrade strings -->
<string name="early_access">Early Access</string>
<string name="early_access_benefits">Early Access Vorteile</string>
<string name="cutting_edge_features">Neueste Features</string>
<string name="early_access_updates">Früherer Zugriff auf Updates</string>
<string name="no_manual_installation">Keine manuelle Installation</string>
<string name="prioritized_support">Priorisierte Unterstützung</string>
<string name="helping_game_preservation">Beitrag zur Erhaltung der Spiele</string>
<string name="our_eternal_gratitude">Unsere ewige Dankbarkeit</string>
<string name="are_you_interested">Bist du interessiert?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">Limitierte Geschwindigkeit</string>
<string name="frame_limit_enable_description">Limitiert die Geschwindigkeit auf einen von dir festgelegten Prozentsatz.</string>

View file

@ -390,17 +390,6 @@
<string name="user_data_export_cancelled">Exportación cancelada</string>
<string name="user_data_import_failed_description">Asegúrese de que las carpetas de datos de usuario estén en la raíz de la carpeta del zip y contengan un archivo config en config/config.ini e inténtelo de nuevo.</string>
<!-- Early access upgrade strings -->
<string name="early_access">Early Access</string>
<string name="early_access_benefits">Beneficios Early Access</string>
<string name="cutting_edge_features">Características de vanguardia</string>
<string name="early_access_updates">Acceso anticipado a las actualizaciones</string>
<string name="no_manual_installation">Sin instalación manual</string>
<string name="prioritized_support">Soporte prioritario</string>
<string name="helping_game_preservation">Ayudarás a la preservación de juegos</string>
<string name="our_eternal_gratitude">Nuestra eterna gratitud</string>
<string name="are_you_interested">¿Estás interesado?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">Limitar velocidad</string>
<string name="frame_limit_enable_description">Limita la velocidad de emulación a un porcentaje específico de la velocidad normal.</string>

View file

@ -388,17 +388,6 @@
<string name="user_data_export_cancelled">صدور لغو شد</string>
<string name="user_data_import_failed_description">مطمئن شوید که پوشه‌های داده کاربر در ریشه پوشه zip و حاوی یک فایل پیکربندی در config/config.ini هستند سپس دوباره امتحان کنید.</string>
<!-- Early access upgrade strings -->
<string name="early_access">دسترسی زودهنگام</string>
<string name="early_access_benefits">مزایای دسترسی زودهنگام</string>
<string name="cutting_edge_features">ویژگی‌های پیشرفته</string>
<string name="early_access_updates">دسترسی زودهنگام به بروزرسانی‌ها</string>
<string name="no_manual_installation">بدون نصب دستی</string>
<string name="prioritized_support">پشتیبانی اولویت بندی شده</string>
<string name="helping_game_preservation">کمک به حفظ بازی</string>
<string name="our_eternal_gratitude">سپاس ابدی ما</string>
<string name="are_you_interested">مشتاق هستید؟</string>
<!-- General settings strings -->
<string name="frame_limit_enable">محدودیت سرعت</string>
<string name="frame_limit_enable_description">سرعت شبیه‌سازی را به درصد مشخصی از سرعت عادی محدود می‌کند.</string>

View file

@ -390,17 +390,6 @@
<string name="user_data_export_cancelled">Exportation annulée</string>
<string name="user_data_import_failed_description">Assurez-vous que les dossiers de données utilisateur se trouvent à la racine du dossier ZIP et contiennent un fichier de configuration à config/config.ini, puis réessayez.</string>
<!-- Early access upgrade strings -->
<string name="early_access">Early Access</string>
<string name="early_access_benefits">Avantages de l\'Early Access</string>
<string name="cutting_edge_features">Fonctionnalités de pointe</string>
<string name="early_access_updates">Accès anticipé aux mises à jour</string>
<string name="no_manual_installation">Pas d\'installation manuelle</string>
<string name="prioritized_support">Assistance prioritaire</string>
<string name="helping_game_preservation">Contribuer à la préservation des jeux</string>
<string name="our_eternal_gratitude">Notre gratitude éternelle</string>
<string name="are_you_interested">Es tu intéressé ?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">Limiter la vitesse</string>
<string name="frame_limit_enable_description">Limiter la vitesse d\'émulation à un pourcentage spécifié de la vitesse normale</string>

View file

@ -376,17 +376,6 @@
<string name="user_data_export_cancelled">ייצוא בוטל</string>
<string name="user_data_import_failed_description">ודא שנתוני המשתמש נמצאים בשורש קובץ ה zip ושהוא מכיל קובץ סידור ב config/config.ini ונסה שוב.</string>
<!-- Early access upgrade strings -->
<string name="early_access">גישה מוקדמת</string>
<string name="early_access_benefits">יתרונות של גישה מקודמת</string>
<string name="cutting_edge_features">תכונות חותכות קצה</string>
<string name="early_access_updates">גישה מוקדמת לעדכונים</string>
<string name="no_manual_installation">ללא התקנה ידנית</string>
<string name="prioritized_support">תמיכה בעדיפות</string>
<string name="helping_game_preservation">עוזר בשמירת משחקים</string>
<string name="our_eternal_gratitude">התודה האינסופית שלנו</string>
<string name="are_you_interested">אתה מעוניין?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">הגבל מהירות</string>
<string name="frame_limit_enable_description">מגביל את מהירות האמולציה לאחוז מהירות המבוקש מהמהירות הרגילה.</string>

View file

@ -385,16 +385,6 @@
<string name="user_data_export_cancelled">Exportálás megszakítva</string>
<string name="user_data_import_failed_description">Ellenőrizd, hogy a felhasználói adatok mappái a zip mappa gyökerében vannak, és tartalmaznak egy konfig fájlt a config/config.ini címen, majd próbáld meg újra.</string>
<!-- Early access upgrade strings -->
<string name="early_access">Korai hozzáférés</string>
<string name="early_access_benefits">Korai hozzáférés előnyei</string>
<string name="cutting_edge_features">Legújabb funkciók</string>
<string name="early_access_updates">Korai hozzáférés a frissítésekhez</string>
<string name="no_manual_installation">Automatikus telepítések</string>
<string name="prioritized_support">Priorizált támogatás</string>
<string name="our_eternal_gratitude">Valamint az örök hálánk</string>
<string name="are_you_interested">Érdekel a dolog?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">Sebességkorlát</string>
<string name="frame_limit_enable_description">Korlátozza az emuláció sebességét a normál sebesség adott százalékára.</string>

View file

@ -386,17 +386,6 @@
<string name="user_data_export_cancelled">Ekspor Dibatalkan</string>
<string name="user_data_import_failed_description">Pastikan folder data pengguna berada di akar folder zip dan berisi file konfigurasi di config/config.ini dan coba lagi.</string>
<!-- Early access upgrade strings -->
<string name="early_access">Akses lebih awal</string>
<string name="early_access_benefits">Manfaat Akses Awal</string>
<string name="cutting_edge_features">Fitur-fitur yang paling baru</string>
<string name="early_access_updates">Akses lebih awal untuk pembaruan</string>
<string name="no_manual_installation">Tidak ada instalasi manual</string>
<string name="prioritized_support">Dukungan yang diprioritaskan</string>
<string name="helping_game_preservation">Membantu pemeliharaan game</string>
<string name="our_eternal_gratitude">Ucapan terima kasih kami yang tak terhingga</string>
<string name="are_you_interested">Apa kamu tertarik?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">Batas kecepatan</string>
<string name="frame_limit_enable_description">Membatasi kecepatan emulasi ke persentase tertentu dari kecepatan normal</string>

View file

@ -378,17 +378,6 @@
<string name="user_data_export_cancelled">Esportazione annullata</string>
<string name="user_data_import_failed_description">Assicurati che la cartella dei Dati dell\'utente stiano nella radice del file.zip e che sia presente una cartella config in config/config.ini, poi, riprova.</string>
<!-- Early access upgrade strings -->
<string name="early_access">Accesso Anticipato</string>
<string name="early_access_benefits">Vantaggi dell\'accesso anticipato</string>
<string name="cutting_edge_features">Funzionalità all\'avanguardia</string>
<string name="early_access_updates">Accesso anticipato agli aggiornamenti</string>
<string name="no_manual_installation">Non installare manualmente.</string>
<string name="prioritized_support">Supporto prioritario</string>
<string name="helping_game_preservation">Aiuta a preservare il gioco</string>
<string name="our_eternal_gratitude">La nostra gratitudine eterna</string>
<string name="are_you_interested">Sei interessato?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">Limita velocità</string>
<string name="frame_limit_enable_description">Limita la velocità dell\'emulazione a una specifica percentuale della velocità normale.</string>

View file

@ -357,17 +357,6 @@
<string name="user_data_import_success">ユーザデータのインポートに成功しました</string>
<string name="user_data_export_cancelled">エクスポートをキャンセルしました</string>
<!-- Early access upgrade strings -->
<string name="early_access">早期アクセス</string>
<string name="early_access_benefits">早期アクセスのメリット</string>
<string name="cutting_edge_features">最先端の機能</string>
<string name="early_access_updates">アップデートへの早期アクセス</string>
<string name="no_manual_installation">手動インストールが不要</string>
<string name="prioritized_support">優先サポート</string>
<string name="helping_game_preservation">ゲームの保存に貢献</string>
<string name="our_eternal_gratitude">私たちから永遠の感謝</string>
<string name="are_you_interested">興味がありますか?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">エミュレーション速度を制限</string>
<string name="frame_limit_enable_description">エミュレーション速度を指定した割合に制限します。</string>

View file

@ -385,17 +385,6 @@
<string name="user_data_export_cancelled">내보내기 취소됨</string>
<string name="user_data_import_failed_description">유저 데이터 폴더가 ZIP 폴더의 루트 디렉토리에 위치하고 config/config.ini 구성 파일이 있는지 확인하고 다시 시도하세요.</string>
<!-- Early access upgrade strings -->
<string name="early_access">앞서 해보기</string>
<string name="early_access_benefits">앞서 해보기 혜택</string>
<string name="cutting_edge_features">최신 기능</string>
<string name="early_access_updates">업데이트 미리 체험</string>
<string name="no_manual_installation">수동 설치 불필요</string>
<string name="prioritized_support">우선 지원</string>
<string name="helping_game_preservation">게임 보존 지원</string>
<string name="our_eternal_gratitude">우리의 영원한 감사의 마음</string>
<string name="are_you_interested">관심 있으세요?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">속도 제한</string>
<string name="frame_limit_enable_description">에뮬레이션 속도를 정상 속도의 지정된 비율로 제한합니다.</string>

View file

@ -334,17 +334,6 @@
<string name="licenses_description">Prosjekter som gjør Eden for Android mulig</string>
<string name="build">Bygg</string>
<!-- Early access upgrade strings -->
<string name="early_access">Tidlig tilgang</string>
<string name="early_access_benefits">Fordeler ved tidlig tilgang</string>
<string name="cutting_edge_features">Avanserte funksjoner</string>
<string name="early_access_updates">Tidlig tilgang til oppdateringer</string>
<string name="no_manual_installation">Ingen manuell installasjon</string>
<string name="prioritized_support">Prioritert støtte</string>
<string name="helping_game_preservation">Bidra til bevaring av spill</string>
<string name="our_eternal_gratitude">Vår evige takknemlighet</string>
<string name="are_you_interested">Er du interessert?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">Begrense hastigheten</string>
<string name="frame_limit_enable_description">Begrenser emuleringshastigheten til en spesifisert prosentandel av normal hastighet.</string>

View file

@ -334,17 +334,6 @@
<string name="licenses_description">Projekty dzięki którym Eden mógł zostać stworzony</string>
<string name="build">Wersja</string>
<!-- Early access upgrade strings -->
<string name="early_access">Wczesny dostęp</string>
<string name="early_access_benefits">Korzyści z wcześniejszego dostępu</string>
<string name="cutting_edge_features">Nowatorskie funkcje</string>
<string name="early_access_updates">Częste aktualizacje</string>
<string name="no_manual_installation">Automatyczne aktualizacje</string>
<string name="prioritized_support">Priorytetowe wsparcie</string>
<string name="helping_game_preservation">Pomoc w problemach z grami</string>
<string name="our_eternal_gratitude">Nasza wdzięczność</string>
<string name="are_you_interested">Jesteś zainteresowany?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">Limit szybkość</string>
<string name="frame_limit_enable_description">Włącz, aby ustawić procentowy limit szybkości emulacji</string>

View file

@ -390,17 +390,6 @@
<string name="user_data_export_cancelled">Exportação cancelada</string>
<string name="user_data_import_failed_description">Verifique se as pastas de dados do usuário estão na raiz da pasta zip, se possuem um arquivo de configuração em config/config.ini e tente novamente.</string>
<!-- Early access upgrade strings -->
<string name="early_access">Acesso Antecipado</string>
<string name="early_access_benefits">Benefícios do Acesso Antecipado</string>
<string name="cutting_edge_features">Recursos de ponta</string>
<string name="early_access_updates">Acesso antecipado a atualizações</string>
<string name="no_manual_installation">Sem instalação manual</string>
<string name="prioritized_support">Suporte prioritário</string>
<string name="helping_game_preservation">Ajuda na preservação dos jogos</string>
<string name="our_eternal_gratitude">A nossa eterna gratidão</string>
<string name="are_you_interested">Tem interesse?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">Limite de velocidade</string>
<string name="frame_limit_enable_description">Limita a velocidade da emulação a uma porcentagem específica da velocidade normal.</string>

View file

@ -390,17 +390,6 @@
<string name="user_data_export_cancelled">Exportação cancelada</string>
<string name="user_data_import_failed_description">Verifiqua se as pastas de dados do utilizados estão na raiz da pasta zip e contêm um arquivo de configuração em config/config.ini e tenta novamente.</string>
<!-- Early access upgrade strings -->
<string name="early_access">Acesso antecipado</string>
<string name="early_access_benefits">Benefícios do Acesso Antecipado</string>
<string name="cutting_edge_features">Recursos de ponta</string>
<string name="early_access_updates">Acesso antecipado a atualizações</string>
<string name="no_manual_installation">Sem instalação manual</string>
<string name="prioritized_support">Suporte prioritário</string>
<string name="helping_game_preservation">Ajuda na preservação dos jogos</string>
<string name="our_eternal_gratitude">A nossa eterna gratidão</string>
<string name="are_you_interested">Estás interessado?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">Limite de velocidade</string>
<string name="frame_limit_enable_description">Limita a velocidade da emulação a uma porcentagem específica da velocidade normal.</string>

View file

@ -391,17 +391,6 @@
<string name="user_data_export_cancelled">Экспорт отменен</string>
<string name="user_data_import_failed_description">Убедитесь что папки пользовательских данных находятся в корне zip-папки и содержат файл конфигурации config/config.ini и повторите попытку.</string>
<!-- Early access upgrade strings -->
<string name="early_access">Ранний доступ</string>
<string name="early_access_benefits">Преимущества раннего доступа</string>
<string name="cutting_edge_features">Новейшие возможности</string>
<string name="early_access_updates">Ранний доступ к обновлениям</string>
<string name="no_manual_installation">Без ручной установки</string>
<string name="prioritized_support">Приоритетная поддержка</string>
<string name="helping_game_preservation">Помощь в презервации игр</string>
<string name="our_eternal_gratitude">Наша бесконечная благодарность</string>
<string name="are_you_interested">Вы заинтересованы?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">Ограничить скорость</string>
<string name="frame_limit_enable_description">Ограничивает скорость эмуляции указанным процентом от нормальной скорости.</string>

View file

@ -320,17 +320,6 @@
<string name="contributors_description">Зроблено з \u2764 від команди Eden</string>
<string name="build">Збірка</string>
<!-- Early access upgrade strings -->
<string name="early_access">Ранній доступ</string>
<string name="early_access_benefits">Переваги раннього доступу</string>
<string name="cutting_edge_features">Новітні можливості</string>
<string name="early_access_updates">Ранній доступ до оновлень</string>
<string name="no_manual_installation">Без ручного встановлення</string>
<string name="prioritized_support">Пріоритетна підтримка</string>
<string name="helping_game_preservation">Допомога в презервації ігор</string>
<string name="our_eternal_gratitude">Наша нескінченна вдячність</string>
<string name="are_you_interested">Ви зацікавлені?</string>
<string name="frame_limit_enable">Обмеження швидкості</string>
<string name="frame_limit_enable_description">Обмежує швидкість емуляції у відсотках від нормальної.</string>
<string name="frame_limit_slider">Відсоток обмеження</string>

View file

@ -334,17 +334,6 @@
<string name="licenses_description">Các dự án làm cho Eden trên Android trở thành điều có thể</string>
<string name="build">Dựng</string>
<!-- Early access upgrade strings -->
<string name="early_access">Early Access</string>
<string name="early_access_benefits">Lợi ích của Early Access</string>
<string name="cutting_edge_features">Tính năng tiên tiến</string>
<string name="early_access_updates">Truy cập sớm vào cập nhật</string>
<string name="no_manual_installation">Không có cài đặt thủ công</string>
<string name="prioritized_support">Ưu tiên hỗ trợ</string>
<string name="helping_game_preservation">Hỗ trợ bảo tồn trò chơi</string>
<string name="our_eternal_gratitude">Sự biết ơn vô hạn của chúng tôi</string>
<string name="are_you_interested">Bạn có thấy thú vị không?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">Giới hạn tốc độ</string>
<string name="frame_limit_enable_description">Giới hạn tốc độ giả lập ở một phần trăm cụ thể của tốc độ bình thường.</string>

View file

@ -385,17 +385,6 @@
<string name="user_data_export_cancelled">已取消导出数据</string>
<string name="user_data_import_failed_description">请确保用户数据文件夹位于 zip 压缩包的根目录,并在 config/config.ini 路径中包含配置文件,然后重试。</string>
<!-- Early access upgrade strings -->
<string name="early_access">抢先体验</string>
<string name="early_access_benefits">抢先体验的权益</string>
<string name="cutting_edge_features">最新功能</string>
<string name="early_access_updates">抢先更新</string>
<string name="no_manual_installation">无需手动安装</string>
<string name="prioritized_support">优先支持</string>
<string name="helping_game_preservation">帮助保留游玩历史</string>
<string name="our_eternal_gratitude">我们真诚的感激</string>
<string name="are_you_interested">您对此感兴趣吗?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">运行速度限制</string>
<string name="frame_limit_enable_description">将运行速度限制为正常速度的指定百分比。</string>

View file

@ -391,17 +391,6 @@
<string name="user_data_export_cancelled">匯出已取消</string>
<string name="user_data_import_failed_description">請確保使用者資料夾位於 zip 壓縮檔的根目錄,並在 config/config.ini 路徑中包含組態檔案,並再試一次。</string>
<!-- Early access upgrade strings -->
<string name="early_access">搶先體驗</string>
<string name="early_access_benefits">搶先體驗權益</string>
<string name="cutting_edge_features">最新功能</string>
<string name="early_access_updates">搶先版更新</string>
<string name="no_manual_installation">無需手動安裝</string>
<string name="prioritized_support">優先支援</string>
<string name="helping_game_preservation">協助遊戲保留</string>
<string name="our_eternal_gratitude">我們永遠的感激</string>
<string name="are_you_interested">您仍感興趣嗎?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">限制速度</string>
<string name="frame_limit_enable_description">將模擬速度限制在標準速度的指定百分比。</string>

View file

@ -104,9 +104,10 @@
<string name="multiplayer">Multiplayer</string>
<string name="multiplayer_description">Host your own game room or join an existing one to play with people</string>
<string name="multiplayer_room_title">Room: %1$s</string>
<string name="multiplayer_console_id">Console ID: %1$s</string>
<string name="multiplayer_console_id">Console ID%1$s</string>
<string name="multiplayer_create_room">Create</string>
<string name="multiplayer_join_room">Join</string>
<string name="multiplayer_public_room">Browse Public Rooms</string>
<string name="multiplayer_username">Username</string>
<string name="multiplayer_ip_address">IP Address</string>
<string name="multiplayer_ip_port">Port</string>
@ -167,9 +168,26 @@
<string name="multiplayer_unban">Unban</string>
<string name="multiplayer_unban_message">Are you sure you want to unban %1$s?</string>
<string name="multiplayer_ban">Ban User</string>
<string name="multiplayer_room_browser">Public Rooms</string>
<string name="multiplayer_no_rooms_found">No public rooms found</string>
<string name="multiplayer_password_required">Password Required</string>
<string name="multiplayer_player_count">: %1$d/%2$d</string>
<string name="multiplayer_game">Game</string>
<string name="multiplayer_no_game_info">Any Game</string>
<string name="multiplayer_password_protected">Password protected room</string>
<string name="multiplayer_hide_full_rooms">Hide Full Rooms</string>
<string name="multiplayer_hide_empty_rooms">Hide Empty Rooms</string>
<string name="multiplayer_tap_refresh_to_check_again">Tap refresh to check again</string>
<string name="multiplayer_search_public_lobbies">Search Lobbies…</string>
<string name="emulation_multiplayer">Multiplayer</string>
<string name="multiplayer_game_name">Preferred Games</string>
<string name="multiplayer_preferred_game_name">Preferred Game</string>
<string name="multiplayer_no_game">No Games Found</string>
<string name="multiplayer_preferred_game_name_invalid">You must choose a Preferred Game to host a room.</string>
<string name="cancel">Cancel</string>
<string name="ok">Ok</string>
<string name="refresh">Refresh</string>
<string name="room_list">Room List</string>
<!-- Setup strings -->
<string name="welcome">Welcome!</string>
@ -240,6 +258,7 @@
<string name="invalid_keys_error">Invalid encryption keys</string>
<string name="dumping_keys_quickstart_link">https://yuzu-mirror.github.io/help/quickstart/#dumping-decryption-keys</string>
<string name="install_keys_failure_description">The selected file is incorrect or corrupt. Please redump your keys.</string>
<string name="gpu_driver_fetcher">GPU driver fetcher</string>
<string name="gpu_driver_manager">GPU driver manager</string>
<string name="install_gpu_driver">Install GPU driver</string>
<string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
@ -355,22 +374,10 @@
<string name="user_data_import_success">User data imported successfully</string>
<string name="user_data_export_cancelled">Export cancelled</string>
<string name="user_data_import_failed_description">Make sure the user data folders are at the root of the zip folder and contain a config file at config/config.ini and try again.</string>
<string name="support_link">https://discord.gg/hab4Sh8qj6</string>
<string name="support_link">https://discord.gg/edenemu</string>
<string name="website_link">https://eden-emulator.github.io</string>
<string name="github_link">https://git.eden-emu.dev/eden-emu</string>
<!-- Early access upgrade strings -->
<string name="early_access">Early Access</string>
<string name="play_store_link">https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea</string>
<string name="early_access_benefits">Early Access Benefits</string>
<string name="cutting_edge_features">Cutting-edge features</string>
<string name="early_access_updates">Early access to updates</string>
<string name="no_manual_installation">No manual installation</string>
<string name="prioritized_support">Prioritized support</string>
<string name="helping_game_preservation">Helping game preservation</string>
<string name="our_eternal_gratitude">Our eternal gratitude</string>
<string name="are_you_interested">Are you interested?</string>
<!-- General settings strings -->
<string name="frame_limit_enable">Limit speed</string>
<string name="frame_limit_enable_description">Limits emulation speed to a specified percentage of normal speed.</string>
@ -552,6 +559,7 @@
<string name="import_failed">Import failed</string>
<string name="cancelling">Cancelling</string>
<string name="install">Install</string>
<string name="fetch">Fetch</string>
<string name="delete">Delete</string>
<string name="edit">Edit</string>
<string name="export_success">Exported successfully</string>
@ -580,6 +588,31 @@
<string name="system_gpu_driver">System GPU driver</string>
<string name="installing_driver">Installing driver…</string>
<!-- GPU driver fetcher -->
<string name="fetch_error">Error while Fetching</string>
<string name="check_connection">Check your connection and try again.</string>
<string name="show_releases">Show Releases</string>
<string name="view_full_release_notes">Release Notes</string>
<string name="failed_to_fetch">Failed to fetch</string>
<string name="error_during_fetch">Error during Fetch</string>
<string name="toggle_release_notes">Toggle release notes</string>
<string name="downloads">Downloads</string>
<string name="show_downloads">Show Downloads</string>
<string name="hide_downloads">Hide Downloads</string>
<string name="failed_cache_dir">Cache directory unavailable</string>
<string name="empty_response_body">Empty response body</string>
<string name="successfully_installed">%1$s Successfully Installed</string>
<string name="driver_failed_title">Driver installation failed</string>
<string name="failed_install_driver">Failed to install %1$s driver, does your system support it?</string>
<string name="driver_empty">Downloaded driver is empty, check your internet</string>
<string name="downloading">Downloading…</string>
<string name="installing">Installing…</string>
<string name="latest">Latest</string>
<string name="recommended_driver">Recommended Driver:</string>
<string name="gpu_model">GPU Model:</string>
<string name="unsupported_gpu">Unsupported GPU</string>
<string name="unsupported_gpu_warning">Your GPU does not support driver injection. Attempting to set custom drivers is not recommended.</string>
<!-- Preferences Screen -->
<string name="preferences_settings">Settings</string>
<string name="preferences_general">General</string>