diff options
Diffstat (limited to 'src/android')
13 files changed, 324 insertions, 29 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index 010c44951..b7556e353 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt @@ -548,6 +548,15 @@ object NativeLibrary { external fun getSavePath(programId: String): String /** + * Gets the root save directory for the default profile as either + * /user/save/account/<user id raw string> or /user/save/000...000/<user id> + * + * @param future If true, returns the /user/save/account/... directory + * @return Save data path that may not exist yet + */ + external fun getDefaultProfileSaveDataRoot(future: Boolean): String + + /** * Adds a file to the manual filesystem provider in our EmulationSession instance * @param path Path to the file we're adding. Can be a string representation of a [Uri] or * a normal path 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 43caac989..fee80bb21 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 @@ -79,7 +79,18 @@ object Settings { const val PREF_THEME_MODE = "ThemeMode" const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds" - const val LayoutOption_Unspecified = 0 - const val LayoutOption_MobilePortrait = 4 - const val LayoutOption_MobileLandscape = 5 + enum class EmulationOrientation(val int: Int) { + Unspecified(0), + SensorLandscape(5), + Landscape(1), + ReverseLandscape(2), + SensorPortrait(6), + Portrait(4), + ReversePortrait(3); + + companion object { + fun from(int: Int): EmulationOrientation = + entries.firstOrNull { it.int == int } ?: Unspecified + } + } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index 510b2b5eb..9efc1705d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -50,6 +50,7 @@ import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding 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.features.settings.model.Settings.EmulationOrientation import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.Game @@ -99,6 +100,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { */ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + updateOrientation() val intentUri: Uri? = requireActivity().intent.data var intentGame: Game? = null @@ -458,13 +460,23 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { @SuppressLint("SourceLockedOrientationActivity") private fun updateOrientation() { emulationActivity?.let { - it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.getInt()) { - Settings.LayoutOption_MobileLandscape -> + val orientationSetting = + EmulationOrientation.from(IntSetting.RENDERER_SCREEN_LAYOUT.getInt()) + it.requestedOrientation = when (orientationSetting) { + EmulationOrientation.Unspecified -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + EmulationOrientation.SensorLandscape -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE - Settings.LayoutOption_MobilePortrait -> - ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT - Settings.LayoutOption_Unspecified -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED - else -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + + EmulationOrientation.Landscape -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + EmulationOrientation.ReverseLandscape -> + ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE + + EmulationOrientation.SensorPortrait -> + ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT + + EmulationOrientation.Portrait -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + EmulationOrientation.ReversePortrait -> + ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT } } } @@ -651,7 +663,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { @SuppressLint("SourceLockedOrientationActivity") private fun startConfiguringControls() { // Lock the current orientation to prevent editing inconsistencies - if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == Settings.LayoutOption_Unspecified) { + if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == EmulationOrientation.Unspecified.int) { emulationActivity?.let { it.requestedOrientation = if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { @@ -669,7 +681,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { binding.doneControlConfig.visibility = View.GONE binding.surfaceInputOverlay.setIsInEditMode(false) // Unlock the orientation if it was locked for editing - if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == Settings.LayoutOption_Unspecified) { + if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == EmulationOrientation.Unspecified.int) { emulationActivity?.let { it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt index b1d3c0040..b04d1208f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt @@ -445,7 +445,8 @@ class GamePropertiesFragment : Fragment() { val zipResult = FileUtil.zipFromInternalStorage( File(saveLocation), saveLocation.replaceAfterLast("/", ""), - BufferedOutputStream(requireContext().contentResolver.openOutputStream(result)) + BufferedOutputStream(requireContext().contentResolver.openOutputStream(result)), + compression = false ) return@newInstance when (zipResult) { TaskState.Completed -> getString(R.string.export_success) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt index 569727b90..5b4bf2c9f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt @@ -7,20 +7,39 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.findNavController import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.transition.MaterialSharedAxis +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.adapters.InstallableAdapter import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.Installable +import org.yuzu.yuzu_emu.model.TaskState import org.yuzu.yuzu_emu.ui.main.MainActivity +import org.yuzu.yuzu_emu.utils.DirectoryInitialization +import org.yuzu.yuzu_emu.utils.FileUtil +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.File +import java.math.BigInteger +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter class InstallableFragment : Fragment() { private var _binding: FragmentInstallablesBinding? = null @@ -56,6 +75,17 @@ class InstallableFragment : Fragment() { binding.root.findNavController().popBackStack() } + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + homeViewModel.openImportSaves.collect { + if (it) { + importSaves.launch(arrayOf("application/zip")) + homeViewModel.setOpenImportSaves(false) + } + } + } + } + val installables = listOf( Installable( R.string.user_data, @@ -64,6 +94,43 @@ class InstallableFragment : Fragment() { export = { mainActivity.exportUserData.launch("export.zip") } ), Installable( + R.string.manage_save_data, + R.string.manage_save_data_description, + install = { + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.import_save_warning, + descriptionId = R.string.import_save_warning_description, + positiveAction = { homeViewModel.setOpenImportSaves(true) } + ).show(parentFragmentManager, MessageDialogFragment.TAG) + }, + export = { + val oldSaveDataFolder = File( + "${DirectoryInitialization.userDirectory}/nand" + + NativeLibrary.getDefaultProfileSaveDataRoot(false) + ) + val futureSaveDataFolder = File( + "${DirectoryInitialization.userDirectory}/nand" + + NativeLibrary.getDefaultProfileSaveDataRoot(true) + ) + if (!oldSaveDataFolder.exists() && !futureSaveDataFolder.exists()) { + Toast.makeText( + YuzuApplication.appContext, + R.string.no_save_data_found, + Toast.LENGTH_SHORT + ).show() + return@Installable + } else { + exportSaves.launch( + "${getString(R.string.save_data)} " + + LocalDateTime.now().format( + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + ) + ) + } + } + ), + Installable( R.string.install_game_content, R.string.install_game_content_description, install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) } @@ -121,4 +188,156 @@ class InstallableFragment : Fragment() { windowInsets } + + private val importSaves = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result == null) { + return@registerForActivityResult + } + + val inputZip = requireContext().contentResolver.openInputStream(result) + val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") + cacheSaveDir.mkdir() + + if (inputZip == null) { + Toast.makeText( + YuzuApplication.appContext, + getString(R.string.fatal_error), + Toast.LENGTH_LONG + ).show() + return@registerForActivityResult + } + + IndeterminateProgressDialogFragment.newInstance( + requireActivity(), + R.string.save_files_importing, + false + ) { + try { + FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) + val files = cacheSaveDir.listFiles() + var successfulImports = 0 + var failedImports = 0 + if (files != null) { + for (file in files) { + if (file.isDirectory) { + val baseSaveDir = + NativeLibrary.getSavePath(BigInteger(file.name, 16).toString()) + if (baseSaveDir.isEmpty()) { + failedImports++ + continue + } + + val internalSaveFolder = File( + "${DirectoryInitialization.userDirectory}/nand$baseSaveDir" + ) + internalSaveFolder.deleteRecursively() + internalSaveFolder.mkdir() + file.copyRecursively(target = internalSaveFolder, overwrite = true) + successfulImports++ + } + } + } + + withContext(Dispatchers.Main) { + if (successfulImports == 0) { + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.save_file_invalid_zip_structure, + descriptionId = R.string.save_file_invalid_zip_structure_description + ).show(parentFragmentManager, MessageDialogFragment.TAG) + return@withContext + } + val successString = if (failedImports > 0) { + """ + ${ + requireContext().resources.getQuantityString( + R.plurals.saves_import_success, + successfulImports, + successfulImports + ) + } + ${ + requireContext().resources.getQuantityString( + R.plurals.saves_import_failed, + failedImports, + failedImports + ) + } + """ + } else { + requireContext().resources.getQuantityString( + R.plurals.saves_import_success, + successfulImports, + successfulImports + ) + } + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.import_complete, + descriptionString = successString + ).show(parentFragmentManager, MessageDialogFragment.TAG) + } + + cacheSaveDir.deleteRecursively() + } catch (e: Exception) { + Toast.makeText( + YuzuApplication.appContext, + getString(R.string.fatal_error), + Toast.LENGTH_LONG + ).show() + } + }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) + } + + private val exportSaves = registerForActivityResult( + ActivityResultContracts.CreateDocument("application/zip") + ) { result -> + if (result == null) { + return@registerForActivityResult + } + + IndeterminateProgressDialogFragment.newInstance( + requireActivity(), + R.string.save_files_exporting, + false + ) { + val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") + cacheSaveDir.mkdir() + + val oldSaveDataFolder = File( + "${DirectoryInitialization.userDirectory}/nand" + + NativeLibrary.getDefaultProfileSaveDataRoot(false) + ) + if (oldSaveDataFolder.exists()) { + oldSaveDataFolder.copyRecursively(cacheSaveDir) + } + + val futureSaveDataFolder = File( + "${DirectoryInitialization.userDirectory}/nand" + + NativeLibrary.getDefaultProfileSaveDataRoot(true) + ) + if (futureSaveDataFolder.exists()) { + futureSaveDataFolder.copyRecursively(cacheSaveDir) + } + + val saveFilesTotal = cacheSaveDir.listFiles()?.size ?: 0 + if (saveFilesTotal == 0) { + cacheSaveDir.deleteRecursively() + return@newInstance getString(R.string.no_save_data_found) + } + + val zipResult = FileUtil.zipFromInternalStorage( + cacheSaveDir, + cacheSaveDir.path, + BufferedOutputStream(requireContext().contentResolver.openOutputStream(result)) + ) + cacheSaveDir.deleteRecursively() + + return@newInstance when (zipResult) { + TaskState.Completed -> getString(R.string.export_success) + TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) + } + }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) + } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt index d19f20dc2..5ae05b5cc 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt @@ -167,13 +167,14 @@ class GamesViewModel : ViewModel() { } } - fun onCloseGameFoldersFragment() = + fun onCloseGameFoldersFragment() { + NativeConfig.saveGlobalConfig() viewModelScope.launch { withContext(Dispatchers.IO) { - NativeConfig.saveGlobalConfig() getGameDirs(true) } } + } private fun getGameDirs(reloadList: Boolean = false) { val gameDirs = NativeConfig.getGameDirs() 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 b4117d761..622ae996e 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 @@ -625,7 +625,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider { File(DirectoryInitialization.userDirectory!!), DirectoryInitialization.userDirectory!!, BufferedOutputStream(contentResolver.openOutputStream(result)), - taskViewModel.cancelled + taskViewModel.cancelled, + compression = false ) return@newInstance when (zipResult) { TaskState.Completed -> getString(R.string.user_data_export_success) 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 00c6bf90e..132f002fb 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 @@ -21,6 +21,7 @@ import org.yuzu.yuzu_emu.model.TaskState import java.io.BufferedOutputStream import java.lang.NullPointerException import java.nio.charset.StandardCharsets +import java.util.zip.Deflater import java.util.zip.ZipOutputStream import kotlin.IllegalStateException @@ -312,15 +313,23 @@ object FileUtil { * @param inputFile File representation of the item that will be zipped * @param rootDir Directory containing the inputFile * @param outputStream Stream where the zip file will be output + * @param cancelled [StateFlow] that reports whether this process has been cancelled + * @param compression Disables compression if true */ fun zipFromInternalStorage( inputFile: File, rootDir: String, outputStream: BufferedOutputStream, - cancelled: StateFlow<Boolean>? = null + cancelled: StateFlow<Boolean>? = null, + compression: Boolean = true ): TaskState { try { ZipOutputStream(outputStream).use { zos -> + if (!compression) { + zos.setMethod(ZipOutputStream.DEFLATED) + zos.setLevel(Deflater.NO_COMPRESSION) + } + inputFile.walkTopDown().forEach { file -> if (cancelled?.value == true) { return TaskState.Cancelled @@ -338,6 +347,7 @@ object FileUtil { } } } catch (e: Exception) { + Log.error("[FileUtil] Failed creating zip file - ${e.message}") return TaskState.Failed } return TaskState.Completed diff --git a/src/android/app/src/main/jni/android_config.cpp b/src/android/app/src/main/jni/android_config.cpp index c86aa1c39..08aed3216 100644 --- a/src/android/app/src/main/jni/android_config.cpp +++ b/src/android/app/src/main/jni/android_config.cpp @@ -14,12 +14,6 @@ AndroidConfig::AndroidConfig(const std::string& config_name, ConfigType config_t } } -AndroidConfig::~AndroidConfig() { - if (global) { - AndroidConfig::SaveAllValues(); - } -} - void AndroidConfig::ReloadAllValues() { Reload(); ReadAndroidValues(); diff --git a/src/android/app/src/main/jni/android_config.h b/src/android/app/src/main/jni/android_config.h index d83852de9..693e1e3f0 100644 --- a/src/android/app/src/main/jni/android_config.h +++ b/src/android/app/src/main/jni/android_config.h @@ -9,7 +9,6 @@ class AndroidConfig final : public Config { public: explicit AndroidConfig(const std::string& config_name = "config", ConfigType config_type = ConfigType::GlobalConfig); - ~AndroidConfig() override; void ReloadAllValues() override; void SaveAllValues() override; diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 0c1db7d46..136c8dee6 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -45,15 +45,15 @@ #include "core/frontend/applets/profile_select.h" #include "core/frontend/applets/software_keyboard.h" #include "core/frontend/applets/web_browser.h" -#include "core/hid/emulated_controller.h" -#include "core/hid/hid_core.h" -#include "core/hid/hid_types.h" #include "core/hle/service/am/applet_ae.h" #include "core/hle/service/am/applet_oe.h" #include "core/hle/service/am/applets/applets.h" #include "core/hle/service/filesystem/filesystem.h" #include "core/loader/loader.h" #include "frontend_common/config.h" +#include "hid_core/frontend/emulated_controller.h" +#include "hid_core/hid_core.h" +#include "hid_core/hid_types.h" #include "jni/android_common/android_common.h" #include "jni/id_cache.h" #include "jni/native.h" @@ -862,6 +862,9 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj, jstring jprogramId) { auto program_id = EmulationSession::GetProgramId(env, jprogramId); + if (program_id == 0) { + return ToJString(env, ""); + } auto& system = EmulationSession::GetInstance().System(); @@ -880,6 +883,19 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject j return ToJString(env, user_save_data_path); } +jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getDefaultProfileSaveDataRoot(JNIEnv* env, + jobject jobj, + jboolean jfuture) { + Service::Account::ProfileManager manager; + // TODO: Pass in a selected user once we get the relevant UI working + const auto user_id = manager.GetUser(static_cast<std::size_t>(0)); + ASSERT(user_id); + + const auto user_save_data_root = + FileSys::SaveDataFactory::GetUserGameSaveDataRoot(user_id->AsU128(), jfuture); + return ToJString(env, user_save_data_root); +} + void Java_org_yuzu_yuzu_1emu_NativeLibrary_addFileToFilesystemProvider(JNIEnv* env, jobject jobj, jstring jpath) { EmulationSession::GetInstance().ConfigureFilesystemProvider(GetJString(env, jpath)); diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index 45d57c3ea..0363ff3b6 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -118,15 +118,23 @@ </integer-array> <string-array name="rendererScreenLayoutNames"> + <item>@string/screen_layout_auto</item> + <item>@string/screen_layout_sensor_landscape</item> <item>@string/screen_layout_landscape</item> + <item>@string/screen_layout_reverse_landscape</item> + <item>@string/screen_layout_sensor_portrait</item> <item>@string/screen_layout_portrait</item> - <item>@string/screen_layout_auto</item> + <item>@string/screen_layout_reverse_portrait</item> </string-array> <integer-array name="rendererScreenLayoutValues"> + <item>0</item> <item>5</item> + <item>1</item> + <item>2</item> + <item>6</item> <item>4</item> - <item>0</item> + <item>3</item> </integer-array> <string-array name="rendererAspectRatioNames"> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 1bedcb1ef..3bb92ad67 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -133,6 +133,15 @@ <string name="add_game_folder">Add game folder</string> <string name="folder_already_added">This folder was already added!</string> <string name="game_folder_properties">Game folder properties</string> + <plurals name="saves_import_failed"> + <item quantity="one">Failed to import %d save</item> + <item quantity="other">Failed to import %d saves</item> + </plurals> + <plurals name="saves_import_success"> + <item quantity="one">Successfully imported %d save</item> + <item quantity="other">Successfully imported %d saves</item> + </plurals> + <string name="no_save_data_found">No save data found</string> <!-- Applet launcher strings --> <string name="applets">Applet launcher</string> @@ -276,6 +285,7 @@ <string name="global">Global</string> <string name="custom">Custom</string> <string name="notice">Notice</string> + <string name="import_complete">Import complete</string> <!-- GPU driver installation --> <string name="select_gpu_driver">Select GPU driver</string> @@ -463,9 +473,13 @@ <string name="anti_aliasing_smaa">SMAA</string> <!-- Screen Layouts --> + <string name="screen_layout_auto">Auto</string> + <string name="screen_layout_sensor_landscape">Sensor landscape</string> <string name="screen_layout_landscape">Landscape</string> + <string name="screen_layout_reverse_landscape">Reverse landscape</string> + <string name="screen_layout_sensor_portrait">Sensor portrait</string> <string name="screen_layout_portrait">Portrait</string> - <string name="screen_layout_auto">Auto</string> + <string name="screen_layout_reverse_portrait">Reverse portrait</string> <!-- Aspect Ratios --> <string name="ratio_default">Default (16:9)</string> |