diff options
| author | Charles Lombardo <clombardo169@gmail.com> | 2023-09-25 23:48:28 -0400 | 
|---|---|---|
| committer | Charles Lombardo <clombardo169@gmail.com> | 2023-09-25 23:48:28 -0400 | 
| commit | e9e62968931570e3a511d10509f5d63c859a49e4 (patch) | |
| tree | 5ed24895098ae837ad4d93e47d691b4345d0d14b | |
| parent | a19f62e636b516b8a4d218f3f7327c16bd70af94 (diff) | |
android: Consolidate installers to one fragment
This also allows save imports to happen without starting a game at first.
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 {      external fun submitInlineKeyboardInput(key_code: Int)      /** +     * Creates a generic user directory if it doesn't exist already +     */ +    external fun initializeEmptyUserDirectory() + +    /**       * Button type for use in onTouchEvent       */      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 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.yuzu.yuzu_emu.databinding.CardInstallableBinding +import org.yuzu.yuzu_emu.model.Installable + +class InstallableAdapter(private val installables: List<Installable>) : +    RecyclerView.Adapter<InstallableAdapter.InstallableViewHolder>() { +    override fun onCreateViewHolder( +        parent: ViewGroup, +        viewType: Int +    ): InstallableAdapter.InstallableViewHolder { +        val binding = +            CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false) +        return InstallableViewHolder(binding) +    } + +    override fun getItemCount(): Int = installables.size + +    override fun onBindViewHolder(holder: InstallableAdapter.InstallableViewHolder, position: Int) = +        holder.bind(installables[position]) + +    inner class InstallableViewHolder(val binding: CardInstallableBinding) : +        RecyclerView.ViewHolder(binding.root) { +        lateinit var installable: Installable + +        fun bind(installable: Installable) { +            this.installable = installable + +            binding.title.setText(installable.titleId) +            binding.description.setText(installable.descriptionId) + +            if (installable.install != null) { +                binding.buttonInstall.visibility = View.VISIBLE +                binding.buttonInstall.setOnClickListener { installable.install.invoke() } +            } +            if (installable.export != null) { +                binding.buttonExport.visibility = View.VISIBLE +                binding.buttonExport.setOnClickListener { installable.export.invoke() } +            } +        } +    } +} 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  import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding  import org.yuzu.yuzu_emu.model.HomeViewModel -import org.yuzu.yuzu_emu.ui.main.MainActivity  class AboutFragment : Fragment() {      private var _binding: FragmentAboutBinding? = null @@ -93,12 +92,6 @@ class AboutFragment : Fragment() {              }          } -        val mainActivity = requireActivity() as MainActivity -        binding.buttonExport.setOnClickListener { mainActivity.exportUserData.launch("export.zip") } -        binding.buttonImport.setOnClickListener { -            mainActivity.importUserData.launch(arrayOf("application/zip")) -        } -          binding.buttonDiscord.setOnClickListener { openLink(getString(R.string.support_link)) }          binding.buttonWebsite.setOnClickListener { openLink(getString(R.string.website_link)) }          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() {              )              add(                  HomeSetting( -                    R.string.install_amiibo_keys, -                    R.string.install_amiibo_keys_description, -                    R.drawable.ic_nfc, -                    { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) } -                ) -            ) -            add( -                HomeSetting( -                    R.string.install_game_content, -                    R.string.install_game_content_description, -                    R.drawable.ic_system_update_alt, -                    { mainActivity.installGameUpdate.launch(arrayOf("*/*")) } +                    R.string.manage_yuzu_data, +                    R.string.manage_yuzu_data_description, +                    R.drawable.ic_install, +                    { +                        binding.root.findNavController() +                            .navigate(R.id.action_homeSettingsFragment_to_installableFragment) +                    }                  )              )              add( @@ -150,35 +145,6 @@ class HomeSettingsFragment : Fragment() {              )              add(                  HomeSetting( -                    R.string.manage_save_data, -                    R.string.import_export_saves_description, -                    R.drawable.ic_save, -                    { -                        ImportExportSavesFragment().show( -                            parentFragmentManager, -                            ImportExportSavesFragment.TAG -                        ) -                    } -                ) -            ) -            add( -                HomeSetting( -                    R.string.install_prod_keys, -                    R.string.install_prod_keys_description, -                    R.drawable.ic_unlock, -                    { mainActivity.getProdKey.launch(arrayOf("*/*")) } -                ) -            ) -            add( -                HomeSetting( -                    R.string.install_firmware, -                    R.string.install_firmware_description, -                    R.drawable.ic_firmware, -                    { mainActivity.getFirmware.launch(arrayOf("application/zip")) } -                ) -            ) -            add( -                HomeSetting(                      R.string.share_log,                      R.string.share_log_description,                      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 @@ -// SPDX-FileCopyrightText: 2023 yuzu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.yuzu.yuzu_emu.fragments - -import android.app.Dialog -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.provider.DocumentsContract -import android.widget.Toast -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity -import androidx.documentfile.provider.DocumentFile -import androidx.fragment.app.DialogFragment -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import java.io.BufferedOutputStream -import java.io.File -import java.io.FileOutputStream -import java.io.FilenameFilter -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.yuzu.yuzu_emu.R -import org.yuzu.yuzu_emu.YuzuApplication -import org.yuzu.yuzu_emu.features.DocumentProvider -import org.yuzu.yuzu_emu.getPublicFilesDir -import org.yuzu.yuzu_emu.utils.FileUtil - -class ImportExportSavesFragment : DialogFragment() { -    private val context = YuzuApplication.appContext -    private val savesFolder = -        "${context.getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000" - -    // Get first subfolder in saves folder (should be the user folder) -    private val savesFolderRoot = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: "" -    private var lastZipCreated: File? = null - -    private lateinit var startForResultExportSave: ActivityResultLauncher<Intent> -    private lateinit var documentPicker: ActivityResultLauncher<Array<String>> - -    override fun onCreate(savedInstanceState: Bundle?) { -        super.onCreate(savedInstanceState) -        val activity = requireActivity() as AppCompatActivity - -        val activityResultRegistry = requireActivity().activityResultRegistry -        startForResultExportSave = activityResultRegistry.register( -            "startForResultExportSaveKey", -            ActivityResultContracts.StartActivityForResult() -        ) { -            File(context.getPublicFilesDir().canonicalPath, "temp").deleteRecursively() -        } -        documentPicker = activityResultRegistry.register( -            "documentPickerKey", -            ActivityResultContracts.OpenDocument() -        ) { -            it?.let { uri -> importSave(uri, activity) } -        } -    } - -    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { -        return if (savesFolderRoot == "") { -            MaterialAlertDialogBuilder(requireContext()) -                .setTitle(R.string.manage_save_data) -                .setMessage(R.string.import_export_saves_no_profile) -                .setPositiveButton(android.R.string.ok, null) -                .show() -        } else { -            MaterialAlertDialogBuilder(requireContext()) -                .setTitle(R.string.manage_save_data) -                .setMessage(R.string.manage_save_data_description) -                .setNegativeButton(R.string.export_saves) { _, _ -> -                    exportSave() -                } -                .setPositiveButton(R.string.import_saves) { _, _ -> -                    documentPicker.launch(arrayOf("application/zip")) -                } -                .setNeutralButton(android.R.string.cancel, null) -                .show() -        } -    } - -    /** -     * Zips the save files located in the given folder path and creates a new zip file with the current date and time. -     * @return true if the zip file is successfully created, false otherwise. -     */ -    private fun zipSave(): Boolean { -        try { -            val tempFolder = File(requireContext().getPublicFilesDir().canonicalPath, "temp") -            tempFolder.mkdirs() -            val saveFolder = File(savesFolderRoot) -            val outputZipFile = File( -                tempFolder, -                "yuzu saves - ${ -                LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) -                }.zip" -            ) -            outputZipFile.createNewFile() -            ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos -> -                saveFolder.walkTopDown().forEach { file -> -                    val zipFileName = -                        file.absolutePath.removePrefix(savesFolderRoot).removePrefix("/") -                    if (zipFileName == "") { -                        return@forEach -                    } -                    val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}") -                    zos.putNextEntry(entry) -                    if (file.isFile) { -                        file.inputStream().use { fis -> fis.copyTo(zos) } -                    } -                } -            } -            lastZipCreated = outputZipFile -        } catch (e: Exception) { -            return false -        } -        return true -    } - -    /** -     * Exports the save file located in the given folder path by creating a zip file and sharing it via intent. -     */ -    private fun exportSave() { -        CoroutineScope(Dispatchers.IO).launch { -            val wasZipCreated = zipSave() -            val lastZipFile = lastZipCreated -            if (!wasZipCreated || lastZipFile == null) { -                withContext(Dispatchers.Main) { -                    Toast.makeText(context, "Failed to export save", Toast.LENGTH_LONG).show() -                } -                return@launch -            } - -            withContext(Dispatchers.Main) { -                val file = DocumentFile.fromSingleUri( -                    context, -                    DocumentsContract.buildDocumentUri( -                        DocumentProvider.AUTHORITY, -                        "${DocumentProvider.ROOT_ID}/temp/${lastZipFile.name}" -                    ) -                )!! -                val intent = Intent(Intent.ACTION_SEND) -                    .setDataAndType(file.uri, "application/zip") -                    .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) -                    .putExtra(Intent.EXTRA_STREAM, file.uri) -                startForResultExportSave.launch(Intent.createChooser(intent, "Share save file")) -            } -        } -    } - -    /** -     * Imports the save files contained in the zip file, and replaces any existing ones with the new save file. -     * @param zipUri The Uri of the zip file containing the save file(s) to import. -     */ -    private fun importSave(zipUri: Uri, activity: AppCompatActivity) { -        val inputZip = context.contentResolver.openInputStream(zipUri) -        // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid. -        var validZip = false -        val savesFolder = File(savesFolderRoot) -        val cacheSaveDir = File("${context.cacheDir.path}/saves/") -        cacheSaveDir.mkdir() - -        if (inputZip == null) { -            Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG) -                .show() -            return -        } - -        val filterTitleId = -            FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) } - -        try { -            CoroutineScope(Dispatchers.IO).launch { -                FileUtil.unzip(inputZip, cacheSaveDir) -                cacheSaveDir.list(filterTitleId)?.forEach { savePath -> -                    File(savesFolder, savePath).deleteRecursively() -                    File(cacheSaveDir, savePath).copyRecursively(File(savesFolder, savePath), true) -                    validZip = true -                } - -                withContext(Dispatchers.Main) { -                    if (!validZip) { -                        MessageDialogFragment.newInstance( -                            requireActivity(), -                            titleId = R.string.save_file_invalid_zip_structure, -                            descriptionId = R.string.save_file_invalid_zip_structure_description -                        ).show(activity.supportFragmentManager, MessageDialogFragment.TAG) -                        return@withContext -                    } -                    Toast.makeText( -                        context, -                        context.getString(R.string.save_file_imported_success), -                        Toast.LENGTH_LONG -                    ).show() -                } - -                cacheSaveDir.deleteRecursively() -            } -        } catch (e: Exception) { -            Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG) -                .show() -        } -    } - -    companion object { -        const val TAG = "ImportExportSavesFragment" -    } -} 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 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.findNavController +import androidx.recyclerview.widget.GridLayoutManager +import com.google.android.material.transition.MaterialSharedAxis +import org.yuzu.yuzu_emu.R +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.ui.main.MainActivity + +class InstallableFragment : Fragment() { +    private var _binding: FragmentInstallablesBinding? = null +    private val binding get() = _binding!! + +    private val homeViewModel: HomeViewModel by activityViewModels() + +    override fun onCreate(savedInstanceState: Bundle?) { +        super.onCreate(savedInstanceState) +        enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) +        returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) +        reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) +    } + +    override fun onCreateView( +        inflater: LayoutInflater, +        container: ViewGroup?, +        savedInstanceState: Bundle? +    ): View { +        _binding = FragmentInstallablesBinding.inflate(layoutInflater) +        return binding.root +    } + +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) { +        super.onViewCreated(view, savedInstanceState) + +        val mainActivity = requireActivity() as MainActivity + +        homeViewModel.setNavigationVisibility(visible = false, animated = true) +        homeViewModel.setStatusBarShadeVisibility(visible = false) + +        binding.toolbarInstallables.setNavigationOnClickListener { +            binding.root.findNavController().popBackStack() +        } + +        val installables = listOf( +            Installable( +                R.string.user_data, +                R.string.user_data_description, +                install = { mainActivity.importUserData.launch(arrayOf("application/zip")) }, +                export = { mainActivity.exportUserData.launch("export.zip") } +            ), +            Installable( +                R.string.install_game_content, +                R.string.install_game_content_description, +                install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) } +            ), +            Installable( +                R.string.install_firmware, +                R.string.install_firmware_description, +                install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) } +            ), +            if (mainActivity.savesFolderRoot != "") { +                Installable( +                    R.string.manage_save_data, +                    R.string.import_export_saves_description, +                    install = { mainActivity.importSaves.launch(arrayOf("application/zip")) }, +                    export = { mainActivity.exportSave() } +                ) +            } else { +                Installable( +                    R.string.manage_save_data, +                    R.string.import_export_saves_description, +                    install = { mainActivity.importSaves.launch(arrayOf("application/zip")) } +                ) +            }, +            Installable( +                R.string.install_prod_keys, +                R.string.install_prod_keys_description, +                install = { mainActivity.getProdKey.launch(arrayOf("*/*")) } +            ), +            Installable( +                R.string.install_amiibo_keys, +                R.string.install_amiibo_keys_description, +                install = { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) } +            ) +        ) + +        binding.listInstallables.apply { +            layoutManager = GridLayoutManager( +                requireContext(), +                resources.getInteger(R.integer.grid_columns) +            ) +            adapter = InstallableAdapter(installables) +        } + +        setInsets() +    } + +    private fun setInsets() = +        ViewCompat.setOnApplyWindowInsetsListener( +            binding.root +        ) { _: View, windowInsets: WindowInsetsCompat -> +            val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) +            val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) + +            val leftInsets = barInsets.left + cutoutInsets.left +            val rightInsets = barInsets.right + cutoutInsets.right + +            val mlpAppBar = binding.toolbarInstallables.layoutParams as ViewGroup.MarginLayoutParams +            mlpAppBar.leftMargin = leftInsets +            mlpAppBar.rightMargin = rightInsets +            binding.toolbarInstallables.layoutParams = mlpAppBar + +            val mlpScrollAbout = +                binding.listInstallables.layoutParams as ViewGroup.MarginLayoutParams +            mlpScrollAbout.leftMargin = leftInsets +            mlpScrollAbout.rightMargin = rightInsets +            binding.listInstallables.layoutParams = mlpScrollAbout + +            binding.listInstallables.updatePadding(bottom = barInsets.bottom) + +            windowInsets +        } +} 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 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.model + +import androidx.annotation.StringRes + +data class Installable( +    @StringRes val titleId: Int, +    @StringRes val descriptionId: Int, +    val install: (() -> Unit)? = null, +    val export: (() -> Unit)? = null +) 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  import android.content.Intent  import android.net.Uri  import android.os.Bundle +import android.provider.DocumentsContract  import android.view.View  import android.view.ViewGroup.MarginLayoutParams  import android.view.WindowManager @@ -19,6 +20,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen  import androidx.core.view.ViewCompat  import androidx.core.view.WindowCompat  import androidx.core.view.WindowInsetsCompat +import androidx.documentfile.provider.DocumentFile  import androidx.lifecycle.Lifecycle  import androidx.lifecycle.lifecycleScope  import androidx.lifecycle.repeatOnLifecycle @@ -29,6 +31,7 @@ import androidx.preference.PreferenceManager  import com.google.android.material.color.MaterialColors  import com.google.android.material.dialog.MaterialAlertDialogBuilder  import com.google.android.material.navigation.NavigationBarView +import kotlinx.coroutines.CoroutineScope  import java.io.File  import java.io.FilenameFilter  import java.io.IOException @@ -41,9 +44,11 @@ import org.yuzu.yuzu_emu.R  import org.yuzu.yuzu_emu.activities.EmulationActivity  import org.yuzu.yuzu_emu.databinding.ActivityMainBinding  import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding +import org.yuzu.yuzu_emu.features.DocumentProvider  import org.yuzu.yuzu_emu.features.settings.model.Settings  import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment  import org.yuzu.yuzu_emu.fragments.MessageDialogFragment +import org.yuzu.yuzu_emu.getPublicFilesDir  import org.yuzu.yuzu_emu.model.GamesViewModel  import org.yuzu.yuzu_emu.model.HomeViewModel  import org.yuzu.yuzu_emu.model.TaskViewModel @@ -52,6 +57,8 @@ import java.io.BufferedInputStream  import java.io.BufferedOutputStream  import java.io.FileInputStream  import java.io.FileOutputStream +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter  import java.util.zip.ZipEntry  import java.util.zip.ZipInputStream  import java.util.zip.ZipOutputStream @@ -65,6 +72,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {      override var themeId: Int = 0 +    private val savesFolder +        get() = "${getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000" + +    // Get first subfolder in saves folder (should be the user folder) +    val savesFolderRoot get() = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: "" +    private var lastZipCreated: File? = null +      override fun onCreate(savedInstanceState: Bundle?) {          val splashScreen = installSplashScreen()          splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } @@ -727,4 +741,152 @@ class MainActivity : AppCompatActivity(), ThemeProvider {                  return@newInstance getString(R.string.user_data_import_success)              }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)          } + +    /** +     * Zips the save files located in the given folder path and creates a new zip file with the current date and time. +     * @return true if the zip file is successfully created, false otherwise. +     */ +    private fun zipSave(): Boolean { +        try { +            val tempFolder = File(getPublicFilesDir().canonicalPath, "temp") +            tempFolder.mkdirs() +            val saveFolder = File(savesFolderRoot) +            val outputZipFile = File( +                tempFolder, +                "yuzu saves - ${ +                LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) +                }.zip" +            ) +            outputZipFile.createNewFile() +            ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos -> +                saveFolder.walkTopDown().forEach { file -> +                    val zipFileName = +                        file.absolutePath.removePrefix(savesFolderRoot).removePrefix("/") +                    if (zipFileName == "") { +                        return@forEach +                    } +                    val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}") +                    zos.putNextEntry(entry) +                    if (file.isFile) { +                        file.inputStream().use { fis -> fis.copyTo(zos) } +                    } +                } +            } +            lastZipCreated = outputZipFile +        } catch (e: Exception) { +            return false +        } +        return true +    } + +    /** +     * Exports the save file located in the given folder path by creating a zip file and sharing it via intent. +     */ +    fun exportSave() { +        CoroutineScope(Dispatchers.IO).launch { +            val wasZipCreated = zipSave() +            val lastZipFile = lastZipCreated +            if (!wasZipCreated || lastZipFile == null) { +                withContext(Dispatchers.Main) { +                    Toast.makeText( +                        this@MainActivity, +                        getString(R.string.export_save_failed), +                        Toast.LENGTH_LONG +                    ).show() +                } +                return@launch +            } + +            withContext(Dispatchers.Main) { +                val file = DocumentFile.fromSingleUri( +                    this@MainActivity, +                    DocumentsContract.buildDocumentUri( +                        DocumentProvider.AUTHORITY, +                        "${DocumentProvider.ROOT_ID}/temp/${lastZipFile.name}" +                    ) +                )!! +                val intent = Intent(Intent.ACTION_SEND) +                    .setDataAndType(file.uri, "application/zip") +                    .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) +                    .putExtra(Intent.EXTRA_STREAM, file.uri) +                startForResultExportSave.launch( +                    Intent.createChooser( +                        intent, +                        getString(R.string.share_save_file) +                    ) +                ) +            } +        } +    } + +    private val startForResultExportSave = +        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ -> +            File(getPublicFilesDir().canonicalPath, "temp").deleteRecursively() +        } + +    val importSaves = +        registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> +            if (result == null) { +                return@registerForActivityResult +            } + +            NativeLibrary.initializeEmptyUserDirectory() + +            val inputZip = applicationContext.contentResolver.openInputStream(result) +            // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid. +            var validZip = false +            val savesFolder = File(savesFolderRoot) +            val cacheSaveDir = File("${applicationContext.cacheDir.path}/saves/") +            cacheSaveDir.mkdir() + +            if (inputZip == null) { +                Toast.makeText( +                    applicationContext, +                    getString(R.string.fatal_error), +                    Toast.LENGTH_LONG +                ).show() +                return@registerForActivityResult +            } + +            val filterTitleId = +                FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) } + +            try { +                CoroutineScope(Dispatchers.IO).launch { +                    FileUtil.unzip(inputZip, cacheSaveDir) +                    cacheSaveDir.list(filterTitleId)?.forEach { savePath -> +                        File(savesFolder, savePath).deleteRecursively() +                        File(cacheSaveDir, savePath).copyRecursively( +                            File(savesFolder, savePath), +                            true +                        ) +                        validZip = true +                    } + +                    withContext(Dispatchers.Main) { +                        if (!validZip) { +                            MessageDialogFragment.newInstance( +                                this@MainActivity, +                                titleId = R.string.save_file_invalid_zip_structure, +                                descriptionId = R.string.save_file_invalid_zip_structure_description +                            ).show(supportFragmentManager, MessageDialogFragment.TAG) +                            return@withContext +                        } +                        Toast.makeText( +                            applicationContext, +                            getString(R.string.save_file_imported_success), +                            Toast.LENGTH_LONG +                        ).show() +                    } + +                    cacheSaveDir.deleteRecursively() +                } +            } catch (e: Exception) { +                Toast.makeText( +                    applicationContext, +                    getString(R.string.fatal_error), +                    Toast.LENGTH_LONG +                ).show() +            } +        }  } 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 @@  #include <android/api-level.h>  #include <android/native_window_jni.h> +#include <common/fs/fs.h> +#include <core/file_sys/savedata_factory.h>  #include <core/loader/nro.h>  #include <jni.h> @@ -879,4 +881,24 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_submitInlineKeyboardInput(JNIEnv* env      EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardInput(j_key_code);  } +void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmptyUserDirectory(JNIEnv* env, +                                                                        jobject instance) { +    const auto nand_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir); +    auto vfs_nand_dir = EmulationSession::GetInstance().System().GetFilesystem()->OpenDirectory( +        Common::FS::PathToUTF8String(nand_dir), FileSys::Mode::Read); + +    Service::Account::ProfileManager manager; +    const auto user_id = manager.GetUser(static_cast<std::size_t>(0)); +    ASSERT(user_id); + +    const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath( +        EmulationSession::GetInstance().System(), vfs_nand_dir, FileSys::SaveDataSpaceId::NandUser, +        FileSys::SaveDataType::SaveData, 1, user_id->AsU128(), 0); + +    const auto full_path = Common::FS::ConcatPathSafe(nand_dir, user_save_data_path); +    if (!Common::FS::CreateParentDirs(full_path)) { +        LOG_WARNING(Frontend, "Failed to create full path of the default user's save directory"); +    } +} +  } // 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:app="http://schemas.android.com/apk/res-auto" +    xmlns:tools="http://schemas.android.com/tools" +    style="?attr/materialCardViewOutlinedStyle" +    android:layout_width="match_parent" +    android:layout_height="wrap_content" +    android:layout_marginHorizontal="16dp" +    android:layout_marginVertical="12dp"> + +    <LinearLayout +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:layout_margin="16dp" +        android:orientation="horizontal" +        android:layout_gravity="center"> + +        <LinearLayout +            android:layout_width="0dp" +            android:layout_height="wrap_content" +            android:layout_marginEnd="16dp" +            android:layout_weight="1" +            android:orientation="vertical"> + +            <com.google.android.material.textview.MaterialTextView +                android:id="@+id/title" +                style="@style/TextAppearance.Material3.TitleMedium" +                android:layout_width="match_parent" +                android:layout_height="wrap_content" +                android:text="@string/user_data" +                android:textAlignment="viewStart" /> + +            <com.google.android.material.textview.MaterialTextView +                android:id="@+id/description" +                style="@style/TextAppearance.Material3.BodyMedium" +                android:layout_width="match_parent" +                android:layout_height="wrap_content" +                android:layout_marginTop="6dp" +                android:text="@string/user_data_description" +                android:textAlignment="viewStart" /> + +        </LinearLayout> + +        <Button +            android:id="@+id/button_export" +            style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" +            android:layout_gravity="center_vertical" +            android:contentDescription="@string/export" +            android:tooltipText="@string/export" +            android:visibility="gone" +            app:icon="@drawable/ic_export" +            tools:visibility="visible" /> + +        <Button +            android:id="@+id/button_install" +            style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" +            android:layout_gravity="center_vertical" +            android:layout_marginStart="12dp" +            android:contentDescription="@string/string_import" +            android:tooltipText="@string/string_import" +            android:visibility="gone" +            app:icon="@drawable/ic_import" +            tools:visibility="visible" /> + +    </LinearLayout> + +</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 @@              <LinearLayout                  android:layout_width="match_parent"                  android:layout_height="wrap_content" -                android:orientation="horizontal"> - -                <LinearLayout -                    android:layout_width="match_parent" -                    android:layout_height="wrap_content" -                    android:paddingVertical="16dp" -                    android:paddingHorizontal="16dp" -                    android:orientation="vertical" -                    android:layout_weight="1"> - -                    <com.google.android.material.textview.MaterialTextView -                        style="@style/TextAppearance.Material3.TitleMedium" -                        android:layout_width="match_parent" -                        android:layout_height="wrap_content" -                        android:layout_marginHorizontal="24dp" -                        android:textAlignment="viewStart" -                        android:text="@string/user_data" /> - -                    <com.google.android.material.textview.MaterialTextView -                        style="@style/TextAppearance.Material3.BodyMedium" -                        android:layout_width="match_parent" -                        android:layout_height="wrap_content" -                        android:layout_marginHorizontal="24dp" -                        android:layout_marginTop="6dp" -                        android:textAlignment="viewStart" -                        android:text="@string/user_data_description" /> - -                </LinearLayout> - -                <Button -                    android:id="@+id/button_import" -                    style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" -                    android:layout_width="wrap_content" -                    android:layout_height="wrap_content" -                    android:layout_gravity="center_vertical" -                    android:contentDescription="@string/string_import" -                    android:tooltipText="@string/string_import" -                    app:icon="@drawable/ic_import" /> - -                <Button -                    android:id="@+id/button_export" -                    style="@style/Widget.Material3.Button.IconButton.Filled.Tonal" -                    android:layout_width="wrap_content" -                    android:layout_height="wrap_content" -                    android:layout_marginStart="12dp" -                    android:layout_marginEnd="24dp" -                    android:layout_gravity="center_vertical" -                    android:contentDescription="@string/export" -                    android:tooltipText="@string/export" -                    app:icon="@drawable/ic_export" /> - -            </LinearLayout> - -            <com.google.android.material.divider.MaterialDivider -                android:layout_width="match_parent" -                android:layout_height="wrap_content" -                android:layout_marginHorizontal="20dp" /> - -            <LinearLayout -                android:layout_width="match_parent" -                android:layout_height="wrap_content"                  android:orientation="horizontal"                  android:gravity="center_horizontal"                  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 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:app="http://schemas.android.com/apk/res-auto" +    android:id="@+id/coordinator_licenses" +    android:layout_width="match_parent" +    android:layout_height="match_parent" +    android:background="?attr/colorSurface"> + +    <com.google.android.material.appbar.AppBarLayout +        android:id="@+id/appbar_installables" +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:fitsSystemWindows="true"> + +        <com.google.android.material.appbar.MaterialToolbar +            android:id="@+id/toolbar_installables" +            android:layout_width="match_parent" +            android:layout_height="?attr/actionBarSize" +            app:title="@string/manage_yuzu_data" +            app:navigationIcon="@drawable/ic_back" /> + +    </com.google.android.material.appbar.AppBarLayout> + +    <androidx.recyclerview.widget.RecyclerView +        android:id="@+id/list_installables" +        android:layout_width="match_parent" +        android:layout_height="match_parent" +        android:clipToPadding="false" +        app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + +</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 @@          <action              android:id="@+id/action_homeSettingsFragment_to_earlyAccessFragment"              app:destination="@id/earlyAccessFragment" /> +        <action +            android:id="@+id/action_homeSettingsFragment_to_installableFragment" +            app:destination="@id/installableFragment" />      </fragment>      <fragment @@ -88,5 +91,9 @@      <action          android:id="@+id/action_global_settingsActivity"          app:destination="@id/settingsActivity" /> +    <fragment +        android:id="@+id/installableFragment" +        android:name="org.yuzu.yuzu_emu.fragments.InstallableFragment" +        android:label="InstallableFragment" />  </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 @@      <string name="manage_save_data">Speicherdaten verwalten</string>      <string name="manage_save_data_description">Speicherdaten gefunden. Bitte wähle unten eine Option aus.</string>      <string name="import_export_saves_description">Speicherdaten importieren oder exportieren</string> -    <string name="import_export_saves_no_profile">Keine Speicherdaten gefunden. Bitte starte ein Spiel und versuche es erneut.</string>      <string name="save_file_imported_success">Erfolgreich importiert</string>      <string name="save_file_invalid_zip_structure">Ungültige Speicherverzeichnisstruktur</string>      <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 @@      <string name="manage_save_data">Administrar datos de guardado</string>      <string name="manage_save_data_description">Guardar los datos encontrados. Por favor, seleccione una opción de abajo.</string>      <string name="import_export_saves_description">Importar o exportar archivos de guardado</string> -    <string name="import_export_saves_no_profile">No se han encontrado datos de guardado. Por favor, ejecute un juego y vuelva a intentarlo.</string>      <string name="save_file_imported_success">Importado correctamente</string>      <string name="save_file_invalid_zip_structure">Estructura del directorio de guardado no válido</string>      <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 @@      <string name="manage_save_data">Gérer les données de sauvegarde</string>      <string name="manage_save_data_description">Données de sauvegarde trouvées. Veuillez sélectionner une option ci-dessous.</string>      <string name="import_export_saves_description">Importer ou exporter des fichiers de sauvegarde</string> -    <string name="import_export_saves_no_profile">Aucune données de sauvegarde trouvées. Veuillez lancer un jeu et réessayer.</string>      <string name="save_file_imported_success">Importé avec succès</string>      <string name="save_file_invalid_zip_structure">Structure de répertoire de sauvegarde non valide</string>      <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 @@      <string name="manage_save_data">Gestisci i salvataggi</string>      <string name="manage_save_data_description">Salvataggio non trovato. Seleziona un\'opzione di seguito.</string>      <string name="import_export_saves_description">Importa o esporta i salvataggi</string> -    <string name="import_export_saves_no_profile">Nessun salvataggio trovato. Avvia un gioco e riprova.</string>      <string name="save_file_imported_success">Importato con successo</string>      <string name="save_file_invalid_zip_structure">La struttura della cartella dei salvataggi è invalida</string>      <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 @@      <string name="manage_save_data">セーブデータを管理</string>      <string name="manage_save_data_description">セーブデータが見つかりました。以下のオプションから選択してください。</string>      <string name="import_export_saves_description">セーブファイルをインポート/エクスポート</string> -    <string name="import_export_saves_no_profile">セーブデータがありません。ゲームを起動してから再度お試しください。</string>      <string name="save_file_imported_success">インポートが完了しました</string>      <string name="save_file_invalid_zip_structure">セーブデータのディレクトリ構造が無効です</string>      <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 @@      <string name="manage_save_data">저장 데이터 관리</string>      <string name="manage_save_data_description">데이터를 저장했습니다. 아래에서 옵션을 선택하세요.</string>      <string name="import_export_saves_description">저장 파일 가져오기 또는 내보내기</string> -    <string name="import_export_saves_no_profile">저장 데이터를 찾을 수 없습니다. 게임을 실행한 후 다시 시도하세요.</string>      <string name="save_file_imported_success">가져오기 성공</string>      <string name="save_file_invalid_zip_structure">저장 디렉터리 구조가 잘못됨</string>      <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 @@      <string name="manage_save_data">Administrere lagringsdata</string>      <string name="manage_save_data_description">Lagringsdata funnet. Velg et alternativ nedenfor.</string>      <string name="import_export_saves_description">Importer eller eksporter lagringsfiler</string> -    <string name="import_export_saves_no_profile">Ingen lagringsdata funnet. Start et nytt spill og prøv på nytt.</string>      <string name="save_file_imported_success">Vellykket import</string>      <string name="save_file_invalid_zip_structure">Ugyldig struktur for lagringskatalog</string>      <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 @@      <string name="manage_save_data">Zarządzaj plikami zapisów gier</string>      <string name="manage_save_data_description">Znaleziono pliki zapisów gier. Wybierz opcję poniżej.</string>      <string name="import_export_saves_description">Importuj lub wyeksportuj pliki zapisów</string> -    <string name="import_export_saves_no_profile">Nie znaleziono plików zapisów. Uruchom grę i spróbuj ponownie.</string>      <string name="save_file_imported_success">Zaimportowano pomyślnie</string>      <string name="save_file_invalid_zip_structure">Niepoprawna struktura folderów</string>      <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 @@      <string name="manage_save_data">Gerir dados guardados</string>      <string name="manage_save_data_description">Dados não encontrados. Por favor seleciona uma opção abaixo.</string>      <string name="import_export_saves_description">Importa ou exporta dados guardados</string> -    <string name="import_export_saves_no_profile">Dados não encontrados. Por favor lança o jogo e tenta novamente.</string>      <string name="save_file_imported_success">Importado com sucesso</string>      <string name="save_file_invalid_zip_structure">Estrutura de diretório de dados invalida</string>      <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 @@      <string name="manage_save_data">Gerir dados guardados</string>      <string name="manage_save_data_description">Dados não encontrados. Por favor seleciona uma opção abaixo.</string>      <string name="import_export_saves_description">Importa ou exporta dados guardados</string> -    <string name="import_export_saves_no_profile">Dados não encontrados. Por favor lança o jogo e tenta novamente.</string>      <string name="save_file_imported_success">Importado com sucesso</string>      <string name="save_file_invalid_zip_structure">Estrutura de diretório de dados invalida</string>      <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 @@      <string name="manage_save_data">Управление данными сохранений</string>      <string name="manage_save_data_description">Найдено данные сохранений. Пожалуйста, выберите вариант ниже.</string>      <string name="import_export_saves_description">Импорт или экспорт файлов сохранения</string> -    <string name="import_export_saves_no_profile">Данные сохранений не найдены. Пожалуйста, запустите игру и повторите попытку.</string>      <string name="save_file_imported_success">Успешно импортировано</string>      <string name="save_file_invalid_zip_structure">Недопустимая структура папки сохранения</string>      <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 @@      <string name="manage_save_data">Керування даними збережень</string>      <string name="manage_save_data_description">Знайдено дані збережень. Будь ласка, виберіть варіант нижче.</string>      <string name="import_export_saves_description">Імпорт або експорт файлів збереження</string> -    <string name="import_export_saves_no_profile">Дані збережень не знайдено. Будь ласка, запустіть гру та повторіть спробу.</string>      <string name="save_file_imported_success">Успішно імпортовано</string>      <string name="save_file_invalid_zip_structure">Неприпустима структура папки збереження</string>      <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 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + +    <integer name="grid_columns">2</integer> + +</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 @@      <string name="manage_save_data">管理存档数据</string>      <string name="manage_save_data_description">已找到存档数据,请选择下方的选项。</string>      <string name="import_export_saves_description">导入或导出存档</string> -    <string name="import_export_saves_no_profile">找不到存档数据,请启动游戏并重试。</string>      <string name="save_file_imported_success">已成功导入存档</string>      <string name="save_file_invalid_zip_structure">无效的存档目录</string>      <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 @@      <string name="manage_save_data">管理儲存資料</string>      <string name="manage_save_data_description">已找到儲存資料,請選取下方的選項。</string>      <string name="import_export_saves_description">匯入或匯出儲存檔案</string> -    <string name="import_export_saves_no_profile">找不到儲存資料,請啟動遊戲並重試。</string>      <string name="save_file_imported_success">已成功匯入</string>      <string name="save_file_invalid_zip_structure">無效的儲存目錄結構</string>      <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 @@  <?xml version="1.0" encoding="utf-8"?>  <resources> -    <integer name="game_title_lines">2</integer> +    <integer name="grid_columns">1</integer>      <!-- Default SWITCH landscape layout -->      <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 @@      <string name="manage_save_data">Manage save data</string>      <string name="manage_save_data_description">Save data found. Please select an option below.</string>      <string name="import_export_saves_description">Import or export save files</string> -    <string name="import_export_saves_no_profile">No save data found. Please launch a game and retry.</string>      <string name="save_file_imported_success">Imported successfully</string>      <string name="save_file_invalid_zip_structure">Invalid save directory structure</string>      <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 @@      <string name="install_game_content_help_link">https://yuzu-emu.org/help/quickstart/#dumping-installed-updates</string>      <string name="custom_driver_not_supported">Custom drivers not supported</string>      <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> +    <string name="manage_yuzu_data">Manage yuzu data</string> +    <string name="manage_yuzu_data_description">Import/export firmware, keys, user data, and more!</string> +    <string name="share_save_file">Share save file</string> +    <string name="export_save_failed">Failed to export save</string>      <!-- About screen strings -->      <string name="gaia_is_not_real">Gaia isn\'t real</string> | 
