diff options
30 files changed, 516 insertions, 339 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 21f67f32a..f474a3873 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 | |||
| @@ -512,6 +512,11 @@ object NativeLibrary { | |||
| 512 | external fun submitInlineKeyboardInput(key_code: Int) | 512 | external fun submitInlineKeyboardInput(key_code: Int) |
| 513 | 513 | ||
| 514 | /** | 514 | /** |
| 515 | * Creates a generic user directory if it doesn't exist already | ||
| 516 | */ | ||
| 517 | external fun initializeEmptyUserDirectory() | ||
| 518 | |||
| 519 | /** | ||
| 515 | * Button type for use in onTouchEvent | 520 | * Button type for use in onTouchEvent |
| 516 | */ | 521 | */ |
| 517 | object ButtonType { | 522 | object ButtonType { |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt new file mode 100644 index 000000000..e960fbaab --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt | |||
| @@ -0,0 +1,49 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.adapters | ||
| 5 | |||
| 6 | import android.view.LayoutInflater | ||
| 7 | import android.view.View | ||
| 8 | import android.view.ViewGroup | ||
| 9 | import androidx.recyclerview.widget.RecyclerView | ||
| 10 | import org.yuzu.yuzu_emu.databinding.CardInstallableBinding | ||
| 11 | import org.yuzu.yuzu_emu.model.Installable | ||
| 12 | |||
| 13 | class InstallableAdapter(private val installables: List<Installable>) : | ||
| 14 | RecyclerView.Adapter<InstallableAdapter.InstallableViewHolder>() { | ||
| 15 | override fun onCreateViewHolder( | ||
| 16 | parent: ViewGroup, | ||
| 17 | viewType: Int | ||
| 18 | ): InstallableAdapter.InstallableViewHolder { | ||
| 19 | val binding = | ||
| 20 | CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false) | ||
| 21 | return InstallableViewHolder(binding) | ||
| 22 | } | ||
| 23 | |||
| 24 | override fun getItemCount(): Int = installables.size | ||
| 25 | |||
| 26 | override fun onBindViewHolder(holder: InstallableAdapter.InstallableViewHolder, position: Int) = | ||
| 27 | holder.bind(installables[position]) | ||
| 28 | |||
| 29 | inner class InstallableViewHolder(val binding: CardInstallableBinding) : | ||
| 30 | RecyclerView.ViewHolder(binding.root) { | ||
| 31 | lateinit var installable: Installable | ||
| 32 | |||
| 33 | fun bind(installable: Installable) { | ||
| 34 | this.installable = installable | ||
| 35 | |||
| 36 | binding.title.setText(installable.titleId) | ||
| 37 | binding.description.setText(installable.descriptionId) | ||
| 38 | |||
| 39 | if (installable.install != null) { | ||
| 40 | binding.buttonInstall.visibility = View.VISIBLE | ||
| 41 | binding.buttonInstall.setOnClickListener { installable.install.invoke() } | ||
| 42 | } | ||
| 43 | if (installable.export != null) { | ||
| 44 | binding.buttonExport.visibility = View.VISIBLE | ||
| 45 | binding.buttonExport.setOnClickListener { installable.export.invoke() } | ||
| 46 | } | ||
| 47 | } | ||
| 48 | } | ||
| 49 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt index 7b8f99872..2ff827c6b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt | |||
| @@ -26,7 +26,6 @@ import org.yuzu.yuzu_emu.BuildConfig | |||
| 26 | import org.yuzu.yuzu_emu.R | 26 | import org.yuzu.yuzu_emu.R |
| 27 | import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding | 27 | import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding |
| 28 | import org.yuzu.yuzu_emu.model.HomeViewModel | 28 | import org.yuzu.yuzu_emu.model.HomeViewModel |
| 29 | import org.yuzu.yuzu_emu.ui.main.MainActivity | ||
| 30 | 29 | ||
| 31 | class AboutFragment : Fragment() { | 30 | class AboutFragment : Fragment() { |
| 32 | private var _binding: FragmentAboutBinding? = null | 31 | private var _binding: FragmentAboutBinding? = null |
| @@ -93,12 +92,6 @@ class AboutFragment : Fragment() { | |||
| 93 | } | 92 | } |
| 94 | } | 93 | } |
| 95 | 94 | ||
| 96 | val mainActivity = requireActivity() as MainActivity | ||
| 97 | binding.buttonExport.setOnClickListener { mainActivity.exportUserData.launch("export.zip") } | ||
| 98 | binding.buttonImport.setOnClickListener { | ||
| 99 | mainActivity.importUserData.launch(arrayOf("application/zip")) | ||
| 100 | } | ||
| 101 | |||
| 102 | binding.buttonDiscord.setOnClickListener { openLink(getString(R.string.support_link)) } | 95 | binding.buttonDiscord.setOnClickListener { openLink(getString(R.string.support_link)) } |
| 103 | binding.buttonWebsite.setOnClickListener { openLink(getString(R.string.website_link)) } | 96 | binding.buttonWebsite.setOnClickListener { openLink(getString(R.string.website_link)) } |
| 104 | binding.buttonGithub.setOnClickListener { openLink(getString(R.string.github_link)) } | 97 | binding.buttonGithub.setOnClickListener { openLink(getString(R.string.github_link)) } |
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 c119e69c9..8923c0ea2 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 | |||
| @@ -118,18 +118,13 @@ class HomeSettingsFragment : Fragment() { | |||
| 118 | ) | 118 | ) |
| 119 | add( | 119 | add( |
| 120 | HomeSetting( | 120 | HomeSetting( |
| 121 | R.string.install_amiibo_keys, | 121 | R.string.manage_yuzu_data, |
| 122 | R.string.install_amiibo_keys_description, | 122 | R.string.manage_yuzu_data_description, |
| 123 | R.drawable.ic_nfc, | 123 | R.drawable.ic_install, |
| 124 | { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) } | 124 | { |
| 125 | ) | 125 | binding.root.findNavController() |
| 126 | ) | 126 | .navigate(R.id.action_homeSettingsFragment_to_installableFragment) |
| 127 | add( | 127 | } |
| 128 | HomeSetting( | ||
| 129 | R.string.install_game_content, | ||
| 130 | R.string.install_game_content_description, | ||
| 131 | R.drawable.ic_system_update_alt, | ||
| 132 | { mainActivity.installGameUpdate.launch(arrayOf("*/*")) } | ||
| 133 | ) | 128 | ) |
| 134 | ) | 129 | ) |
| 135 | add( | 130 | add( |
| @@ -150,35 +145,6 @@ class HomeSettingsFragment : Fragment() { | |||
| 150 | ) | 145 | ) |
| 151 | add( | 146 | add( |
| 152 | HomeSetting( | 147 | HomeSetting( |
| 153 | R.string.manage_save_data, | ||
| 154 | R.string.import_export_saves_description, | ||
| 155 | R.drawable.ic_save, | ||
| 156 | { | ||
| 157 | ImportExportSavesFragment().show( | ||
| 158 | parentFragmentManager, | ||
| 159 | ImportExportSavesFragment.TAG | ||
| 160 | ) | ||
| 161 | } | ||
| 162 | ) | ||
| 163 | ) | ||
| 164 | add( | ||
| 165 | HomeSetting( | ||
| 166 | R.string.install_prod_keys, | ||
| 167 | R.string.install_prod_keys_description, | ||
| 168 | R.drawable.ic_unlock, | ||
| 169 | { mainActivity.getProdKey.launch(arrayOf("*/*")) } | ||
| 170 | ) | ||
| 171 | ) | ||
| 172 | add( | ||
| 173 | HomeSetting( | ||
| 174 | R.string.install_firmware, | ||
| 175 | R.string.install_firmware_description, | ||
| 176 | R.drawable.ic_firmware, | ||
| 177 | { mainActivity.getFirmware.launch(arrayOf("application/zip")) } | ||
| 178 | ) | ||
| 179 | ) | ||
| 180 | add( | ||
| 181 | HomeSetting( | ||
| 182 | R.string.share_log, | 148 | R.string.share_log, |
| 183 | R.string.share_log_description, | 149 | R.string.share_log_description, |
| 184 | R.drawable.ic_log, | 150 | R.drawable.ic_log, |
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt deleted file mode 100644 index ee2d44718..000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt +++ /dev/null | |||
| @@ -1,214 +0,0 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.fragments | ||
| 5 | |||
| 6 | import android.app.Dialog | ||
| 7 | import android.content.Intent | ||
| 8 | import android.net.Uri | ||
| 9 | import android.os.Bundle | ||
| 10 | import android.provider.DocumentsContract | ||
| 11 | import android.widget.Toast | ||
| 12 | import androidx.activity.result.ActivityResultLauncher | ||
| 13 | import androidx.activity.result.contract.ActivityResultContracts | ||
| 14 | import androidx.appcompat.app.AppCompatActivity | ||
| 15 | import androidx.documentfile.provider.DocumentFile | ||
| 16 | import androidx.fragment.app.DialogFragment | ||
| 17 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||
| 18 | import java.io.BufferedOutputStream | ||
| 19 | import java.io.File | ||
| 20 | import java.io.FileOutputStream | ||
| 21 | import java.io.FilenameFilter | ||
| 22 | import java.time.LocalDateTime | ||
| 23 | import java.time.format.DateTimeFormatter | ||
| 24 | import java.util.zip.ZipEntry | ||
| 25 | import java.util.zip.ZipOutputStream | ||
| 26 | import kotlinx.coroutines.CoroutineScope | ||
| 27 | import kotlinx.coroutines.Dispatchers | ||
| 28 | import kotlinx.coroutines.launch | ||
| 29 | import kotlinx.coroutines.withContext | ||
| 30 | import org.yuzu.yuzu_emu.R | ||
| 31 | import org.yuzu.yuzu_emu.YuzuApplication | ||
| 32 | import org.yuzu.yuzu_emu.features.DocumentProvider | ||
| 33 | import org.yuzu.yuzu_emu.getPublicFilesDir | ||
| 34 | import org.yuzu.yuzu_emu.utils.FileUtil | ||
| 35 | |||
| 36 | class ImportExportSavesFragment : DialogFragment() { | ||
| 37 | private val context = YuzuApplication.appContext | ||
| 38 | private val savesFolder = | ||
| 39 | "${context.getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000" | ||
| 40 | |||
| 41 | // Get first subfolder in saves folder (should be the user folder) | ||
| 42 | private val savesFolderRoot = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: "" | ||
| 43 | private var lastZipCreated: File? = null | ||
| 44 | |||
| 45 | private lateinit var startForResultExportSave: ActivityResultLauncher<Intent> | ||
| 46 | private lateinit var documentPicker: ActivityResultLauncher<Array<String>> | ||
| 47 | |||
| 48 | override fun onCreate(savedInstanceState: Bundle?) { | ||
| 49 | super.onCreate(savedInstanceState) | ||
| 50 | val activity = requireActivity() as AppCompatActivity | ||
| 51 | |||
| 52 | val activityResultRegistry = requireActivity().activityResultRegistry | ||
| 53 | startForResultExportSave = activityResultRegistry.register( | ||
| 54 | "startForResultExportSaveKey", | ||
| 55 | ActivityResultContracts.StartActivityForResult() | ||
| 56 | ) { | ||
| 57 | File(context.getPublicFilesDir().canonicalPath, "temp").deleteRecursively() | ||
| 58 | } | ||
| 59 | documentPicker = activityResultRegistry.register( | ||
| 60 | "documentPickerKey", | ||
| 61 | ActivityResultContracts.OpenDocument() | ||
| 62 | ) { | ||
| 63 | it?.let { uri -> importSave(uri, activity) } | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | ||
| 68 | return if (savesFolderRoot == "") { | ||
| 69 | MaterialAlertDialogBuilder(requireContext()) | ||
| 70 | .setTitle(R.string.manage_save_data) | ||
| 71 | .setMessage(R.string.import_export_saves_no_profile) | ||
| 72 | .setPositiveButton(android.R.string.ok, null) | ||
| 73 | .show() | ||
| 74 | } else { | ||
| 75 | MaterialAlertDialogBuilder(requireContext()) | ||
| 76 | .setTitle(R.string.manage_save_data) | ||
| 77 | .setMessage(R.string.manage_save_data_description) | ||
| 78 | .setNegativeButton(R.string.export_saves) { _, _ -> | ||
| 79 | exportSave() | ||
| 80 | } | ||
| 81 | .setPositiveButton(R.string.import_saves) { _, _ -> | ||
| 82 | documentPicker.launch(arrayOf("application/zip")) | ||
| 83 | } | ||
| 84 | .setNeutralButton(android.R.string.cancel, null) | ||
| 85 | .show() | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | /** | ||
| 90 | * Zips the save files located in the given folder path and creates a new zip file with the current date and time. | ||
| 91 | * @return true if the zip file is successfully created, false otherwise. | ||
| 92 | */ | ||
| 93 | private fun zipSave(): Boolean { | ||
| 94 | try { | ||
| 95 | val tempFolder = File(requireContext().getPublicFilesDir().canonicalPath, "temp") | ||
| 96 | tempFolder.mkdirs() | ||
| 97 | val saveFolder = File(savesFolderRoot) | ||
| 98 | val outputZipFile = File( | ||
| 99 | tempFolder, | ||
| 100 | "yuzu saves - ${ | ||
| 101 | LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) | ||
| 102 | }.zip" | ||
| 103 | ) | ||
| 104 | outputZipFile.createNewFile() | ||
| 105 | ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos -> | ||
| 106 | saveFolder.walkTopDown().forEach { file -> | ||
| 107 | val zipFileName = | ||
| 108 | file.absolutePath.removePrefix(savesFolderRoot).removePrefix("/") | ||
| 109 | if (zipFileName == "") { | ||
| 110 | return@forEach | ||
| 111 | } | ||
| 112 | val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}") | ||
| 113 | zos.putNextEntry(entry) | ||
| 114 | if (file.isFile) { | ||
| 115 | file.inputStream().use { fis -> fis.copyTo(zos) } | ||
| 116 | } | ||
| 117 | } | ||
| 118 | } | ||
| 119 | lastZipCreated = outputZipFile | ||
| 120 | } catch (e: Exception) { | ||
| 121 | return false | ||
| 122 | } | ||
| 123 | return true | ||
| 124 | } | ||
| 125 | |||
| 126 | /** | ||
| 127 | * Exports the save file located in the given folder path by creating a zip file and sharing it via intent. | ||
| 128 | */ | ||
| 129 | private fun exportSave() { | ||
| 130 | CoroutineScope(Dispatchers.IO).launch { | ||
| 131 | val wasZipCreated = zipSave() | ||
| 132 | val lastZipFile = lastZipCreated | ||
| 133 | if (!wasZipCreated || lastZipFile == null) { | ||
| 134 | withContext(Dispatchers.Main) { | ||
| 135 | Toast.makeText(context, "Failed to export save", Toast.LENGTH_LONG).show() | ||
| 136 | } | ||
| 137 | return@launch | ||
| 138 | } | ||
| 139 | |||
| 140 | withContext(Dispatchers.Main) { | ||
| 141 | val file = DocumentFile.fromSingleUri( | ||
| 142 | context, | ||
| 143 | DocumentsContract.buildDocumentUri( | ||
| 144 | DocumentProvider.AUTHORITY, | ||
| 145 | "${DocumentProvider.ROOT_ID}/temp/${lastZipFile.name}" | ||
| 146 | ) | ||
| 147 | )!! | ||
| 148 | val intent = Intent(Intent.ACTION_SEND) | ||
| 149 | .setDataAndType(file.uri, "application/zip") | ||
| 150 | .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) | ||
| 151 | .putExtra(Intent.EXTRA_STREAM, file.uri) | ||
| 152 | startForResultExportSave.launch(Intent.createChooser(intent, "Share save file")) | ||
| 153 | } | ||
| 154 | } | ||
| 155 | } | ||
| 156 | |||
| 157 | /** | ||
| 158 | * Imports the save files contained in the zip file, and replaces any existing ones with the new save file. | ||
| 159 | * @param zipUri The Uri of the zip file containing the save file(s) to import. | ||
| 160 | */ | ||
| 161 | private fun importSave(zipUri: Uri, activity: AppCompatActivity) { | ||
| 162 | val inputZip = context.contentResolver.openInputStream(zipUri) | ||
| 163 | // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid. | ||
| 164 | var validZip = false | ||
| 165 | val savesFolder = File(savesFolderRoot) | ||
| 166 | val cacheSaveDir = File("${context.cacheDir.path}/saves/") | ||
| 167 | cacheSaveDir.mkdir() | ||
| 168 | |||
| 169 | if (inputZip == null) { | ||
| 170 | Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG) | ||
| 171 | .show() | ||
| 172 | return | ||
| 173 | } | ||
| 174 | |||
| 175 | val filterTitleId = | ||
| 176 | FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) } | ||
| 177 | |||
| 178 | try { | ||
| 179 | CoroutineScope(Dispatchers.IO).launch { | ||
| 180 | FileUtil.unzip(inputZip, cacheSaveDir) | ||
| 181 | cacheSaveDir.list(filterTitleId)?.forEach { savePath -> | ||
| 182 | File(savesFolder, savePath).deleteRecursively() | ||
| 183 | File(cacheSaveDir, savePath).copyRecursively(File(savesFolder, savePath), true) | ||
| 184 | validZip = true | ||
| 185 | } | ||
| 186 | |||
| 187 | withContext(Dispatchers.Main) { | ||
| 188 | if (!validZip) { | ||
| 189 | MessageDialogFragment.newInstance( | ||
| 190 | requireActivity(), | ||
| 191 | titleId = R.string.save_file_invalid_zip_structure, | ||
| 192 | descriptionId = R.string.save_file_invalid_zip_structure_description | ||
| 193 | ).show(activity.supportFragmentManager, MessageDialogFragment.TAG) | ||
| 194 | return@withContext | ||
| 195 | } | ||
| 196 | Toast.makeText( | ||
| 197 | context, | ||
| 198 | context.getString(R.string.save_file_imported_success), | ||
| 199 | Toast.LENGTH_LONG | ||
| 200 | ).show() | ||
| 201 | } | ||
| 202 | |||
| 203 | cacheSaveDir.deleteRecursively() | ||
| 204 | } | ||
| 205 | } catch (e: Exception) { | ||
| 206 | Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG) | ||
| 207 | .show() | ||
| 208 | } | ||
| 209 | } | ||
| 210 | |||
| 211 | companion object { | ||
| 212 | const val TAG = "ImportExportSavesFragment" | ||
| 213 | } | ||
| 214 | } | ||
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 new file mode 100644 index 000000000..ec116ab62 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt | |||
| @@ -0,0 +1,138 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.fragments | ||
| 5 | |||
| 6 | import android.os.Bundle | ||
| 7 | import android.view.LayoutInflater | ||
| 8 | import android.view.View | ||
| 9 | import android.view.ViewGroup | ||
| 10 | import androidx.core.view.ViewCompat | ||
| 11 | import androidx.core.view.WindowInsetsCompat | ||
| 12 | import androidx.core.view.updatePadding | ||
| 13 | import androidx.fragment.app.Fragment | ||
| 14 | import androidx.fragment.app.activityViewModels | ||
| 15 | import androidx.navigation.findNavController | ||
| 16 | import androidx.recyclerview.widget.GridLayoutManager | ||
| 17 | import com.google.android.material.transition.MaterialSharedAxis | ||
| 18 | import org.yuzu.yuzu_emu.R | ||
| 19 | import org.yuzu.yuzu_emu.adapters.InstallableAdapter | ||
| 20 | import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding | ||
| 21 | import org.yuzu.yuzu_emu.model.HomeViewModel | ||
| 22 | import org.yuzu.yuzu_emu.model.Installable | ||
| 23 | import org.yuzu.yuzu_emu.ui.main.MainActivity | ||
| 24 | |||
| 25 | class InstallableFragment : Fragment() { | ||
| 26 | private var _binding: FragmentInstallablesBinding? = null | ||
| 27 | private val binding get() = _binding!! | ||
| 28 | |||
| 29 | private val homeViewModel: HomeViewModel by activityViewModels() | ||
| 30 | |||
| 31 | override fun onCreate(savedInstanceState: Bundle?) { | ||
| 32 | super.onCreate(savedInstanceState) | ||
| 33 | enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) | ||
| 34 | returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) | ||
| 35 | reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) | ||
| 36 | } | ||
| 37 | |||
| 38 | override fun onCreateView( | ||
| 39 | inflater: LayoutInflater, | ||
| 40 | container: ViewGroup?, | ||
| 41 | savedInstanceState: Bundle? | ||
| 42 | ): View { | ||
| 43 | _binding = FragmentInstallablesBinding.inflate(layoutInflater) | ||
| 44 | return binding.root | ||
| 45 | } | ||
| 46 | |||
| 47 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||
| 48 | super.onViewCreated(view, savedInstanceState) | ||
| 49 | |||
| 50 | val mainActivity = requireActivity() as MainActivity | ||
| 51 | |||
| 52 | homeViewModel.setNavigationVisibility(visible = false, animated = true) | ||
| 53 | homeViewModel.setStatusBarShadeVisibility(visible = false) | ||
| 54 | |||
| 55 | binding.toolbarInstallables.setNavigationOnClickListener { | ||
| 56 | binding.root.findNavController().popBackStack() | ||
| 57 | } | ||
| 58 | |||
| 59 | val installables = listOf( | ||
| 60 | Installable( | ||
| 61 | R.string.user_data, | ||
| 62 | R.string.user_data_description, | ||
| 63 | install = { mainActivity.importUserData.launch(arrayOf("application/zip")) }, | ||
| 64 | export = { mainActivity.exportUserData.launch("export.zip") } | ||
| 65 | ), | ||
| 66 | Installable( | ||
| 67 | R.string.install_game_content, | ||
| 68 | R.string.install_game_content_description, | ||
| 69 | install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) } | ||
| 70 | ), | ||
| 71 | Installable( | ||
| 72 | R.string.install_firmware, | ||
| 73 | R.string.install_firmware_description, | ||
| 74 | install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) } | ||
| 75 | ), | ||
| 76 | if (mainActivity.savesFolderRoot != "") { | ||
| 77 | Installable( | ||
| 78 | R.string.manage_save_data, | ||
| 79 | R.string.import_export_saves_description, | ||
| 80 | install = { mainActivity.importSaves.launch(arrayOf("application/zip")) }, | ||
| 81 | export = { mainActivity.exportSave() } | ||
| 82 | ) | ||
| 83 | } else { | ||
| 84 | Installable( | ||
| 85 | R.string.manage_save_data, | ||
| 86 | R.string.import_export_saves_description, | ||
| 87 | install = { mainActivity.importSaves.launch(arrayOf("application/zip")) } | ||
| 88 | ) | ||
| 89 | }, | ||
| 90 | Installable( | ||
| 91 | R.string.install_prod_keys, | ||
| 92 | R.string.install_prod_keys_description, | ||
| 93 | install = { mainActivity.getProdKey.launch(arrayOf("*/*")) } | ||
| 94 | ), | ||
| 95 | Installable( | ||
| 96 | R.string.install_amiibo_keys, | ||
| 97 | R.string.install_amiibo_keys_description, | ||
| 98 | install = { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) } | ||
| 99 | ) | ||
| 100 | ) | ||
| 101 | |||
| 102 | binding.listInstallables.apply { | ||
| 103 | layoutManager = GridLayoutManager( | ||
| 104 | requireContext(), | ||
| 105 | resources.getInteger(R.integer.grid_columns) | ||
| 106 | ) | ||
| 107 | adapter = InstallableAdapter(installables) | ||
| 108 | } | ||
| 109 | |||
| 110 | setInsets() | ||
| 111 | } | ||
| 112 | |||
| 113 | private fun setInsets() = | ||
| 114 | ViewCompat.setOnApplyWindowInsetsListener( | ||
| 115 | binding.root | ||
| 116 | ) { _: View, windowInsets: WindowInsetsCompat -> | ||
| 117 | val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) | ||
| 118 | val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) | ||
| 119 | |||
| 120 | val leftInsets = barInsets.left + cutoutInsets.left | ||
| 121 | val rightInsets = barInsets.right + cutoutInsets.right | ||
| 122 | |||
| 123 | val mlpAppBar = binding.toolbarInstallables.layoutParams as ViewGroup.MarginLayoutParams | ||
| 124 | mlpAppBar.leftMargin = leftInsets | ||
| 125 | mlpAppBar.rightMargin = rightInsets | ||
| 126 | binding.toolbarInstallables.layoutParams = mlpAppBar | ||
| 127 | |||
| 128 | val mlpScrollAbout = | ||
| 129 | binding.listInstallables.layoutParams as ViewGroup.MarginLayoutParams | ||
| 130 | mlpScrollAbout.leftMargin = leftInsets | ||
| 131 | mlpScrollAbout.rightMargin = rightInsets | ||
| 132 | binding.listInstallables.layoutParams = mlpScrollAbout | ||
| 133 | |||
| 134 | binding.listInstallables.updatePadding(bottom = barInsets.bottom) | ||
| 135 | |||
| 136 | windowInsets | ||
| 137 | } | ||
| 138 | } | ||
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Installable.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Installable.kt new file mode 100644 index 000000000..36a7c97b8 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Installable.kt | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | package org.yuzu.yuzu_emu.model | ||
| 5 | |||
| 6 | import androidx.annotation.StringRes | ||
| 7 | |||
| 8 | data class Installable( | ||
| 9 | @StringRes val titleId: Int, | ||
| 10 | @StringRes val descriptionId: Int, | ||
| 11 | val install: (() -> Unit)? = null, | ||
| 12 | val export: (() -> Unit)? = null | ||
| 13 | ) | ||
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 6fa847631..1164dfe94 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 | |||
| @@ -6,6 +6,7 @@ package org.yuzu.yuzu_emu.ui.main | |||
| 6 | import android.content.Intent | 6 | import android.content.Intent |
| 7 | import android.net.Uri | 7 | import android.net.Uri |
| 8 | import android.os.Bundle | 8 | import android.os.Bundle |
| 9 | import android.provider.DocumentsContract | ||
| 9 | import android.view.View | 10 | import android.view.View |
| 10 | import android.view.ViewGroup.MarginLayoutParams | 11 | import android.view.ViewGroup.MarginLayoutParams |
| 11 | import android.view.WindowManager | 12 | import android.view.WindowManager |
| @@ -19,6 +20,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen | |||
| 19 | import androidx.core.view.ViewCompat | 20 | import androidx.core.view.ViewCompat |
| 20 | import androidx.core.view.WindowCompat | 21 | import androidx.core.view.WindowCompat |
| 21 | import androidx.core.view.WindowInsetsCompat | 22 | import androidx.core.view.WindowInsetsCompat |
| 23 | import androidx.documentfile.provider.DocumentFile | ||
| 22 | import androidx.lifecycle.Lifecycle | 24 | import androidx.lifecycle.Lifecycle |
| 23 | import androidx.lifecycle.lifecycleScope | 25 | import androidx.lifecycle.lifecycleScope |
| 24 | import androidx.lifecycle.repeatOnLifecycle | 26 | import androidx.lifecycle.repeatOnLifecycle |
| @@ -29,6 +31,7 @@ import androidx.preference.PreferenceManager | |||
| 29 | import com.google.android.material.color.MaterialColors | 31 | import com.google.android.material.color.MaterialColors |
| 30 | import com.google.android.material.dialog.MaterialAlertDialogBuilder | 32 | import com.google.android.material.dialog.MaterialAlertDialogBuilder |
| 31 | import com.google.android.material.navigation.NavigationBarView | 33 | import com.google.android.material.navigation.NavigationBarView |
| 34 | import kotlinx.coroutines.CoroutineScope | ||
| 32 | import java.io.File | 35 | import java.io.File |
| 33 | import java.io.FilenameFilter | 36 | import java.io.FilenameFilter |
| 34 | import java.io.IOException | 37 | import java.io.IOException |
| @@ -41,9 +44,11 @@ import org.yuzu.yuzu_emu.R | |||
| 41 | import org.yuzu.yuzu_emu.activities.EmulationActivity | 44 | import org.yuzu.yuzu_emu.activities.EmulationActivity |
| 42 | import org.yuzu.yuzu_emu.databinding.ActivityMainBinding | 45 | import org.yuzu.yuzu_emu.databinding.ActivityMainBinding |
| 43 | import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding | 46 | import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding |
| 47 | import org.yuzu.yuzu_emu.features.DocumentProvider | ||
| 44 | import org.yuzu.yuzu_emu.features.settings.model.Settings | 48 | import org.yuzu.yuzu_emu.features.settings.model.Settings |
| 45 | import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment | 49 | import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment |
| 46 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment | 50 | import org.yuzu.yuzu_emu.fragments.MessageDialogFragment |
| 51 | import org.yuzu.yuzu_emu.getPublicFilesDir | ||
| 47 | import org.yuzu.yuzu_emu.model.GamesViewModel | 52 | import org.yuzu.yuzu_emu.model.GamesViewModel |
| 48 | import org.yuzu.yuzu_emu.model.HomeViewModel | 53 | import org.yuzu.yuzu_emu.model.HomeViewModel |
| 49 | import org.yuzu.yuzu_emu.model.TaskViewModel | 54 | import org.yuzu.yuzu_emu.model.TaskViewModel |
| @@ -52,6 +57,8 @@ import java.io.BufferedInputStream | |||
| 52 | import java.io.BufferedOutputStream | 57 | import java.io.BufferedOutputStream |
| 53 | import java.io.FileInputStream | 58 | import java.io.FileInputStream |
| 54 | import java.io.FileOutputStream | 59 | import java.io.FileOutputStream |
| 60 | import java.time.LocalDateTime | ||
| 61 | import java.time.format.DateTimeFormatter | ||
| 55 | import java.util.zip.ZipEntry | 62 | import java.util.zip.ZipEntry |
| 56 | import java.util.zip.ZipInputStream | 63 | import java.util.zip.ZipInputStream |
| 57 | import java.util.zip.ZipOutputStream | 64 | import java.util.zip.ZipOutputStream |
| @@ -65,6 +72,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 65 | 72 | ||
| 66 | override var themeId: Int = 0 | 73 | override var themeId: Int = 0 |
| 67 | 74 | ||
| 75 | private val savesFolder | ||
| 76 | get() = "${getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000" | ||
| 77 | |||
| 78 | // Get first subfolder in saves folder (should be the user folder) | ||
| 79 | val savesFolderRoot get() = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: "" | ||
| 80 | private var lastZipCreated: File? = null | ||
| 81 | |||
| 68 | override fun onCreate(savedInstanceState: Bundle?) { | 82 | override fun onCreate(savedInstanceState: Bundle?) { |
| 69 | val splashScreen = installSplashScreen() | 83 | val splashScreen = installSplashScreen() |
| 70 | splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } | 84 | splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } |
| @@ -727,4 +741,152 @@ class MainActivity : AppCompatActivity(), ThemeProvider { | |||
| 727 | return@newInstance getString(R.string.user_data_import_success) | 741 | return@newInstance getString(R.string.user_data_import_success) |
| 728 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) | 742 | }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) |
| 729 | } | 743 | } |
| 744 | |||
| 745 | /** | ||
| 746 | * Zips the save files located in the given folder path and creates a new zip file with the current date and time. | ||
| 747 | * @return true if the zip file is successfully created, false otherwise. | ||
| 748 | */ | ||
| 749 | private fun zipSave(): Boolean { | ||
| 750 | try { | ||
| 751 | val tempFolder = File(getPublicFilesDir().canonicalPath, "temp") | ||
| 752 | tempFolder.mkdirs() | ||
| 753 | val saveFolder = File(savesFolderRoot) | ||
| 754 | val outputZipFile = File( | ||
| 755 | tempFolder, | ||
| 756 | "yuzu saves - ${ | ||
| 757 | LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) | ||
| 758 | }.zip" | ||
| 759 | ) | ||
| 760 | outputZipFile.createNewFile() | ||
| 761 | ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos -> | ||
| 762 | saveFolder.walkTopDown().forEach { file -> | ||
| 763 | val zipFileName = | ||
| 764 | file.absolutePath.removePrefix(savesFolderRoot).removePrefix("/") | ||
| 765 | if (zipFileName == "") { | ||
| 766 | return@forEach | ||
| 767 | } | ||
| 768 | val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}") | ||
| 769 | zos.putNextEntry(entry) | ||
| 770 | if (file.isFile) { | ||
| 771 | file.inputStream().use { fis -> fis.copyTo(zos) } | ||
| 772 | } | ||
| 773 | } | ||
| 774 | } | ||
| 775 | lastZipCreated = outputZipFile | ||
| 776 | } catch (e: Exception) { | ||
| 777 | return false | ||
| 778 | } | ||
| 779 | return true | ||
| 780 | } | ||
| 781 | |||
| 782 | /** | ||
| 783 | * Exports the save file located in the given folder path by creating a zip file and sharing it via intent. | ||
| 784 | */ | ||
| 785 | fun exportSave() { | ||
| 786 | CoroutineScope(Dispatchers.IO).launch { | ||
| 787 | val wasZipCreated = zipSave() | ||
| 788 | val lastZipFile = lastZipCreated | ||
| 789 | if (!wasZipCreated || lastZipFile == null) { | ||
| 790 | withContext(Dispatchers.Main) { | ||
| 791 | Toast.makeText( | ||
| 792 | this@MainActivity, | ||
| 793 | getString(R.string.export_save_failed), | ||
| 794 | Toast.LENGTH_LONG | ||
| 795 | ).show() | ||
| 796 | } | ||
| 797 | return@launch | ||
| 798 | } | ||
| 799 | |||
| 800 | withContext(Dispatchers.Main) { | ||
| 801 | val file = DocumentFile.fromSingleUri( | ||
| 802 | this@MainActivity, | ||
| 803 | DocumentsContract.buildDocumentUri( | ||
| 804 | DocumentProvider.AUTHORITY, | ||
| 805 | "${DocumentProvider.ROOT_ID}/temp/${lastZipFile.name}" | ||
| 806 | ) | ||
| 807 | )!! | ||
| 808 | val intent = Intent(Intent.ACTION_SEND) | ||
| 809 | .setDataAndType(file.uri, "application/zip") | ||
| 810 | .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) | ||
| 811 | .putExtra(Intent.EXTRA_STREAM, file.uri) | ||
| 812 | startForResultExportSave.launch( | ||
| 813 | Intent.createChooser( | ||
| 814 | intent, | ||
| 815 | getString(R.string.share_save_file) | ||
| 816 | ) | ||
| 817 | ) | ||
| 818 | } | ||
| 819 | } | ||
| 820 | } | ||
| 821 | |||
| 822 | private val startForResultExportSave = | ||
| 823 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ -> | ||
| 824 | File(getPublicFilesDir().canonicalPath, "temp").deleteRecursively() | ||
| 825 | } | ||
| 826 | |||
| 827 | val importSaves = | ||
| 828 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> | ||
| 829 | if (result == null) { | ||
| 830 | return@registerForActivityResult | ||
| 831 | } | ||
| 832 | |||
| 833 | NativeLibrary.initializeEmptyUserDirectory() | ||
| 834 | |||
| 835 | val inputZip = applicationContext.contentResolver.openInputStream(result) | ||
| 836 | // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid. | ||
| 837 | var validZip = false | ||
| 838 | val savesFolder = File(savesFolderRoot) | ||
| 839 | val cacheSaveDir = File("${applicationContext.cacheDir.path}/saves/") | ||
| 840 | cacheSaveDir.mkdir() | ||
| 841 | |||
| 842 | if (inputZip == null) { | ||
| 843 | Toast.makeText( | ||
| 844 | applicationContext, | ||
| 845 | getString(R.string.fatal_error), | ||
| 846 | Toast.LENGTH_LONG | ||
| 847 | ).show() | ||
| 848 | return@registerForActivityResult | ||
| 849 | } | ||
| 850 | |||
| 851 | val filterTitleId = | ||
| 852 | FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) } | ||
| 853 | |||
| 854 | try { | ||
| 855 | CoroutineScope(Dispatchers.IO).launch { | ||
| 856 | FileUtil.unzip(inputZip, cacheSaveDir) | ||
| 857 | cacheSaveDir.list(filterTitleId)?.forEach { savePath -> | ||
| 858 | File(savesFolder, savePath).deleteRecursively() | ||
| 859 | File(cacheSaveDir, savePath).copyRecursively( | ||
| 860 | File(savesFolder, savePath), | ||
| 861 | true | ||
| 862 | ) | ||
| 863 | validZip = true | ||
| 864 | } | ||
| 865 | |||
| 866 | withContext(Dispatchers.Main) { | ||
| 867 | if (!validZip) { | ||
| 868 | MessageDialogFragment.newInstance( | ||
| 869 | this@MainActivity, | ||
| 870 | titleId = R.string.save_file_invalid_zip_structure, | ||
| 871 | descriptionId = R.string.save_file_invalid_zip_structure_description | ||
| 872 | ).show(supportFragmentManager, MessageDialogFragment.TAG) | ||
| 873 | return@withContext | ||
| 874 | } | ||
| 875 | Toast.makeText( | ||
| 876 | applicationContext, | ||
| 877 | getString(R.string.save_file_imported_success), | ||
| 878 | Toast.LENGTH_LONG | ||
| 879 | ).show() | ||
| 880 | } | ||
| 881 | |||
| 882 | cacheSaveDir.deleteRecursively() | ||
| 883 | } | ||
| 884 | } catch (e: Exception) { | ||
| 885 | Toast.makeText( | ||
| 886 | applicationContext, | ||
| 887 | getString(R.string.fatal_error), | ||
| 888 | Toast.LENGTH_LONG | ||
| 889 | ).show() | ||
| 890 | } | ||
| 891 | } | ||
| 730 | } | 892 | } |
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index f31fe054b..26666f59a 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp | |||
| @@ -13,6 +13,8 @@ | |||
| 13 | 13 | ||
| 14 | #include <android/api-level.h> | 14 | #include <android/api-level.h> |
| 15 | #include <android/native_window_jni.h> | 15 | #include <android/native_window_jni.h> |
| 16 | #include <common/fs/fs.h> | ||
| 17 | #include <core/file_sys/savedata_factory.h> | ||
| 16 | #include <core/loader/nro.h> | 18 | #include <core/loader/nro.h> |
| 17 | #include <jni.h> | 19 | #include <jni.h> |
| 18 | 20 | ||
| @@ -879,4 +881,24 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_submitInlineKeyboardInput(JNIEnv* env | |||
| 879 | EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardInput(j_key_code); | 881 | EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardInput(j_key_code); |
| 880 | } | 882 | } |
| 881 | 883 | ||
| 884 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmptyUserDirectory(JNIEnv* env, | ||
| 885 | jobject instance) { | ||
| 886 | const auto nand_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir); | ||
| 887 | auto vfs_nand_dir = EmulationSession::GetInstance().System().GetFilesystem()->OpenDirectory( | ||
| 888 | Common::FS::PathToUTF8String(nand_dir), FileSys::Mode::Read); | ||
| 889 | |||
| 890 | Service::Account::ProfileManager manager; | ||
| 891 | const auto user_id = manager.GetUser(static_cast<std::size_t>(0)); | ||
| 892 | ASSERT(user_id); | ||
| 893 | |||
| 894 | const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath( | ||
| 895 | EmulationSession::GetInstance().System(), vfs_nand_dir, FileSys::SaveDataSpaceId::NandUser, | ||
| 896 | FileSys::SaveDataType::SaveData, 1, user_id->AsU128(), 0); | ||
| 897 | |||
| 898 | const auto full_path = Common::FS::ConcatPathSafe(nand_dir, user_save_data_path); | ||
| 899 | if (!Common::FS::CreateParentDirs(full_path)) { | ||
| 900 | LOG_WARNING(Frontend, "Failed to create full path of the default user's save directory"); | ||
| 901 | } | ||
| 902 | } | ||
| 903 | |||
| 882 | } // extern "C" | 904 | } // extern "C" |
diff --git a/src/android/app/src/main/res/layout/card_installable.xml b/src/android/app/src/main/res/layout/card_installable.xml new file mode 100644 index 000000000..f5b0e3741 --- /dev/null +++ b/src/android/app/src/main/res/layout/card_installable.xml | |||
| @@ -0,0 +1,71 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 4 | xmlns:tools="http://schemas.android.com/tools" | ||
| 5 | style="?attr/materialCardViewOutlinedStyle" | ||
| 6 | android:layout_width="match_parent" | ||
| 7 | android:layout_height="wrap_content" | ||
| 8 | android:layout_marginHorizontal="16dp" | ||
| 9 | android:layout_marginVertical="12dp"> | ||
| 10 | |||
| 11 | <LinearLayout | ||
| 12 | android:layout_width="match_parent" | ||
| 13 | android:layout_height="wrap_content" | ||
| 14 | android:layout_margin="16dp" | ||
| 15 | android:orientation="horizontal" | ||
| 16 | android:layout_gravity="center"> | ||
| 17 | |||
| 18 | <LinearLayout | ||
| 19 | android:layout_width="0dp" | ||
| 20 | android:layout_height="wrap_content" | ||
| 21 | android:layout_marginEnd="16dp" | ||
| 22 | android:layout_weight="1" | ||
| 23 | android:orientation="vertical"> | ||
| 24 | |||
| 25 | <com.google.android.material.textview.MaterialTextView | ||
| 26 | android:id="@+id/title" | ||
| 27 | style="@style/TextAppearance.Material3.TitleMedium" | ||
| 28 | android:layout_width="match_parent" | ||
| 29 | android:layout_height="wrap_content" | ||
| 30 | android:text="@string/user_data" | ||
| 31 | android:textAlignment="viewStart" /> | ||
| 32 | |||
| 33 | <com.google.android.material.textview.MaterialTextView | ||
| 34 | android:id="@+id/description" | ||
| 35 | style="@style/TextAppearance.Material3.BodyMedium" | ||
| 36 | android:layout_width="match_parent" | ||
| 37 | android:layout_height="wrap_content" | ||
| 38 | android:layout_marginTop="6dp" | ||
| 39 | android:text="@string/user_data_description" | ||
| 40 | android:textAlignment="viewStart" /> | ||
| 41 | |||
| 42 | </LinearLayout> | ||
| 43 | |||
| 44 | <Button | ||
| 45 | android:id="@+id/button_export" | ||
| 46 | style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" | ||
| 47 | android:layout_width="wrap_content" | ||
| 48 | android:layout_height="wrap_content" | ||
| 49 | android:layout_gravity="center_vertical" | ||
| 50 | android:contentDescription="@string/export" | ||
| 51 | android:tooltipText="@string/export" | ||
| 52 | android:visibility="gone" | ||
| 53 | app:icon="@drawable/ic_export" | ||
| 54 | tools:visibility="visible" /> | ||
| 55 | |||
| 56 | <Button | ||
| 57 | android:id="@+id/button_install" | ||
| 58 | style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" | ||
| 59 | android:layout_width="wrap_content" | ||
| 60 | android:layout_height="wrap_content" | ||
| 61 | android:layout_gravity="center_vertical" | ||
| 62 | android:layout_marginStart="12dp" | ||
| 63 | android:contentDescription="@string/string_import" | ||
| 64 | android:tooltipText="@string/string_import" | ||
| 65 | android:visibility="gone" | ||
| 66 | app:icon="@drawable/ic_import" | ||
| 67 | tools:visibility="visible" /> | ||
| 68 | |||
| 69 | </LinearLayout> | ||
| 70 | |||
| 71 | </com.google.android.material.card.MaterialCardView> | ||
diff --git a/src/android/app/src/main/res/layout/fragment_about.xml b/src/android/app/src/main/res/layout/fragment_about.xml index 36b350338..3e1d98451 100644 --- a/src/android/app/src/main/res/layout/fragment_about.xml +++ b/src/android/app/src/main/res/layout/fragment_about.xml | |||
| @@ -184,67 +184,6 @@ | |||
| 184 | <LinearLayout | 184 | <LinearLayout |
| 185 | android:layout_width="match_parent" | 185 | android:layout_width="match_parent" |
| 186 | android:layout_height="wrap_content" | 186 | android:layout_height="wrap_content" |
| 187 | android:orientation="horizontal"> | ||
| 188 | |||
| 189 | <LinearLayout | ||
| 190 | android:layout_width="match_parent" | ||
| 191 | android:layout_height="wrap_content" | ||
| 192 | android:paddingVertical="16dp" | ||
| 193 | android:paddingHorizontal="16dp" | ||
| 194 | android:orientation="vertical" | ||
| 195 | android:layout_weight="1"> | ||
| 196 | |||
| 197 | <com.google.android.material.textview.MaterialTextView | ||
| 198 | style="@style/TextAppearance.Material3.TitleMedium" | ||
| 199 | android:layout_width="match_parent" | ||
| 200 | android:layout_height="wrap_content" | ||
| 201 | android:layout_marginHorizontal="24dp" | ||
| 202 | android:textAlignment="viewStart" | ||
| 203 | android:text="@string/user_data" /> | ||
| 204 | |||
| 205 | <com.google.android.material.textview.MaterialTextView | ||
| 206 | style="@style/TextAppearance.Material3.BodyMedium" | ||
| 207 | android:layout_width="match_parent" | ||
| 208 | android:layout_height="wrap_content" | ||
| 209 | android:layout_marginHorizontal="24dp" | ||
| 210 | android:layout_marginTop="6dp" | ||
| 211 | android:textAlignment="viewStart" | ||
| 212 | android:text="@string/user_data_description" /> | ||
| 213 | |||
| 214 | </LinearLayout> | ||
| 215 | |||
| 216 | <Button | ||
| 217 | android:id="@+id/button_import" | ||
| 218 | style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" | ||
| 219 | android:layout_width="wrap_content" | ||
| 220 | android:layout_height="wrap_content" | ||
| 221 | android:layout_gravity="center_vertical" | ||
| 222 | android:contentDescription="@string/string_import" | ||
| 223 | android:tooltipText="@string/string_import" | ||
| 224 | app:icon="@drawable/ic_import" /> | ||
| 225 | |||
| 226 | <Button | ||
| 227 | android:id="@+id/button_export" | ||
| 228 | style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" | ||
| 229 | android:layout_width="wrap_content" | ||
| 230 | android:layout_height="wrap_content" | ||
| 231 | android:layout_marginStart="12dp" | ||
| 232 | android:layout_marginEnd="24dp" | ||
| 233 | android:layout_gravity="center_vertical" | ||
| 234 | android:contentDescription="@string/export" | ||
| 235 | android:tooltipText="@string/export" | ||
| 236 | app:icon="@drawable/ic_export" /> | ||
| 237 | |||
| 238 | </LinearLayout> | ||
| 239 | |||
| 240 | <com.google.android.material.divider.MaterialDivider | ||
| 241 | android:layout_width="match_parent" | ||
| 242 | android:layout_height="wrap_content" | ||
| 243 | android:layout_marginHorizontal="20dp" /> | ||
| 244 | |||
| 245 | <LinearLayout | ||
| 246 | android:layout_width="match_parent" | ||
| 247 | android:layout_height="wrap_content" | ||
| 248 | android:orientation="horizontal" | 187 | android:orientation="horizontal" |
| 249 | android:gravity="center_horizontal" | 188 | android:gravity="center_horizontal" |
| 250 | android:layout_marginTop="12dp" | 189 | android:layout_marginTop="12dp" |
diff --git a/src/android/app/src/main/res/layout/fragment_installables.xml b/src/android/app/src/main/res/layout/fragment_installables.xml new file mode 100644 index 000000000..3a4df81a6 --- /dev/null +++ b/src/android/app/src/main/res/layout/fragment_installables.xml | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 4 | android:id="@+id/coordinator_licenses" | ||
| 5 | android:layout_width="match_parent" | ||
| 6 | android:layout_height="match_parent" | ||
| 7 | android:background="?attr/colorSurface"> | ||
| 8 | |||
| 9 | <com.google.android.material.appbar.AppBarLayout | ||
| 10 | android:id="@+id/appbar_installables" | ||
| 11 | android:layout_width="match_parent" | ||
| 12 | android:layout_height="wrap_content" | ||
| 13 | android:fitsSystemWindows="true"> | ||
| 14 | |||
| 15 | <com.google.android.material.appbar.MaterialToolbar | ||
| 16 | android:id="@+id/toolbar_installables" | ||
| 17 | android:layout_width="match_parent" | ||
| 18 | android:layout_height="?attr/actionBarSize" | ||
| 19 | app:title="@string/manage_yuzu_data" | ||
| 20 | app:navigationIcon="@drawable/ic_back" /> | ||
| 21 | |||
| 22 | </com.google.android.material.appbar.AppBarLayout> | ||
| 23 | |||
| 24 | <androidx.recyclerview.widget.RecyclerView | ||
| 25 | android:id="@+id/list_installables" | ||
| 26 | android:layout_width="match_parent" | ||
| 27 | android:layout_height="match_parent" | ||
| 28 | android:clipToPadding="false" | ||
| 29 | app:layout_behavior="@string/appbar_scrolling_view_behavior" /> | ||
| 30 | |||
| 31 | </androidx.coordinatorlayout.widget.CoordinatorLayout> | ||
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 2e0ce7a3d..2356b802b 100644 --- a/src/android/app/src/main/res/navigation/home_navigation.xml +++ b/src/android/app/src/main/res/navigation/home_navigation.xml | |||
| @@ -19,6 +19,9 @@ | |||
| 19 | <action | 19 | <action |
| 20 | android:id="@+id/action_homeSettingsFragment_to_earlyAccessFragment" | 20 | android:id="@+id/action_homeSettingsFragment_to_earlyAccessFragment" |
| 21 | app:destination="@id/earlyAccessFragment" /> | 21 | app:destination="@id/earlyAccessFragment" /> |
| 22 | <action | ||
| 23 | android:id="@+id/action_homeSettingsFragment_to_installableFragment" | ||
| 24 | app:destination="@id/installableFragment" /> | ||
| 22 | </fragment> | 25 | </fragment> |
| 23 | 26 | ||
| 24 | <fragment | 27 | <fragment |
| @@ -88,5 +91,9 @@ | |||
| 88 | <action | 91 | <action |
| 89 | android:id="@+id/action_global_settingsActivity" | 92 | android:id="@+id/action_global_settingsActivity" |
| 90 | app:destination="@id/settingsActivity" /> | 93 | app:destination="@id/settingsActivity" /> |
| 94 | <fragment | ||
| 95 | android:id="@+id/installableFragment" | ||
| 96 | android:name="org.yuzu.yuzu_emu.fragments.InstallableFragment" | ||
| 97 | android:label="InstallableFragment" /> | ||
| 91 | 98 | ||
| 92 | </navigation> | 99 | </navigation> |
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 daaa7ffde..dd0f36392 100644 --- a/src/android/app/src/main/res/values-de/strings.xml +++ b/src/android/app/src/main/res/values-de/strings.xml | |||
| @@ -79,7 +79,6 @@ | |||
| 79 | <string name="manage_save_data">Speicherdaten verwalten</string> | 79 | <string name="manage_save_data">Speicherdaten verwalten</string> |
| 80 | <string name="manage_save_data_description">Speicherdaten gefunden. Bitte wähle unten eine Option aus.</string> | 80 | <string name="manage_save_data_description">Speicherdaten gefunden. Bitte wähle unten eine Option aus.</string> |
| 81 | <string name="import_export_saves_description">Speicherdaten importieren oder exportieren</string> | 81 | <string name="import_export_saves_description">Speicherdaten importieren oder exportieren</string> |
| 82 | <string name="import_export_saves_no_profile">Keine Speicherdaten gefunden. Bitte starte ein Spiel und versuche es erneut.</string> | ||
| 83 | <string name="save_file_imported_success">Erfolgreich importiert</string> | 82 | <string name="save_file_imported_success">Erfolgreich importiert</string> |
| 84 | <string name="save_file_invalid_zip_structure">Ungültige Speicherverzeichnisstruktur</string> | 83 | <string name="save_file_invalid_zip_structure">Ungültige Speicherverzeichnisstruktur</string> |
| 85 | <string name="save_file_invalid_zip_structure_description">Der erste Unterordnername muss die Titel-ID des Spiels sein.</string> | 84 | <string name="save_file_invalid_zip_structure_description">Der erste Unterordnername muss die Titel-ID des Spiels sein.</string> |
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 e9129cb00..d398f862f 100644 --- a/src/android/app/src/main/res/values-es/strings.xml +++ b/src/android/app/src/main/res/values-es/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">Administrar datos de guardado</string> | 81 | <string name="manage_save_data">Administrar datos de guardado</string> |
| 82 | <string name="manage_save_data_description">Guardar los datos encontrados. Por favor, seleccione una opción de abajo.</string> | 82 | <string name="manage_save_data_description">Guardar los datos encontrados. Por favor, seleccione una opción de abajo.</string> |
| 83 | <string name="import_export_saves_description">Importar o exportar archivos de guardado</string> | 83 | <string name="import_export_saves_description">Importar o exportar archivos de guardado</string> |
| 84 | <string name="import_export_saves_no_profile">No se han encontrado datos de guardado. Por favor, ejecute un juego y vuelva a intentarlo.</string> | ||
| 85 | <string name="save_file_imported_success">Importado correctamente</string> | 84 | <string name="save_file_imported_success">Importado correctamente</string> |
| 86 | <string name="save_file_invalid_zip_structure">Estructura del directorio de guardado no válido</string> | 85 | <string name="save_file_invalid_zip_structure">Estructura del directorio de guardado no válido</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">El nombre de la primera subcarpeta debe ser el Title ID del juego.</string> | 86 | <string name="save_file_invalid_zip_structure_description">El nombre de la primera subcarpeta debe ser el Title ID del juego.</string> |
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 2d99d618e..a7abd9077 100644 --- a/src/android/app/src/main/res/values-fr/strings.xml +++ b/src/android/app/src/main/res/values-fr/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">Gérer les données de sauvegarde</string> | 81 | <string name="manage_save_data">Gérer les données de sauvegarde</string> |
| 82 | <string name="manage_save_data_description">Données de sauvegarde trouvées. Veuillez sélectionner une option ci-dessous.</string> | 82 | <string name="manage_save_data_description">Données de sauvegarde trouvées. Veuillez sélectionner une option ci-dessous.</string> |
| 83 | <string name="import_export_saves_description">Importer ou exporter des fichiers de sauvegarde</string> | 83 | <string name="import_export_saves_description">Importer ou exporter des fichiers de sauvegarde</string> |
| 84 | <string name="import_export_saves_no_profile">Aucune données de sauvegarde trouvées. Veuillez lancer un jeu et réessayer.</string> | ||
| 85 | <string name="save_file_imported_success">Importé avec succès</string> | 84 | <string name="save_file_imported_success">Importé avec succès</string> |
| 86 | <string name="save_file_invalid_zip_structure">Structure de répertoire de sauvegarde non valide</string> | 85 | <string name="save_file_invalid_zip_structure">Structure de répertoire de sauvegarde non valide</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">Le nom du premier sous-dossier doit être l\'identifiant du titre du jeu.</string> | 86 | <string name="save_file_invalid_zip_structure_description">Le nom du premier sous-dossier doit être l\'identifiant du titre du jeu.</string> |
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 d9c3de385..b18161801 100644 --- a/src/android/app/src/main/res/values-it/strings.xml +++ b/src/android/app/src/main/res/values-it/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">Gestisci i salvataggi</string> | 81 | <string name="manage_save_data">Gestisci i salvataggi</string> |
| 82 | <string name="manage_save_data_description">Salvataggio non trovato. Seleziona un\'opzione di seguito.</string> | 82 | <string name="manage_save_data_description">Salvataggio non trovato. Seleziona un\'opzione di seguito.</string> |
| 83 | <string name="import_export_saves_description">Importa o esporta i salvataggi</string> | 83 | <string name="import_export_saves_description">Importa o esporta i salvataggi</string> |
| 84 | <string name="import_export_saves_no_profile">Nessun salvataggio trovato. Avvia un gioco e riprova.</string> | ||
| 85 | <string name="save_file_imported_success">Importato con successo</string> | 84 | <string name="save_file_imported_success">Importato con successo</string> |
| 86 | <string name="save_file_invalid_zip_structure">La struttura della cartella dei salvataggi è invalida</string> | 85 | <string name="save_file_invalid_zip_structure">La struttura della cartella dei salvataggi è invalida</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">La prima sotto cartella <b>deve</b> chiamarsi come l\'ID del titolo del gioco.</string> | 86 | <string name="save_file_invalid_zip_structure_description">La prima sotto cartella <b>deve</b> chiamarsi come l\'ID del titolo del gioco.</string> |
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 7a226cd5c..88fa5a0bb 100644 --- a/src/android/app/src/main/res/values-ja/strings.xml +++ b/src/android/app/src/main/res/values-ja/strings.xml | |||
| @@ -80,7 +80,6 @@ | |||
| 80 | <string name="manage_save_data">セーブデータを管理</string> | 80 | <string name="manage_save_data">セーブデータを管理</string> |
| 81 | <string name="manage_save_data_description">セーブデータが見つかりました。以下のオプションから選択してください。</string> | 81 | <string name="manage_save_data_description">セーブデータが見つかりました。以下のオプションから選択してください。</string> |
| 82 | <string name="import_export_saves_description">セーブファイルをインポート/エクスポート</string> | 82 | <string name="import_export_saves_description">セーブファイルをインポート/エクスポート</string> |
| 83 | <string name="import_export_saves_no_profile">セーブデータがありません。ゲームを起動してから再度お試しください。</string> | ||
| 84 | <string name="save_file_imported_success">インポートが完了しました</string> | 83 | <string name="save_file_imported_success">インポートが完了しました</string> |
| 85 | <string name="save_file_invalid_zip_structure">セーブデータのディレクトリ構造が無効です</string> | 84 | <string name="save_file_invalid_zip_structure">セーブデータのディレクトリ構造が無効です</string> |
| 86 | <string name="save_file_invalid_zip_structure_description">最初のサブフォルダ名は、ゲームのタイトルIDである必要があります。</string> | 85 | <string name="save_file_invalid_zip_structure_description">最初のサブフォルダ名は、ゲームのタイトルIDである必要があります。</string> |
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 427b6e5a0..4b658255c 100644 --- a/src/android/app/src/main/res/values-ko/strings.xml +++ b/src/android/app/src/main/res/values-ko/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">저장 데이터 관리</string> | 81 | <string name="manage_save_data">저장 데이터 관리</string> |
| 82 | <string name="manage_save_data_description">데이터를 저장했습니다. 아래에서 옵션을 선택하세요.</string> | 82 | <string name="manage_save_data_description">데이터를 저장했습니다. 아래에서 옵션을 선택하세요.</string> |
| 83 | <string name="import_export_saves_description">저장 파일 가져오기 또는 내보내기</string> | 83 | <string name="import_export_saves_description">저장 파일 가져오기 또는 내보내기</string> |
| 84 | <string name="import_export_saves_no_profile">저장 데이터를 찾을 수 없습니다. 게임을 실행한 후 다시 시도하세요.</string> | ||
| 85 | <string name="save_file_imported_success">가져오기 성공</string> | 84 | <string name="save_file_imported_success">가져오기 성공</string> |
| 86 | <string name="save_file_invalid_zip_structure">저장 디렉터리 구조가 잘못됨</string> | 85 | <string name="save_file_invalid_zip_structure">저장 디렉터리 구조가 잘못됨</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">첫 번째 하위 폴더 이름은 게임의 타이틀 ID여야 합니다.</string> | 86 | <string name="save_file_invalid_zip_structure_description">첫 번째 하위 폴더 이름은 게임의 타이틀 ID여야 합니다.</string> |
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 ce8d7a9e4..dd602a389 100644 --- a/src/android/app/src/main/res/values-nb/strings.xml +++ b/src/android/app/src/main/res/values-nb/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">Administrere lagringsdata</string> | 81 | <string name="manage_save_data">Administrere lagringsdata</string> |
| 82 | <string name="manage_save_data_description">Lagringsdata funnet. Velg et alternativ nedenfor.</string> | 82 | <string name="manage_save_data_description">Lagringsdata funnet. Velg et alternativ nedenfor.</string> |
| 83 | <string name="import_export_saves_description">Importer eller eksporter lagringsfiler</string> | 83 | <string name="import_export_saves_description">Importer eller eksporter lagringsfiler</string> |
| 84 | <string name="import_export_saves_no_profile">Ingen lagringsdata funnet. Start et nytt spill og prøv på nytt.</string> | ||
| 85 | <string name="save_file_imported_success">Vellykket import</string> | 84 | <string name="save_file_imported_success">Vellykket import</string> |
| 86 | <string name="save_file_invalid_zip_structure">Ugyldig struktur for lagringskatalog</string> | 85 | <string name="save_file_invalid_zip_structure">Ugyldig struktur for lagringskatalog</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">Det første undermappenavnet må være spillets tittel-ID.</string> | 86 | <string name="save_file_invalid_zip_structure_description">Det første undermappenavnet må være spillets tittel-ID.</string> |
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 c2c24b48f..2fdd1f952 100644 --- a/src/android/app/src/main/res/values-pl/strings.xml +++ b/src/android/app/src/main/res/values-pl/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">Zarządzaj plikami zapisów gier</string> | 81 | <string name="manage_save_data">Zarządzaj plikami zapisów gier</string> |
| 82 | <string name="manage_save_data_description">Znaleziono pliki zapisów gier. Wybierz opcję poniżej.</string> | 82 | <string name="manage_save_data_description">Znaleziono pliki zapisów gier. Wybierz opcję poniżej.</string> |
| 83 | <string name="import_export_saves_description">Importuj lub wyeksportuj pliki zapisów</string> | 83 | <string name="import_export_saves_description">Importuj lub wyeksportuj pliki zapisów</string> |
| 84 | <string name="import_export_saves_no_profile">Nie znaleziono plików zapisów. Uruchom grę i spróbuj ponownie.</string> | ||
| 85 | <string name="save_file_imported_success">Zaimportowano pomyślnie</string> | 84 | <string name="save_file_imported_success">Zaimportowano pomyślnie</string> |
| 86 | <string name="save_file_invalid_zip_structure">Niepoprawna struktura folderów</string> | 85 | <string name="save_file_invalid_zip_structure">Niepoprawna struktura folderów</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">Pierwszy podkatalog musi zawierać w nazwie numer ID tytułu gry.</string> | 86 | <string name="save_file_invalid_zip_structure_description">Pierwszy podkatalog musi zawierać w nazwie numer ID tytułu gry.</string> |
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 04f276108..2f26367fe 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 | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">Gerir dados guardados</string> | 81 | <string name="manage_save_data">Gerir dados guardados</string> |
| 82 | <string name="manage_save_data_description">Dados não encontrados. Por favor seleciona uma opção abaixo.</string> | 82 | <string name="manage_save_data_description">Dados não encontrados. Por favor seleciona uma opção abaixo.</string> |
| 83 | <string name="import_export_saves_description">Importa ou exporta dados guardados</string> | 83 | <string name="import_export_saves_description">Importa ou exporta dados guardados</string> |
| 84 | <string name="import_export_saves_no_profile">Dados não encontrados. Por favor lança o jogo e tenta novamente.</string> | ||
| 85 | <string name="save_file_imported_success">Importado com sucesso</string> | 84 | <string name="save_file_imported_success">Importado com sucesso</string> |
| 86 | <string name="save_file_invalid_zip_structure">Estrutura de diretório de dados invalida</string> | 85 | <string name="save_file_invalid_zip_structure">Estrutura de diretório de dados invalida</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">O nome da primeira sub pasta tem de ser a ID do jogo.</string> | 86 | <string name="save_file_invalid_zip_structure_description">O nome da primeira sub pasta tem de ser a ID do jogo.</string> |
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 66a3a1a2e..4e1eb4cd7 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 | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">Gerir dados guardados</string> | 81 | <string name="manage_save_data">Gerir dados guardados</string> |
| 82 | <string name="manage_save_data_description">Dados não encontrados. Por favor seleciona uma opção abaixo.</string> | 82 | <string name="manage_save_data_description">Dados não encontrados. Por favor seleciona uma opção abaixo.</string> |
| 83 | <string name="import_export_saves_description">Importa ou exporta dados guardados</string> | 83 | <string name="import_export_saves_description">Importa ou exporta dados guardados</string> |
| 84 | <string name="import_export_saves_no_profile">Dados não encontrados. Por favor lança o jogo e tenta novamente.</string> | ||
| 85 | <string name="save_file_imported_success">Importado com sucesso</string> | 84 | <string name="save_file_imported_success">Importado com sucesso</string> |
| 86 | <string name="save_file_invalid_zip_structure">Estrutura de diretório de dados invalida</string> | 85 | <string name="save_file_invalid_zip_structure">Estrutura de diretório de dados invalida</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">O nome da primeira sub pasta tem de ser a ID do jogo.</string> | 86 | <string name="save_file_invalid_zip_structure_description">O nome da primeira sub pasta tem de ser a ID do jogo.</string> |
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 f770e954f..f5695dc93 100644 --- a/src/android/app/src/main/res/values-ru/strings.xml +++ b/src/android/app/src/main/res/values-ru/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">Управление данными сохранений</string> | 81 | <string name="manage_save_data">Управление данными сохранений</string> |
| 82 | <string name="manage_save_data_description">Найдено данные сохранений. Пожалуйста, выберите вариант ниже.</string> | 82 | <string name="manage_save_data_description">Найдено данные сохранений. Пожалуйста, выберите вариант ниже.</string> |
| 83 | <string name="import_export_saves_description">Импорт или экспорт файлов сохранения</string> | 83 | <string name="import_export_saves_description">Импорт или экспорт файлов сохранения</string> |
| 84 | <string name="import_export_saves_no_profile">Данные сохранений не найдены. Пожалуйста, запустите игру и повторите попытку.</string> | ||
| 85 | <string name="save_file_imported_success">Успешно импортировано</string> | 84 | <string name="save_file_imported_success">Успешно импортировано</string> |
| 86 | <string name="save_file_invalid_zip_structure">Недопустимая структура папки сохранения</string> | 85 | <string name="save_file_invalid_zip_structure">Недопустимая структура папки сохранения</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">Название первой вложенной папки должно быть идентификатором игры.</string> | 86 | <string name="save_file_invalid_zip_structure_description">Название первой вложенной папки должно быть идентификатором игры.</string> |
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 ea3ab1b15..061bc6f04 100644 --- a/src/android/app/src/main/res/values-uk/strings.xml +++ b/src/android/app/src/main/res/values-uk/strings.xml | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">Керування даними збережень</string> | 81 | <string name="manage_save_data">Керування даними збережень</string> |
| 82 | <string name="manage_save_data_description">Знайдено дані збережень. Будь ласка, виберіть варіант нижче.</string> | 82 | <string name="manage_save_data_description">Знайдено дані збережень. Будь ласка, виберіть варіант нижче.</string> |
| 83 | <string name="import_export_saves_description">Імпорт або експорт файлів збереження</string> | 83 | <string name="import_export_saves_description">Імпорт або експорт файлів збереження</string> |
| 84 | <string name="import_export_saves_no_profile">Дані збережень не знайдено. Будь ласка, запустіть гру та повторіть спробу.</string> | ||
| 85 | <string name="save_file_imported_success">Успішно імпортовано</string> | 84 | <string name="save_file_imported_success">Успішно імпортовано</string> |
| 86 | <string name="save_file_invalid_zip_structure">Неприпустима структура папки збереження</string> | 85 | <string name="save_file_invalid_zip_structure">Неприпустима структура папки збереження</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">Назва першої вкладеної папки має бути ідентифікатором гри.</string> | 86 | <string name="save_file_invalid_zip_structure_description">Назва першої вкладеної папки має бути ідентифікатором гри.</string> |
diff --git a/src/android/app/src/main/res/values-w600dp/integers.xml b/src/android/app/src/main/res/values-w600dp/integers.xml new file mode 100644 index 000000000..9975db801 --- /dev/null +++ b/src/android/app/src/main/res/values-w600dp/integers.xml | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | ||
| 2 | <resources> | ||
| 3 | |||
| 4 | <integer name="grid_columns">2</integer> | ||
| 5 | |||
| 6 | </resources> | ||
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 b45a5a528..fe6dd5eaa 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 | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">管理存档数据</string> | 81 | <string name="manage_save_data">管理存档数据</string> |
| 82 | <string name="manage_save_data_description">已找到存档数据,请选择下方的选项。</string> | 82 | <string name="manage_save_data_description">已找到存档数据,请选择下方的选项。</string> |
| 83 | <string name="import_export_saves_description">导入或导出存档</string> | 83 | <string name="import_export_saves_description">导入或导出存档</string> |
| 84 | <string name="import_export_saves_no_profile">找不到存档数据,请启动游戏并重试。</string> | ||
| 85 | <string name="save_file_imported_success">已成功导入存档</string> | 84 | <string name="save_file_imported_success">已成功导入存档</string> |
| 86 | <string name="save_file_invalid_zip_structure">无效的存档目录</string> | 85 | <string name="save_file_invalid_zip_structure">无效的存档目录</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">第一个子文件夹名称必须为当前游戏的 ID。</string> | 86 | <string name="save_file_invalid_zip_structure_description">第一个子文件夹名称必须为当前游戏的 ID。</string> |
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 3aab889e4..9b3e54224 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 | |||
| @@ -81,7 +81,6 @@ | |||
| 81 | <string name="manage_save_data">管理儲存資料</string> | 81 | <string name="manage_save_data">管理儲存資料</string> |
| 82 | <string name="manage_save_data_description">已找到儲存資料,請選取下方的選項。</string> | 82 | <string name="manage_save_data_description">已找到儲存資料,請選取下方的選項。</string> |
| 83 | <string name="import_export_saves_description">匯入或匯出儲存檔案</string> | 83 | <string name="import_export_saves_description">匯入或匯出儲存檔案</string> |
| 84 | <string name="import_export_saves_no_profile">找不到儲存資料,請啟動遊戲並重試。</string> | ||
| 85 | <string name="save_file_imported_success">已成功匯入</string> | 84 | <string name="save_file_imported_success">已成功匯入</string> |
| 86 | <string name="save_file_invalid_zip_structure">無效的儲存目錄結構</string> | 85 | <string name="save_file_invalid_zip_structure">無效的儲存目錄結構</string> |
| 87 | <string name="save_file_invalid_zip_structure_description">首個子資料夾名稱必須為遊戲標題 ID。</string> | 86 | <string name="save_file_invalid_zip_structure_description">首個子資料夾名稱必須為遊戲標題 ID。</string> |
diff --git a/src/android/app/src/main/res/values/integers.xml b/src/android/app/src/main/res/values/integers.xml index 5e39bc7d9..dc527965c 100644 --- a/src/android/app/src/main/res/values/integers.xml +++ b/src/android/app/src/main/res/values/integers.xml | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | <?xml version="1.0" encoding="utf-8"?> | 1 | <?xml version="1.0" encoding="utf-8"?> |
| 2 | <resources> | 2 | <resources> |
| 3 | <integer name="game_title_lines">2</integer> | 3 | <integer name="grid_columns">1</integer> |
| 4 | 4 | ||
| 5 | <!-- Default SWITCH landscape layout --> | 5 | <!-- Default SWITCH landscape layout --> |
| 6 | <integer name="SWITCH_BUTTON_A_X">760</integer> | 6 | <integer name="SWITCH_BUTTON_A_X">760</integer> |
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 0730143bd..067141866 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml | |||
| @@ -90,7 +90,6 @@ | |||
| 90 | <string name="manage_save_data">Manage save data</string> | 90 | <string name="manage_save_data">Manage save data</string> |
| 91 | <string name="manage_save_data_description">Save data found. Please select an option below.</string> | 91 | <string name="manage_save_data_description">Save data found. Please select an option below.</string> |
| 92 | <string name="import_export_saves_description">Import or export save files</string> | 92 | <string name="import_export_saves_description">Import or export save files</string> |
| 93 | <string name="import_export_saves_no_profile">No save data found. Please launch a game and retry.</string> | ||
| 94 | <string name="save_file_imported_success">Imported successfully</string> | 93 | <string name="save_file_imported_success">Imported successfully</string> |
| 95 | <string name="save_file_invalid_zip_structure">Invalid save directory structure</string> | 94 | <string name="save_file_invalid_zip_structure">Invalid save directory structure</string> |
| 96 | <string name="save_file_invalid_zip_structure_description">The first subfolder name must be the title ID of the game.</string> | 95 | <string name="save_file_invalid_zip_structure_description">The first subfolder name must be the title ID of the game.</string> |
| @@ -118,6 +117,10 @@ | |||
| 118 | <string name="install_game_content_help_link">https://yuzu-emu.org/help/quickstart/#dumping-installed-updates</string> | 117 | <string name="install_game_content_help_link">https://yuzu-emu.org/help/quickstart/#dumping-installed-updates</string> |
| 119 | <string name="custom_driver_not_supported">Custom drivers not supported</string> | 118 | <string name="custom_driver_not_supported">Custom drivers not supported</string> |
| 120 | <string name="custom_driver_not_supported_description">Custom driver loading isn\'t currently supported for this device.\nCheck this option again in the future to see if support was added!</string> | 119 | <string name="custom_driver_not_supported_description">Custom driver loading isn\'t currently supported for this device.\nCheck this option again in the future to see if support was added!</string> |
| 120 | <string name="manage_yuzu_data">Manage yuzu data</string> | ||
| 121 | <string name="manage_yuzu_data_description">Import/export firmware, keys, user data, and more!</string> | ||
| 122 | <string name="share_save_file">Share save file</string> | ||
| 123 | <string name="export_save_failed">Failed to export save</string> | ||
| 121 | 124 | ||
| 122 | <!-- About screen strings --> | 125 | <!-- About screen strings --> |
| 123 | <string name="gaia_is_not_real">Gaia isn\'t real</string> | 126 | <string name="gaia_is_not_real">Gaia isn\'t real</string> |