diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 7ba1e74279..6c5592c1f5 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -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") diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/DriverGroupAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/DriverGroupAdapter.kt new file mode 100644 index 0000000000..06105d2702 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/DriverGroupAdapter.kt @@ -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() { + private var driverGroups: List = 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) { + driverGroups = newDriverGroups + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/ReleaseAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/ReleaseAdapter.kt new file mode 100644 index 0000000000..1dcec3c9f0 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/ReleaseAdapter.kt @@ -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, + private val activity: FragmentActivity, + private val driverViewModel: DriverViewModel +) : RecyclerView.Adapter() { + + 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", "
") + + 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", "
") + + 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 +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/SpacingItemDecoration.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/SpacingItemDecoration.kt new file mode 100644 index 0000000000..8a8c025d8d --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/fetcher/SpacingItemDecoration.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt index 6c8bb65d6e..3a44e30bdb 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt @@ -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" diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt new file mode 100644 index 0000000000..bb804f02ae --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt @@ -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 = 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) + 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() + + 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 + 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() + } + } + + 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 = ArrayList(), + var prerelease: Boolean = false, + var latest: Boolean = false + ) { + companion object { + fun fromJsonArray(jsonString: String, useTagName: Boolean): ArrayList { + val mapper = jacksonObjectMapper() + + try { + val rootNode = mapper.readTree(jsonString) + + val releases = ArrayList() + + 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() + } + } + + 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() + 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, + val sort: Int, + ) +} \ No newline at end of file diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt index c58c02506c..ce5c85011d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt @@ -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) + } + } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index 2a0ed2f639..319d2d6a19 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt @@ -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 diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt index 730c59e592..c1d0f6297a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt @@ -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 { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 468c758c2f..eb8837b8f1 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -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 diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt index fc2339f5a2..feed8b3cf8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt @@ -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) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt index a72dea8f10..81943e9235 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt @@ -202,6 +202,11 @@ object GpuDriverHelper { hookLibPath: String = GpuDriverHelper.hookLibPath!! ): Array? + 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)) diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 962fae126c..f18fc2e180 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -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(handle); + InputCommon::InputSubsystem input_subsystem; + auto window = + std::make_unique(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(Core::Crypto::KeyManager::Instance().AreKeysLoaded()); diff --git a/src/android/app/src/main/res/drawable/ic_dropdown_arrow.xml b/src/android/app/src/main/res/drawable/ic_dropdown_arrow.xml new file mode 100644 index 0000000000..26424d9d4c --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_dropdown_arrow.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/android/app/src/main/res/drawable/item_release_box.xml b/src/android/app/src/main/res/drawable/item_release_box.xml new file mode 100644 index 0000000000..2f2ada1961 --- /dev/null +++ b/src/android/app/src/main/res/drawable/item_release_box.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/drawable/item_release_latest_badge_background.xml b/src/android/app/src/main/res/drawable/item_release_latest_badge_background.xml new file mode 100644 index 0000000000..3fff3b7333 --- /dev/null +++ b/src/android/app/src/main/res/drawable/item_release_latest_badge_background.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/dialog_progress.xml b/src/android/app/src/main/res/layout/dialog_progress.xml new file mode 100644 index 0000000000..09fecba6ad --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_progress.xml @@ -0,0 +1,34 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/fragment_driver_fetcher.xml b/src/android/app/src/main/res/layout/fragment_driver_fetcher.xml new file mode 100644 index 0000000000..462627c673 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_driver_fetcher.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/android/app/src/main/res/layout/fragment_driver_manager.xml b/src/android/app/src/main/res/layout/fragment_driver_manager.xml index 56d8e6bb8f..47e71e033e 100644 --- a/src/android/app/src/main/res/layout/fragment_driver_manager.xml +++ b/src/android/app/src/main/res/layout/fragment_driver_manager.xml @@ -7,6 +7,7 @@ android:background="?attr/colorSurface"> @@ -47,4 +48,14 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> + + diff --git a/src/android/app/src/main/res/layout/item_driver_group.xml b/src/android/app/src/main/res/layout/item_driver_group.xml new file mode 100644 index 0000000000..b995f66a0f --- /dev/null +++ b/src/android/app/src/main/res/layout/item_driver_group.xml @@ -0,0 +1,39 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/item_release.xml b/src/android/app/src/main/res/layout/item_release.xml new file mode 100644 index 0000000000..2c02672467 --- /dev/null +++ b/src/android/app/src/main/res/layout/item_release.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml index c9409875fd..bfea21e557 100644 --- a/src/android/app/src/main/res/navigation/home_navigation.xml +++ b/src/android/app/src/main/res/navigation/home_navigation.xml @@ -1,6 +1,7 @@ @@ -111,6 +112,9 @@ app:argType="org.yuzu.yuzu_emu.model.Game" app:nullable="true" android:defaultValue="@null" /> + + diff --git a/src/android/app/src/main/res/values-ar/strings.xml b/src/android/app/src/main/res/values-ar/strings.xml index e639390f2a..19db7ada41 100644 --- a/src/android/app/src/main/res/values-ar/strings.xml +++ b/src/android/app/src/main/res/values-ar/strings.xml @@ -365,17 +365,6 @@ تم استيراد بيانات المستخدم بنجاح تم إلغاء التصدير - - الوصول المبكر - مزايا الوصول المبكر - ميزات متطورة - الوصول المبكر إلى التحديثات - لا يوجد التثبيت اليدوي - الدعم ذو الأولوية - المساعدة في الحفاظ على اللعبة - امتناننا الأبدي - هل انت مهتم؟ - الحد من السرعة يحد من سرعة المحاكاة بنسبة محددة من السرعة العادية diff --git a/src/android/app/src/main/res/values-ckb/strings.xml b/src/android/app/src/main/res/values-ckb/strings.xml index 544a09672b..f23bd8e62b 100644 --- a/src/android/app/src/main/res/values-ckb/strings.xml +++ b/src/android/app/src/main/res/values-ckb/strings.xml @@ -334,17 +334,6 @@ ئەو پڕۆژانەی کە یوزوی بۆ ئەندرۆید ڕەخساند بونیات - - بەزوویی دەسپێگەشتن - سوودەکانی بەزوویی دەسپێگەشتن - تایبەتمەندییە پێشکەوتووەکان - زوو دەستگەیشتن بە نوێکارییەکان - چیتر دامەزراندنی دەستی نییە - پشتگیری لە پێشینە - یارمەتیدانی پاراستنی یارییەکان - سوپاس و پێزانینی هەمیشەییمان - ئایا تۆ خوازیاریت؟ - سنووردارکردنی خێرایی خێرایی ئیمولەیشن سنووردار دەکات بۆ ڕێژەیەکی دیاریکراو لە خێرایی ئاسایی. diff --git a/src/android/app/src/main/res/values-cs/strings.xml b/src/android/app/src/main/res/values-cs/strings.xml index 0bbad6c530..6b0dbce29f 100644 --- a/src/android/app/src/main/res/values-cs/strings.xml +++ b/src/android/app/src/main/res/values-cs/strings.xml @@ -326,10 +326,6 @@ Uživatelská data byla úspěšně importována. Export zrušen - Žádná manuální instalace - Prioritní podpora - Naše věčná vděčnost - Máte zájem? Omezit rychlost diff --git a/src/android/app/src/main/res/values-de/strings.xml b/src/android/app/src/main/res/values-de/strings.xml index 3c1e589a97..340e58942e 100644 --- a/src/android/app/src/main/res/values-de/strings.xml +++ b/src/android/app/src/main/res/values-de/strings.xml @@ -358,17 +358,6 @@ Wirklich fortfahren? Nutzerdaten erfolgreich importiert Export abgebrochen - - Early Access - Early Access Vorteile - Neueste Features - Früherer Zugriff auf Updates - Keine manuelle Installation - Priorisierte Unterstützung - Beitrag zur Erhaltung der Spiele - Unsere ewige Dankbarkeit - Bist du interessiert? - Limitierte Geschwindigkeit Limitiert die Geschwindigkeit auf einen von dir festgelegten Prozentsatz. diff --git a/src/android/app/src/main/res/values-es/strings.xml b/src/android/app/src/main/res/values-es/strings.xml index cbe4fc0704..e74242869b 100644 --- a/src/android/app/src/main/res/values-es/strings.xml +++ b/src/android/app/src/main/res/values-es/strings.xml @@ -390,17 +390,6 @@ Exportación cancelada 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. - - Early Access - Beneficios Early Access - Características de vanguardia - Acceso anticipado a las actualizaciones - Sin instalación manual - Soporte prioritario - Ayudarás a la preservación de juegos - Nuestra eterna gratitud - ¿Estás interesado? - Limitar velocidad Limita la velocidad de emulación a un porcentaje específico de la velocidad normal. diff --git a/src/android/app/src/main/res/values-fa/strings.xml b/src/android/app/src/main/res/values-fa/strings.xml index 954ee78c0d..58fb8f8d72 100644 --- a/src/android/app/src/main/res/values-fa/strings.xml +++ b/src/android/app/src/main/res/values-fa/strings.xml @@ -388,17 +388,6 @@ صدور لغو شد مطمئن شوید که پوشه‌های داده کاربر در ریشه پوشه zip و حاوی یک فایل پیکربندی در config/config.ini هستند سپس دوباره امتحان کنید. - - دسترسی زودهنگام - مزایای دسترسی زودهنگام - ویژگی‌های پیشرفته - دسترسی زودهنگام به بروزرسانی‌ها - بدون نصب دستی - پشتیبانی اولویت بندی شده - کمک به حفظ بازی - سپاس ابدی ما - مشتاق هستید؟ - محدودیت سرعت سرعت شبیه‌سازی را به درصد مشخصی از سرعت عادی محدود می‌کند. diff --git a/src/android/app/src/main/res/values-fr/strings.xml b/src/android/app/src/main/res/values-fr/strings.xml index 979b9e7ec3..7688fb2c2c 100644 --- a/src/android/app/src/main/res/values-fr/strings.xml +++ b/src/android/app/src/main/res/values-fr/strings.xml @@ -390,17 +390,6 @@ Exportation annulée 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. - - Early Access - Avantages de l\'Early Access - Fonctionnalités de pointe - Accès anticipé aux mises à jour - Pas d\'installation manuelle - Assistance prioritaire - Contribuer à la préservation des jeux - Notre gratitude éternelle - Es tu intéressé ? - Limiter la vitesse Limiter la vitesse d\'émulation à un pourcentage spécifié de la vitesse normale diff --git a/src/android/app/src/main/res/values-he/strings.xml b/src/android/app/src/main/res/values-he/strings.xml index a7cdf34dd2..8b93ed1117 100644 --- a/src/android/app/src/main/res/values-he/strings.xml +++ b/src/android/app/src/main/res/values-he/strings.xml @@ -376,17 +376,6 @@ ייצוא בוטל ודא שנתוני המשתמש נמצאים בשורש קובץ ה zip ושהוא מכיל קובץ סידור ב config/config.ini ונסה שוב. - - גישה מוקדמת - יתרונות של גישה מקודמת - תכונות חותכות קצה - גישה מוקדמת לעדכונים - ללא התקנה ידנית - תמיכה בעדיפות - עוזר בשמירת משחקים - התודה האינסופית שלנו - אתה מעוניין? - הגבל מהירות מגביל את מהירות האמולציה לאחוז מהירות המבוקש מהמהירות הרגילה. diff --git a/src/android/app/src/main/res/values-hu/strings.xml b/src/android/app/src/main/res/values-hu/strings.xml index 7a43bebc5a..098235b658 100644 --- a/src/android/app/src/main/res/values-hu/strings.xml +++ b/src/android/app/src/main/res/values-hu/strings.xml @@ -385,16 +385,6 @@ Exportálás megszakítva 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. - - Korai hozzáférés - Korai hozzáférés előnyei - Legújabb funkciók - Korai hozzáférés a frissítésekhez - Automatikus telepítések - Priorizált támogatás - Valamint az örök hálánk - Érdekel a dolog? - Sebességkorlát Korlátozza az emuláció sebességét a normál sebesség adott százalékára. diff --git a/src/android/app/src/main/res/values-id/strings.xml b/src/android/app/src/main/res/values-id/strings.xml index 7bb72ccf5b..4be9176120 100644 --- a/src/android/app/src/main/res/values-id/strings.xml +++ b/src/android/app/src/main/res/values-id/strings.xml @@ -386,17 +386,6 @@ Ekspor Dibatalkan Pastikan folder data pengguna berada di akar folder zip dan berisi file konfigurasi di config/config.ini dan coba lagi. - - Akses lebih awal - Manfaat Akses Awal - Fitur-fitur yang paling baru - Akses lebih awal untuk pembaruan - Tidak ada instalasi manual - Dukungan yang diprioritaskan - Membantu pemeliharaan game - Ucapan terima kasih kami yang tak terhingga - Apa kamu tertarik? - Batas kecepatan Membatasi kecepatan emulasi ke persentase tertentu dari kecepatan normal diff --git a/src/android/app/src/main/res/values-it/strings.xml b/src/android/app/src/main/res/values-it/strings.xml index 0c10955176..587ea34818 100644 --- a/src/android/app/src/main/res/values-it/strings.xml +++ b/src/android/app/src/main/res/values-it/strings.xml @@ -378,17 +378,6 @@ Esportazione annullata 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. - - Accesso Anticipato - Vantaggi dell\'accesso anticipato - Funzionalità all\'avanguardia - Accesso anticipato agli aggiornamenti - Non installare manualmente. - Supporto prioritario - Aiuta a preservare il gioco - La nostra gratitudine eterna - Sei interessato? - Limita velocità Limita la velocità dell\'emulazione a una specifica percentuale della velocità normale. diff --git a/src/android/app/src/main/res/values-ja/strings.xml b/src/android/app/src/main/res/values-ja/strings.xml index 13f8ad4ec5..c7f47aca32 100644 --- a/src/android/app/src/main/res/values-ja/strings.xml +++ b/src/android/app/src/main/res/values-ja/strings.xml @@ -357,17 +357,6 @@ ユーザデータのインポートに成功しました エクスポートをキャンセルしました - - 早期アクセス - 早期アクセスのメリット - 最先端の機能 - アップデートへの早期アクセス - 手動インストールが不要 - 優先サポート - ゲームの保存に貢献 - 私たちから永遠の感謝 - 興味がありますか? - エミュレーション速度を制限 エミュレーション速度を指定した割合に制限します。 diff --git a/src/android/app/src/main/res/values-ko/strings.xml b/src/android/app/src/main/res/values-ko/strings.xml index b5ad726fd2..0eb6a9cccb 100644 --- a/src/android/app/src/main/res/values-ko/strings.xml +++ b/src/android/app/src/main/res/values-ko/strings.xml @@ -385,17 +385,6 @@ 내보내기 취소됨 유저 데이터 폴더가 ZIP 폴더의 루트 디렉토리에 위치하고 config/config.ini 구성 파일이 있는지 확인하고 다시 시도하세요. - - 앞서 해보기 - 앞서 해보기 혜택 - 최신 기능 - 업데이트 미리 체험 - 수동 설치 불필요 - 우선 지원 - 게임 보존 지원 - 우리의 영원한 감사의 마음 - 관심 있으세요? - 속도 제한 에뮬레이션 속도를 정상 속도의 지정된 비율로 제한합니다. diff --git a/src/android/app/src/main/res/values-nb/strings.xml b/src/android/app/src/main/res/values-nb/strings.xml index 0419d79316..1c1e3f2181 100644 --- a/src/android/app/src/main/res/values-nb/strings.xml +++ b/src/android/app/src/main/res/values-nb/strings.xml @@ -334,17 +334,6 @@ Prosjekter som gjør Eden for Android mulig Bygg - - Tidlig tilgang - Fordeler ved tidlig tilgang - Avanserte funksjoner - Tidlig tilgang til oppdateringer - Ingen manuell installasjon - Prioritert støtte - Bidra til bevaring av spill - Vår evige takknemlighet - Er du interessert? - Begrense hastigheten Begrenser emuleringshastigheten til en spesifisert prosentandel av normal hastighet. diff --git a/src/android/app/src/main/res/values-pl/strings.xml b/src/android/app/src/main/res/values-pl/strings.xml index 68374d3917..bc74f6437b 100644 --- a/src/android/app/src/main/res/values-pl/strings.xml +++ b/src/android/app/src/main/res/values-pl/strings.xml @@ -334,17 +334,6 @@ Projekty dzięki którym Eden mógł zostać stworzony Wersja - - Wczesny dostęp - Korzyści z wcześniejszego dostępu - Nowatorskie funkcje - Częste aktualizacje - Automatyczne aktualizacje - Priorytetowe wsparcie - Pomoc w problemach z grami - Nasza wdzięczność - Jesteś zainteresowany? - Limit szybkość Włącz, aby ustawić procentowy limit szybkości emulacji diff --git a/src/android/app/src/main/res/values-pt-rBR/strings.xml b/src/android/app/src/main/res/values-pt-rBR/strings.xml index 2057dd722a..83279ba6ec 100644 --- a/src/android/app/src/main/res/values-pt-rBR/strings.xml +++ b/src/android/app/src/main/res/values-pt-rBR/strings.xml @@ -390,17 +390,6 @@ Exportação cancelada 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. - - Acesso Antecipado - Benefícios do Acesso Antecipado - Recursos de ponta - Acesso antecipado a atualizações - Sem instalação manual - Suporte prioritário - Ajuda na preservação dos jogos - A nossa eterna gratidão - Tem interesse? - Limite de velocidade Limita a velocidade da emulação a uma porcentagem específica da velocidade normal. diff --git a/src/android/app/src/main/res/values-pt-rPT/strings.xml b/src/android/app/src/main/res/values-pt-rPT/strings.xml index d8c202cbcd..a961bf4b01 100644 --- a/src/android/app/src/main/res/values-pt-rPT/strings.xml +++ b/src/android/app/src/main/res/values-pt-rPT/strings.xml @@ -390,17 +390,6 @@ Exportação cancelada 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. - - Acesso antecipado - Benefícios do Acesso Antecipado - Recursos de ponta - Acesso antecipado a atualizações - Sem instalação manual - Suporte prioritário - Ajuda na preservação dos jogos - A nossa eterna gratidão - Estás interessado? - Limite de velocidade Limita a velocidade da emulação a uma porcentagem específica da velocidade normal. diff --git a/src/android/app/src/main/res/values-ru/strings.xml b/src/android/app/src/main/res/values-ru/strings.xml index a934802379..93a45e8f9a 100644 --- a/src/android/app/src/main/res/values-ru/strings.xml +++ b/src/android/app/src/main/res/values-ru/strings.xml @@ -391,17 +391,6 @@ Экспорт отменен Убедитесь что папки пользовательских данных находятся в корне zip-папки и содержат файл конфигурации config/config.ini и повторите попытку. - - Ранний доступ - Преимущества раннего доступа - Новейшие возможности - Ранний доступ к обновлениям - Без ручной установки - Приоритетная поддержка - Помощь в презервации игр - Наша бесконечная благодарность - Вы заинтересованы? - Ограничить скорость Ограничивает скорость эмуляции указанным процентом от нормальной скорости. diff --git a/src/android/app/src/main/res/values-uk/strings.xml b/src/android/app/src/main/res/values-uk/strings.xml index 360397de77..8961f128e2 100644 --- a/src/android/app/src/main/res/values-uk/strings.xml +++ b/src/android/app/src/main/res/values-uk/strings.xml @@ -320,17 +320,6 @@ Зроблено з \u2764 від команди Eden Збірка - - Ранній доступ - Переваги раннього доступу - Новітні можливості - Ранній доступ до оновлень - Без ручного встановлення - Пріоритетна підтримка - Допомога в презервації ігор - Наша нескінченна вдячність - Ви зацікавлені? - Обмеження швидкості Обмежує швидкість емуляції у відсотках від нормальної. Відсоток обмеження diff --git a/src/android/app/src/main/res/values-vi/strings.xml b/src/android/app/src/main/res/values-vi/strings.xml index 5a4e6092f5..9ee1bb71d4 100644 --- a/src/android/app/src/main/res/values-vi/strings.xml +++ b/src/android/app/src/main/res/values-vi/strings.xml @@ -334,17 +334,6 @@ Các dự án làm cho Eden trên Android trở thành điều có thể Dựng - - Early Access - Lợi ích của Early Access - Tính năng tiên tiến - Truy cập sớm vào cập nhật - Không có cài đặt thủ công - Ưu tiên hỗ trợ - Hỗ trợ bảo tồn trò chơi - Sự biết ơn vô hạn của chúng tôi - Bạn có thấy thú vị không? - Giới hạn tốc độ 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. diff --git a/src/android/app/src/main/res/values-zh-rCN/strings.xml b/src/android/app/src/main/res/values-zh-rCN/strings.xml index 78585733f8..7427b3352e 100644 --- a/src/android/app/src/main/res/values-zh-rCN/strings.xml +++ b/src/android/app/src/main/res/values-zh-rCN/strings.xml @@ -385,17 +385,6 @@ 已取消导出数据 请确保用户数据文件夹位于 zip 压缩包的根目录,并在 config/config.ini 路径中包含配置文件,然后重试。 - - 抢先体验 - 抢先体验的权益 - 最新功能 - 抢先更新 - 无需手动安装 - 优先支持 - 帮助保留游玩历史 - 我们真诚的感激 - 您对此感兴趣吗? - 运行速度限制 将运行速度限制为正常速度的指定百分比。 diff --git a/src/android/app/src/main/res/values-zh-rTW/strings.xml b/src/android/app/src/main/res/values-zh-rTW/strings.xml index c6070927ca..34f85cc112 100644 --- a/src/android/app/src/main/res/values-zh-rTW/strings.xml +++ b/src/android/app/src/main/res/values-zh-rTW/strings.xml @@ -391,17 +391,6 @@ 匯出已取消 請確保使用者資料夾位於 zip 壓縮檔的根目錄,並在 config/config.ini 路徑中包含組態檔案,並再試一次。 - - 搶先體驗 - 搶先體驗權益 - 最新功能 - 搶先版更新 - 無需手動安裝 - 優先支援 - 協助遊戲保留 - 我們永遠的感激 - 您仍感興趣嗎? - 限制速度 將模擬速度限制在標準速度的指定百分比。 diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index e456afb162..e953666d20 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -104,9 +104,10 @@ Multiplayer Host your own game room or join an existing one to play with people Room: %1$s - Console ID: %1$s + Console ID:%1$s Create Join + Browse Public Rooms Username IP Address Port @@ -167,9 +168,26 @@ Unban Are you sure you want to unban %1$s? Ban User + Public Rooms + No public rooms found + Password Required + : %1$d/%2$d + Game + Any Game + Password protected room + Hide Full Rooms + Hide Empty Rooms + Tap refresh to check again + Search Lobbies… Multiplayer + Preferred Games + Preferred Game + No Games Found + You must choose a Preferred Game to host a room. Cancel Ok + Refresh + Room List Welcome! @@ -240,6 +258,7 @@ Invalid encryption keys https://yuzu-mirror.github.io/help/quickstart/#dumping-decryption-keys The selected file is incorrect or corrupt. Please redump your keys. + GPU driver fetcher GPU driver manager Install GPU driver Install alternative drivers for potentially better performance or accuracy @@ -355,22 +374,10 @@ User data imported successfully Export cancelled 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. - https://discord.gg/hab4Sh8qj6 + https://discord.gg/edenemu https://eden-emulator.github.io https://git.eden-emu.dev/eden-emu - - Early Access - https://play.google.com/store/apps/details?id=org.yuzu.yuzu_emu.ea - Early Access Benefits - Cutting-edge features - Early access to updates - No manual installation - Prioritized support - Helping game preservation - Our eternal gratitude - Are you interested? - Limit speed Limits emulation speed to a specified percentage of normal speed. @@ -552,6 +559,7 @@ Import failed Cancelling Install + Fetch Delete Edit Exported successfully @@ -580,6 +588,31 @@ System GPU driver Installing driver… + + Error while Fetching + Check your connection and try again. + Show Releases + Release Notes + Failed to fetch + Error during Fetch + Toggle release notes + Downloads + Show Downloads + Hide Downloads + Cache directory unavailable + Empty response body + %1$s Successfully Installed + Driver installation failed + Failed to install %1$s driver, does your system support it? + Downloaded driver is empty, check your internet + Downloading… + Installing… + Latest + Recommended Driver: + GPU Model: + Unsupported GPU + Your GPU does not support driver injection. Attempting to set custom drivers is not recommended. + Settings General