Merge pull request #11405 from t895/emulation-loading
android: Emulation loading UI and fixes
This commit is contained in:
commit
a2f0caefd4
|
@ -22,9 +22,7 @@ import org.yuzu.yuzu_emu.utils.FileUtil.exists
|
|||
import org.yuzu.yuzu_emu.utils.FileUtil.getFileSize
|
||||
import org.yuzu.yuzu_emu.utils.FileUtil.isDirectory
|
||||
import org.yuzu.yuzu_emu.utils.FileUtil.openContentUri
|
||||
import org.yuzu.yuzu_emu.utils.Log.error
|
||||
import org.yuzu.yuzu_emu.utils.Log.verbose
|
||||
import org.yuzu.yuzu_emu.utils.Log.warning
|
||||
import org.yuzu.yuzu_emu.utils.Log
|
||||
import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
|
||||
|
||||
/**
|
||||
|
@ -465,7 +463,7 @@ object NativeLibrary {
|
|||
|
||||
val emulationActivity = sEmulationActivity.get()
|
||||
if (emulationActivity == null) {
|
||||
warning("[NativeLibrary] EmulationActivity is null, can't exit.")
|
||||
Log.warning("[NativeLibrary] EmulationActivity is null, can't exit.")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -490,15 +488,27 @@ object NativeLibrary {
|
|||
}
|
||||
|
||||
fun setEmulationActivity(emulationActivity: EmulationActivity?) {
|
||||
verbose("[NativeLibrary] Registering EmulationActivity.")
|
||||
Log.verbose("[NativeLibrary] Registering EmulationActivity.")
|
||||
sEmulationActivity = WeakReference(emulationActivity)
|
||||
}
|
||||
|
||||
fun clearEmulationActivity() {
|
||||
verbose("[NativeLibrary] Unregistering EmulationActivity.")
|
||||
Log.verbose("[NativeLibrary] Unregistering EmulationActivity.")
|
||||
sEmulationActivity.clear()
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun onEmulationStarted() {
|
||||
sEmulationActivity.get()!!.onEmulationStarted()
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun onEmulationStopped(status: Int) {
|
||||
sEmulationActivity.get()!!.onEmulationStopped(status)
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the Yuzu version, Android version and, CPU.
|
||||
*/
|
||||
|
|
|
@ -28,6 +28,7 @@ import android.view.Surface
|
|||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
|
@ -41,6 +42,7 @@ import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding
|
|||
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||
import org.yuzu.yuzu_emu.model.EmulationViewModel
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
import org.yuzu.yuzu_emu.utils.ControllerMappingHelper
|
||||
import org.yuzu.yuzu_emu.utils.ForegroundService
|
||||
|
@ -70,8 +72,11 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
|||
private val actionMute = "ACTION_EMULATOR_MUTE"
|
||||
private val actionUnmute = "ACTION_EMULATOR_UNMUTE"
|
||||
|
||||
private val emulationViewModel: EmulationViewModel by viewModels()
|
||||
|
||||
override fun onDestroy() {
|
||||
stopForegroundService(this)
|
||||
emulationViewModel.clear()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
@ -416,6 +421,16 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
|||
}
|
||||
}
|
||||
|
||||
fun onEmulationStarted() {
|
||||
emulationViewModel.setEmulationStarted(true)
|
||||
}
|
||||
|
||||
fun onEmulationStopped(status: Int) {
|
||||
if (status == 0) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startMotionSensorListener() {
|
||||
val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
|
||||
package org.yuzu.yuzu_emu.adapters
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
|
@ -15,23 +13,20 @@ import android.widget.Toast
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import kotlinx.coroutines.launch
|
||||
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.adapters.GameAdapter.GameViewHolder
|
||||
import org.yuzu.yuzu_emu.databinding.CardGameBinding
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||
import org.yuzu.yuzu_emu.utils.GameIconUtils
|
||||
|
||||
class GameAdapter(private val activity: AppCompatActivity) :
|
||||
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
|
||||
|
@ -98,12 +93,7 @@ class GameAdapter(private val activity: AppCompatActivity) :
|
|||
this.game = game
|
||||
|
||||
binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
activity.lifecycleScope.launch {
|
||||
val bitmap = decodeGameIcon(game.path)
|
||||
binding.imageGameScreen.load(bitmap) {
|
||||
error(R.drawable.default_icon)
|
||||
}
|
||||
}
|
||||
GameIconUtils.loadGameIcon(game, binding.imageGameScreen)
|
||||
|
||||
binding.textGameTitle.text = game.title.replace("[\\t\\n\\r]+".toRegex(), " ")
|
||||
|
||||
|
@ -126,14 +116,4 @@ class GameAdapter(private val activity: AppCompatActivity) :
|
|||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeGameIcon(uri: String): Bitmap? {
|
||||
val data = NativeLibrary.getIcon(uri)
|
||||
return BitmapFactory.decodeByteArray(
|
||||
data,
|
||||
0,
|
||||
data.size,
|
||||
BitmapFactory.Options()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,43 +4,43 @@
|
|||
package org.yuzu.yuzu_emu.disk_shader_cache
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.disk_shader_cache.ui.ShaderProgressDialogFragment
|
||||
import org.yuzu.yuzu_emu.activities.EmulationActivity
|
||||
import org.yuzu.yuzu_emu.model.EmulationViewModel
|
||||
import org.yuzu.yuzu_emu.utils.Log
|
||||
|
||||
@Keep
|
||||
object DiskShaderCacheProgress {
|
||||
val finishLock = Object()
|
||||
private lateinit var fragment: ShaderProgressDialogFragment
|
||||
private lateinit var emulationViewModel: EmulationViewModel
|
||||
|
||||
private fun prepareDialog() {
|
||||
val emulationActivity = NativeLibrary.sEmulationActivity.get()!!
|
||||
emulationActivity.runOnUiThread {
|
||||
fragment = ShaderProgressDialogFragment.newInstance(
|
||||
emulationActivity.getString(R.string.loading),
|
||||
emulationActivity.getString(R.string.preparing_shaders)
|
||||
)
|
||||
fragment.show(
|
||||
emulationActivity.supportFragmentManager,
|
||||
ShaderProgressDialogFragment.TAG
|
||||
)
|
||||
}
|
||||
synchronized(finishLock) { finishLock.wait() }
|
||||
private fun prepareViewModel() {
|
||||
emulationViewModel =
|
||||
ViewModelProvider(
|
||||
NativeLibrary.sEmulationActivity.get() as EmulationActivity
|
||||
)[EmulationViewModel::class.java]
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun loadProgress(stage: Int, progress: Int, max: Int) {
|
||||
val emulationActivity = NativeLibrary.sEmulationActivity.get()
|
||||
?: error("[DiskShaderCacheProgress] EmulationActivity not present")
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[DiskShaderCacheProgress] EmulationActivity not present")
|
||||
return
|
||||
}
|
||||
|
||||
emulationActivity.runOnUiThread {
|
||||
when (LoadCallbackStage.values()[stage]) {
|
||||
LoadCallbackStage.Prepare -> prepareDialog()
|
||||
LoadCallbackStage.Build -> fragment.onUpdateProgress(
|
||||
LoadCallbackStage.Prepare -> prepareViewModel()
|
||||
LoadCallbackStage.Build -> emulationViewModel.updateProgress(
|
||||
emulationActivity.getString(R.string.building_shaders),
|
||||
progress,
|
||||
max
|
||||
)
|
||||
LoadCallbackStage.Complete -> fragment.dismiss()
|
||||
|
||||
LoadCallbackStage.Complete -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.disk_shader_cache
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class ShaderProgressViewModel : ViewModel() {
|
||||
private val _progress = MutableLiveData(0)
|
||||
val progress: LiveData<Int> get() = _progress
|
||||
|
||||
private val _max = MutableLiveData(0)
|
||||
val max: LiveData<Int> get() = _max
|
||||
|
||||
private val _message = MutableLiveData("")
|
||||
val message: LiveData<String> get() = _message
|
||||
|
||||
fun setProgress(progress: Int) {
|
||||
_progress.postValue(progress)
|
||||
}
|
||||
|
||||
fun setMax(max: Int) {
|
||||
_max.postValue(max)
|
||||
}
|
||||
|
||||
fun setMessage(msg: String) {
|
||||
_message.postValue(msg)
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.disk_shader_cache.ui
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
|
||||
import org.yuzu.yuzu_emu.disk_shader_cache.DiskShaderCacheProgress
|
||||
import org.yuzu.yuzu_emu.disk_shader_cache.ShaderProgressViewModel
|
||||
|
||||
class ShaderProgressDialogFragment : DialogFragment() {
|
||||
private var _binding: DialogProgressBarBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var alertDialog: AlertDialog
|
||||
|
||||
private lateinit var shaderProgressViewModel: ShaderProgressViewModel
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
_binding = DialogProgressBarBinding.inflate(layoutInflater)
|
||||
shaderProgressViewModel =
|
||||
ViewModelProvider(requireActivity())[ShaderProgressViewModel::class.java]
|
||||
|
||||
val title = requireArguments().getString(TITLE)
|
||||
val message = requireArguments().getString(MESSAGE)
|
||||
|
||||
isCancelable = false
|
||||
alertDialog = MaterialAlertDialogBuilder(requireActivity())
|
||||
.setView(binding.root)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.create()
|
||||
return alertDialog
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
shaderProgressViewModel.progress.observe(viewLifecycleOwner) { progress ->
|
||||
binding.progressBar.progress = progress
|
||||
setUpdateText()
|
||||
}
|
||||
shaderProgressViewModel.max.observe(viewLifecycleOwner) { max ->
|
||||
binding.progressBar.max = max
|
||||
setUpdateText()
|
||||
}
|
||||
shaderProgressViewModel.message.observe(viewLifecycleOwner) { msg ->
|
||||
alertDialog.setMessage(msg)
|
||||
}
|
||||
synchronized(DiskShaderCacheProgress.finishLock) {
|
||||
DiskShaderCacheProgress.finishLock.notifyAll()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
fun onUpdateProgress(msg: String, progress: Int, max: Int) {
|
||||
shaderProgressViewModel.setProgress(progress)
|
||||
shaderProgressViewModel.setMax(max)
|
||||
shaderProgressViewModel.setMessage(msg)
|
||||
}
|
||||
|
||||
private fun setUpdateText() {
|
||||
binding.progressText.text = String.format(
|
||||
"%d/%d",
|
||||
shaderProgressViewModel.progress.value,
|
||||
shaderProgressViewModel.max.value
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "ProgressDialogFragment"
|
||||
const val TITLE = "title"
|
||||
const val MESSAGE = "message"
|
||||
|
||||
fun newInstance(title: String, message: String): ShaderProgressDialogFragment {
|
||||
val frag = ShaderProgressDialogFragment()
|
||||
val args = Bundle()
|
||||
args.putString(TITLE, title)
|
||||
args.putString(MESSAGE, message)
|
||||
frag.arguments = args
|
||||
return frag
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,8 +24,9 @@ import androidx.core.content.res.ResourcesCompat
|
|||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
|
@ -50,6 +51,7 @@ import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
|||
import org.yuzu.yuzu_emu.features.settings.model.Settings
|
||||
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
import org.yuzu.yuzu_emu.model.EmulationViewModel
|
||||
import org.yuzu.yuzu_emu.overlay.InputOverlay
|
||||
import org.yuzu.yuzu_emu.utils.*
|
||||
|
||||
|
@ -66,6 +68,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
|
||||
private lateinit var game: Game
|
||||
|
||||
private val emulationViewModel: EmulationViewModel by activityViewModels()
|
||||
|
||||
private var isInFoldableLayout = false
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
|
@ -130,9 +134,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
binding.showFpsText.setTextColor(Color.YELLOW)
|
||||
binding.doneControlConfig.setOnClickListener { stopConfiguringControls() }
|
||||
|
||||
// Setup overlay.
|
||||
updateShowFpsOverlay()
|
||||
|
||||
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text =
|
||||
game.title
|
||||
binding.inGameMenu.setNavigationItemSelectedListener {
|
||||
|
@ -174,7 +176,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
|
||||
R.id.menu_exit -> {
|
||||
emulationState.stop()
|
||||
requireActivity().finish()
|
||||
emulationViewModel.setIsEmulationStopping(true)
|
||||
binding.drawerLayout.close()
|
||||
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
true
|
||||
}
|
||||
|
||||
|
@ -188,6 +192,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
requireActivity(),
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (!NativeLibrary.isRunning()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (binding.drawerLayout.isOpen) {
|
||||
binding.drawerLayout.close()
|
||||
} else {
|
||||
|
@ -204,6 +212,54 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
.collect { updateFoldableLayout(requireActivity() as EmulationActivity, it) }
|
||||
}
|
||||
}
|
||||
|
||||
GameIconUtils.loadGameIcon(game, binding.loadingImage)
|
||||
binding.loadingTitle.text = game.title
|
||||
binding.loadingTitle.isSelected = true
|
||||
binding.loadingText.isSelected = true
|
||||
|
||||
emulationViewModel.shaderProgress.observe(viewLifecycleOwner) {
|
||||
if (it > 0 && it != emulationViewModel.totalShaders.value!!) {
|
||||
binding.loadingProgressIndicator.isIndeterminate = false
|
||||
|
||||
if (it < binding.loadingProgressIndicator.max) {
|
||||
binding.loadingProgressIndicator.progress = it
|
||||
}
|
||||
}
|
||||
|
||||
if (it == emulationViewModel.totalShaders.value!!) {
|
||||
binding.loadingText.setText(R.string.loading)
|
||||
binding.loadingProgressIndicator.isIndeterminate = true
|
||||
}
|
||||
}
|
||||
emulationViewModel.totalShaders.observe(viewLifecycleOwner) {
|
||||
binding.loadingProgressIndicator.max = it
|
||||
}
|
||||
emulationViewModel.shaderMessage.observe(viewLifecycleOwner) {
|
||||
if (it.isNotEmpty()) {
|
||||
binding.loadingText.text = it
|
||||
}
|
||||
}
|
||||
|
||||
emulationViewModel.emulationStarted.observe(viewLifecycleOwner) { started ->
|
||||
if (started) {
|
||||
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
|
||||
ViewUtils.showView(binding.surfaceInputOverlay)
|
||||
ViewUtils.hideView(binding.loadingIndicator)
|
||||
|
||||
// Setup overlay
|
||||
updateShowFpsOverlay()
|
||||
}
|
||||
}
|
||||
|
||||
emulationViewModel.isEmulationStopping.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
binding.loadingText.setText(R.string.shutting_down)
|
||||
ViewUtils.showView(binding.loadingIndicator)
|
||||
ViewUtils.hideView(binding.inputContainer)
|
||||
ViewUtils.hideView(binding.showFpsText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
|
@ -213,11 +269,21 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
binding.drawerLayout.close()
|
||||
}
|
||||
if (EmulationMenuSettings.showOverlay) {
|
||||
binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = false }
|
||||
binding.surfaceInputOverlay.post {
|
||||
binding.surfaceInputOverlay.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (EmulationMenuSettings.showOverlay) {
|
||||
binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = true }
|
||||
if (EmulationMenuSettings.showOverlay &&
|
||||
emulationViewModel.emulationStarted.value == true
|
||||
) {
|
||||
binding.surfaceInputOverlay.post {
|
||||
binding.surfaceInputOverlay.visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
binding.surfaceInputOverlay.post {
|
||||
binding.surfaceInputOverlay.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
if (!isInFoldableLayout) {
|
||||
if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
|
@ -226,9 +292,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
binding.surfaceInputOverlay.layout = InputOverlay.LANDSCAPE
|
||||
}
|
||||
}
|
||||
if (!binding.surfaceInputOverlay.isInEditMode) {
|
||||
refreshInputOverlay()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -260,10 +323,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
super.onDetach()
|
||||
}
|
||||
|
||||
private fun refreshInputOverlay() {
|
||||
binding.surfaceInputOverlay.refreshControls()
|
||||
}
|
||||
|
||||
private fun resetInputOverlay() {
|
||||
preferences.edit()
|
||||
.remove(Settings.PREF_CONTROL_SCALE)
|
||||
|
@ -281,17 +340,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
val FRAMETIME = 2
|
||||
val SPEED = 3
|
||||
perfStatsUpdater = {
|
||||
if (emulationViewModel.emulationStarted.value == true) {
|
||||
val perfStats = NativeLibrary.getPerfStats()
|
||||
if (perfStats[FPS] > 0 && _binding != null) {
|
||||
binding.showFpsText.text = String.format("FPS: %.1f", perfStats[FPS])
|
||||
}
|
||||
|
||||
if (!emulationState.isStopped) {
|
||||
perfStatsUpdateHandler.postDelayed(perfStatsUpdater!!, 100)
|
||||
}
|
||||
}
|
||||
perfStatsUpdateHandler.post(perfStatsUpdater!!)
|
||||
binding.showFpsText.text = resources.getString(R.string.emulation_game_loading)
|
||||
binding.showFpsText.visibility = View.VISIBLE
|
||||
} else {
|
||||
if (perfStatsUpdater != null) {
|
||||
|
@ -349,7 +406,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
|
||||
isInFoldableLayout = true
|
||||
binding.surfaceInputOverlay.layout = InputOverlay.FOLDABLE
|
||||
refreshInputOverlay()
|
||||
}
|
||||
}
|
||||
it.isSeparating
|
||||
|
@ -437,7 +493,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
.apply()
|
||||
}
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
refreshInputOverlay()
|
||||
binding.surfaceInputOverlay.refreshControls()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNeutralButton(R.string.emulation_toggle_all) { _, _ -> }
|
||||
|
@ -461,7 +517,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
R.id.menu_show_overlay -> {
|
||||
it.isChecked = !it.isChecked
|
||||
EmulationMenuSettings.showOverlay = it.isChecked
|
||||
refreshInputOverlay()
|
||||
binding.surfaceInputOverlay.refreshControls()
|
||||
true
|
||||
}
|
||||
|
||||
|
@ -567,14 +623,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||
preferences.edit()
|
||||
.putInt(Settings.PREF_CONTROL_SCALE, scale)
|
||||
.apply()
|
||||
refreshInputOverlay()
|
||||
binding.surfaceInputOverlay.refreshControls()
|
||||
}
|
||||
|
||||
private fun setControlOpacity(opacity: Int) {
|
||||
preferences.edit()
|
||||
.putInt(Settings.PREF_CONTROL_OPACITY, opacity)
|
||||
.apply()
|
||||
refreshInputOverlay()
|
||||
binding.surfaceInputOverlay.refreshControls()
|
||||
}
|
||||
|
||||
private fun setInsets() {
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.model
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class EmulationViewModel : ViewModel() {
|
||||
private val _emulationStarted = MutableLiveData(false)
|
||||
val emulationStarted: LiveData<Boolean> get() = _emulationStarted
|
||||
|
||||
private val _isEmulationStopping = MutableLiveData(false)
|
||||
val isEmulationStopping: LiveData<Boolean> get() = _isEmulationStopping
|
||||
|
||||
private val _shaderProgress = MutableLiveData(0)
|
||||
val shaderProgress: LiveData<Int> get() = _shaderProgress
|
||||
|
||||
private val _totalShaders = MutableLiveData(0)
|
||||
val totalShaders: LiveData<Int> get() = _totalShaders
|
||||
|
||||
private val _shaderMessage = MutableLiveData("")
|
||||
val shaderMessage: LiveData<String> get() = _shaderMessage
|
||||
|
||||
fun setEmulationStarted(started: Boolean) {
|
||||
_emulationStarted.postValue(started)
|
||||
}
|
||||
|
||||
fun setIsEmulationStopping(value: Boolean) {
|
||||
_isEmulationStopping.value = value
|
||||
}
|
||||
|
||||
fun setShaderProgress(progress: Int) {
|
||||
_shaderProgress.value = progress
|
||||
}
|
||||
|
||||
fun setTotalShaders(max: Int) {
|
||||
_totalShaders.value = max
|
||||
}
|
||||
|
||||
fun setShaderMessage(msg: String) {
|
||||
_shaderMessage.value = msg
|
||||
}
|
||||
|
||||
fun updateProgress(msg: String, progress: Int, max: Int) {
|
||||
setShaderMessage(msg)
|
||||
setShaderProgress(progress)
|
||||
setTotalShaders(max)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_emulationStarted.value = false
|
||||
_isEmulationStopping.value = false
|
||||
_shaderProgress.value = 0
|
||||
_totalShaders.value = 0
|
||||
_shaderMessage.value = ""
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.utils
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.widget.ImageView
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DataSource
|
||||
import coil.fetch.DrawableResult
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.key.Keyer
|
||||
import coil.memory.MemoryCache
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.Options
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import org.yuzu.yuzu_emu.model.Game
|
||||
|
||||
class GameIconFetcher(
|
||||
private val game: Game,
|
||||
private val options: Options
|
||||
) : Fetcher {
|
||||
override suspend fun fetch(): FetchResult {
|
||||
return DrawableResult(
|
||||
drawable = decodeGameIcon(game.path)!!.toDrawable(options.context.resources),
|
||||
isSampled = false,
|
||||
dataSource = DataSource.DISK
|
||||
)
|
||||
}
|
||||
|
||||
private fun decodeGameIcon(uri: String): Bitmap? {
|
||||
val data = NativeLibrary.getIcon(uri)
|
||||
return BitmapFactory.decodeByteArray(
|
||||
data,
|
||||
0,
|
||||
data.size,
|
||||
BitmapFactory.Options()
|
||||
)
|
||||
}
|
||||
|
||||
class Factory : Fetcher.Factory<Game> {
|
||||
override fun create(data: Game, options: Options, imageLoader: ImageLoader): Fetcher =
|
||||
GameIconFetcher(data, options)
|
||||
}
|
||||
}
|
||||
|
||||
class GameIconKeyer : Keyer<Game> {
|
||||
override fun key(data: Game, options: Options): String = data.path
|
||||
}
|
||||
|
||||
object GameIconUtils {
|
||||
private val imageLoader = ImageLoader.Builder(YuzuApplication.appContext)
|
||||
.components {
|
||||
add(GameIconKeyer())
|
||||
add(GameIconFetcher.Factory())
|
||||
}
|
||||
.memoryCache {
|
||||
MemoryCache.Builder(YuzuApplication.appContext)
|
||||
.maxSizePercent(0.25)
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
|
||||
fun loadGameIcon(game: Game, imageView: ImageView) {
|
||||
val request = ImageRequest.Builder(YuzuApplication.appContext)
|
||||
.data(game)
|
||||
.target(imageView)
|
||||
.error(R.drawable.default_icon)
|
||||
.build()
|
||||
imageLoader.enqueue(request)
|
||||
}
|
||||
}
|
|
@ -15,6 +15,8 @@ static jclass s_disk_cache_progress_class;
|
|||
static jclass s_load_callback_stage_class;
|
||||
static jmethodID s_exit_emulation_activity;
|
||||
static jmethodID s_disk_cache_load_progress;
|
||||
static jmethodID s_on_emulation_started;
|
||||
static jmethodID s_on_emulation_stopped;
|
||||
|
||||
static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
|
||||
|
||||
|
@ -59,6 +61,14 @@ jmethodID GetDiskCacheLoadProgress() {
|
|||
return s_disk_cache_load_progress;
|
||||
}
|
||||
|
||||
jmethodID GetOnEmulationStarted() {
|
||||
return s_on_emulation_started;
|
||||
}
|
||||
|
||||
jmethodID GetOnEmulationStopped() {
|
||||
return s_on_emulation_stopped;
|
||||
}
|
||||
|
||||
} // namespace IDCache
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
@ -85,6 +95,10 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
|||
env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
|
||||
s_disk_cache_load_progress =
|
||||
env->GetStaticMethodID(s_disk_cache_progress_class, "loadProgress", "(III)V");
|
||||
s_on_emulation_started =
|
||||
env->GetStaticMethodID(s_native_library_class, "onEmulationStarted", "()V");
|
||||
s_on_emulation_stopped =
|
||||
env->GetStaticMethodID(s_native_library_class, "onEmulationStopped", "(I)V");
|
||||
|
||||
// Initialize Android Storage
|
||||
Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
|
||||
|
|
|
@ -15,5 +15,7 @@ jclass GetDiskCacheProgressClass();
|
|||
jclass GetDiskCacheLoadCallbackStageClass();
|
||||
jmethodID GetExitEmulationActivity();
|
||||
jmethodID GetDiskCacheLoadProgress();
|
||||
jmethodID GetOnEmulationStarted();
|
||||
jmethodID GetOnEmulationStopped();
|
||||
|
||||
} // namespace IDCache
|
||||
|
|
|
@ -203,12 +203,10 @@ public:
|
|||
}
|
||||
|
||||
bool IsRunning() const {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
return m_is_running;
|
||||
}
|
||||
|
||||
bool IsPaused() const {
|
||||
std::scoped_lock lock(m_mutex);
|
||||
return m_is_running && m_is_paused;
|
||||
}
|
||||
|
||||
|
@ -335,6 +333,8 @@ public:
|
|||
|
||||
// Tear down the render window.
|
||||
m_window.reset();
|
||||
|
||||
OnEmulationStopped(m_load_result);
|
||||
}
|
||||
|
||||
void PauseEmulation() {
|
||||
|
@ -376,6 +376,8 @@ public:
|
|||
m_system.InitializeDebugger();
|
||||
}
|
||||
|
||||
OnEmulationStarted();
|
||||
|
||||
while (true) {
|
||||
{
|
||||
[[maybe_unused]] std::unique_lock lock(m_mutex);
|
||||
|
@ -511,6 +513,18 @@ private:
|
|||
static_cast<jint>(progress), static_cast<jint>(max));
|
||||
}
|
||||
|
||||
static void OnEmulationStarted() {
|
||||
JNIEnv* env = IDCache::GetEnvForThread();
|
||||
env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
|
||||
IDCache::GetOnEmulationStarted());
|
||||
}
|
||||
|
||||
static void OnEmulationStopped(Core::SystemResultStatus result) {
|
||||
JNIEnv* env = IDCache::GetEnvForThread();
|
||||
env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(),
|
||||
IDCache::GetOnEmulationStopped(), static_cast<jint>(result));
|
||||
}
|
||||
|
||||
private:
|
||||
static EmulationSession s_instance;
|
||||
|
||||
|
@ -528,8 +542,8 @@ private:
|
|||
Core::PerfStatsResults m_perf_stats{};
|
||||
std::shared_ptr<FileSys::VfsFilesystem> m_vfs;
|
||||
Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
|
||||
bool m_is_running{};
|
||||
bool m_is_paused{};
|
||||
std::atomic<bool> m_is_running = false;
|
||||
std::atomic<bool> m_is_paused = false;
|
||||
SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
|
||||
std::unique_ptr<Service::Account::ProfileManager> m_profile_manager;
|
||||
std::unique_ptr<FileSys::ManualContentProvider> m_manual_provider;
|
||||
|
|
|
@ -26,6 +26,81 @@
|
|||
android:focusable="false"
|
||||
android:focusableInTouchMode="false" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/loading_indicator"
|
||||
style="?attr/materialCardViewOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:focusable="false">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/loading_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/loading_image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:adjustViewBounds="true"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/linearLayout"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/linearLayout"
|
||||
tools:src="@drawable/default_icon" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:paddingVertical="36dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/loading_image"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/loading_title"
|
||||
style="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:requiresFadingEdge="horizontal"
|
||||
android:singleLine="true"
|
||||
android:textAlignment="viewStart"
|
||||
tools:text="@string/games" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/loading_text"
|
||||
style="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:requiresFadingEdge="horizontal"
|
||||
android:singleLine="true"
|
||||
android:text="@string/loading"
|
||||
android:textAlignment="viewStart" />
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/loading_progress_indicator"
|
||||
android:layout_width="192dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:indeterminate="true"
|
||||
app:trackCornerRadius="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
|
@ -41,11 +116,12 @@
|
|||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true" />
|
||||
android:focusableInTouchMode="true"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Material3.Button.ElevatedButton"
|
||||
android:id="@+id/done_control_config"
|
||||
style="@style/Widget.Material3.Button.ElevatedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
|
@ -81,6 +157,7 @@
|
|||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start|bottom"
|
||||
app:headerLayout="@layout/header_in_game"
|
||||
app:menu="@menu/menu_in_game" />
|
||||
app:menu="@menu/menu_in_game"
|
||||
tools:visibility="gone" />
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
|
|
|
@ -209,7 +209,6 @@
|
|||
<string name="emulation_pause">Emulation pausieren</string>
|
||||
<string name="emulation_unpause">Emulation fortsetzen</string>
|
||||
<string name="emulation_input_overlay">Overlay-Optionen</string>
|
||||
<string name="emulation_game_loading">Spiel lädt…</string>
|
||||
|
||||
<string name="load_settings">Lädt Einstellungen...</string>
|
||||
|
||||
|
|
|
@ -213,7 +213,6 @@
|
|||
<string name="emulation_pause">Pausar Emulación</string>
|
||||
<string name="emulation_unpause">Reanudar Emulación</string>
|
||||
<string name="emulation_input_overlay">Opciones de pantalla </string>
|
||||
<string name="emulation_game_loading">Cargando juego...</string>
|
||||
|
||||
<string name="load_settings">Cargando configuración...</string>
|
||||
|
||||
|
|
|
@ -213,7 +213,6 @@
|
|||
<string name="emulation_pause">Mettre en pause l\'émulation</string>
|
||||
<string name="emulation_unpause">Reprendre l\'émulation</string>
|
||||
<string name="emulation_input_overlay">Options de l\'overlay</string>
|
||||
<string name="emulation_game_loading">Chargement du jeu...</string>
|
||||
|
||||
<string name="load_settings">Chargement des paramètres…</string>
|
||||
|
||||
|
|
|
@ -213,7 +213,6 @@
|
|||
<string name="emulation_pause">Metti in pausa l\'emulazione</string>
|
||||
<string name="emulation_unpause">Riprendi Emulazione</string>
|
||||
<string name="emulation_input_overlay">Impostazioni Overlay</string>
|
||||
<string name="emulation_game_loading">Caricamento del gioco...</string>
|
||||
|
||||
<string name="load_settings">Caricamento delle impostazioni...</string>
|
||||
|
||||
|
|
|
@ -211,7 +211,6 @@
|
|||
<string name="emulation_pause">エミュレーションを一時停止</string>
|
||||
<string name="emulation_unpause">エミュレーションを再開</string>
|
||||
<string name="emulation_input_overlay">オーバーレイオプション</string>
|
||||
<string name="emulation_game_loading">ロード中…</string>
|
||||
|
||||
<string name="load_settings">設定をロード中…</string>
|
||||
|
||||
|
|
|
@ -213,7 +213,6 @@
|
|||
<string name="emulation_pause">에뮬레이션 일시 중지</string>
|
||||
<string name="emulation_unpause">에뮬레이션 일시 중지 해제</string>
|
||||
<string name="emulation_input_overlay">오버레이 옵션</string>
|
||||
<string name="emulation_game_loading">게임 불러오기 중...</string>
|
||||
|
||||
<string name="load_settings">설정 불러오기 중...</string>
|
||||
|
||||
|
|
|
@ -213,7 +213,6 @@
|
|||
<string name="emulation_pause">Pause Emulering</string>
|
||||
<string name="emulation_unpause">Opphev pausing av emulering</string>
|
||||
<string name="emulation_input_overlay">Alternativer for overlegg</string>
|
||||
<string name="emulation_game_loading">Spillet lastes inn...</string>
|
||||
|
||||
<string name="load_settings">Laster inn innstillinger...</string>
|
||||
|
||||
|
|
|
@ -213,7 +213,6 @@
|
|||
<string name="emulation_pause">Wstrzymaj emulację</string>
|
||||
<string name="emulation_unpause">Wznów emulację</string>
|
||||
<string name="emulation_input_overlay">Opcje nakładki</string>
|
||||
<string name="emulation_game_loading">Wczytywanie gry...</string>
|
||||
|
||||
<string name="load_settings">Wczytywanie ustawień...</string>
|
||||
|
||||
|
|
|
@ -213,7 +213,6 @@
|
|||
<string name="emulation_pause">Pausa emulação</string>
|
||||
<string name="emulation_unpause">Retomar emulação</string>
|
||||
<string name="emulation_input_overlay">Opções de sobreposição </string>
|
||||
<string name="emulation_game_loading">Jogo a carregar...</string>
|
||||
|
||||
<string name="load_settings">Configurações a carregar...</string>
|
||||
|
||||
|
|
|
@ -213,7 +213,6 @@
|
|||
<string name="emulation_pause">Pausa emulação</string>
|
||||
<string name="emulation_unpause">Retomar emulação</string>
|
||||
<string name="emulation_input_overlay">Opções de sobreposição </string>
|
||||
<string name="emulation_game_loading">Jogo a carregar...</string>
|
||||
|
||||
<string name="load_settings">Configurações a carregar...</string>
|
||||
|
||||
|
|
|
@ -213,7 +213,6 @@
|
|||
<string name="emulation_pause">Пауза эмуляции</string>
|
||||
<string name="emulation_unpause">Возобновление эмуляции</string>
|
||||
<string name="emulation_input_overlay">Настройки оверлея</string>
|
||||
<string name="emulation_game_loading">Загрузка игры...</string>
|
||||
|
||||
<string name="load_settings">Загрузка настроек...</string>
|
||||
|
||||
|
|
|
@ -213,7 +213,6 @@
|
|||
<string name="emulation_pause">Пауза емуляції</string>
|
||||
<string name="emulation_unpause">Відновлення емуляції</string>
|
||||
<string name="emulation_input_overlay">Налаштування оверлея</string>
|
||||
<string name="emulation_game_loading">Завантаження гри...</string>
|
||||
|
||||
<string name="load_settings">Завантаження налаштувань...</string>
|
||||
|
||||
|
|
|
@ -213,7 +213,6 @@
|
|||
<string name="emulation_pause">暂停模拟</string>
|
||||
<string name="emulation_unpause">继续模拟</string>
|
||||
<string name="emulation_input_overlay">虚拟按键选项</string>
|
||||
<string name="emulation_game_loading">载入游戏中…</string>
|
||||
|
||||
<string name="load_settings">正在载入设定…</string>
|
||||
|
||||
|
|
|
@ -213,7 +213,6 @@
|
|||
<string name="emulation_pause">暫停模擬</string>
|
||||
<string name="emulation_unpause">取消暫停模擬</string>
|
||||
<string name="emulation_input_overlay">覆疊選項</string>
|
||||
<string name="emulation_game_loading">遊戲正在載入…</string>
|
||||
|
||||
<string name="load_settings">正在載入設定…</string>
|
||||
|
||||
|
|
|
@ -204,6 +204,7 @@
|
|||
<string name="error_saving">Error saving %1$s.ini: %2$s</string>
|
||||
<string name="unimplemented_menu">Unimplemented Menu</string>
|
||||
<string name="loading">Loading…</string>
|
||||
<string name="shutting_down">Shutting down…</string>
|
||||
<string name="reset_setting_confirmation">Do you want to reset this setting back to its default value?</string>
|
||||
<string name="reset_to_default">Reset to default</string>
|
||||
<string name="reset_all_settings">Reset all settings?</string>
|
||||
|
@ -262,7 +263,6 @@
|
|||
<string name="emulation_pause">Pause emulation</string>
|
||||
<string name="emulation_unpause">Unpause emulation</string>
|
||||
<string name="emulation_input_overlay">Overlay options</string>
|
||||
<string name="emulation_game_loading">Game loading…</string>
|
||||
|
||||
<string name="load_settings">Loading settings…</string>
|
||||
|
||||
|
|
Reference in New Issue