|
|
@@ -7,20 +7,39 @@ import android.os.Bundle
|
|
|
import android.view.LayoutInflater
|
|
|
import android.view.View
|
|
|
import android.view.ViewGroup
|
|
|
+import android.widget.Toast
|
|
|
+import androidx.activity.result.contract.ActivityResultContracts
|
|
|
import androidx.core.view.ViewCompat
|
|
|
import androidx.core.view.WindowInsetsCompat
|
|
|
import androidx.core.view.updatePadding
|
|
|
import androidx.fragment.app.Fragment
|
|
|
import androidx.fragment.app.activityViewModels
|
|
|
+import androidx.lifecycle.Lifecycle
|
|
|
+import androidx.lifecycle.lifecycleScope
|
|
|
+import androidx.lifecycle.repeatOnLifecycle
|
|
|
import androidx.navigation.findNavController
|
|
|
import androidx.recyclerview.widget.GridLayoutManager
|
|
|
import com.google.android.material.transition.MaterialSharedAxis
|
|
|
+import kotlinx.coroutines.Dispatchers
|
|
|
+import kotlinx.coroutines.launch
|
|
|
+import kotlinx.coroutines.withContext
|
|
|
+import org.yuzu.yuzu_emu.NativeLibrary
|
|
|
import org.yuzu.yuzu_emu.R
|
|
|
+import org.yuzu.yuzu_emu.YuzuApplication
|
|
|
import org.yuzu.yuzu_emu.adapters.InstallableAdapter
|
|
|
import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding
|
|
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
|
|
import org.yuzu.yuzu_emu.model.Installable
|
|
|
+import org.yuzu.yuzu_emu.model.TaskState
|
|
|
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
|
|
+import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
|
|
+import org.yuzu.yuzu_emu.utils.FileUtil
|
|
|
+import java.io.BufferedInputStream
|
|
|
+import java.io.BufferedOutputStream
|
|
|
+import java.io.File
|
|
|
+import java.math.BigInteger
|
|
|
+import java.time.LocalDateTime
|
|
|
+import java.time.format.DateTimeFormatter
|
|
|
|
|
|
class InstallableFragment : Fragment() {
|
|
|
private var _binding: FragmentInstallablesBinding? = null
|
|
|
@@ -56,6 +75,17 @@ class InstallableFragment : Fragment() {
|
|
|
binding.root.findNavController().popBackStack()
|
|
|
}
|
|
|
|
|
|
+ viewLifecycleOwner.lifecycleScope.launch {
|
|
|
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
|
|
|
+ homeViewModel.openImportSaves.collect {
|
|
|
+ if (it) {
|
|
|
+ importSaves.launch(arrayOf("application/zip"))
|
|
|
+ homeViewModel.setOpenImportSaves(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
val installables = listOf(
|
|
|
Installable(
|
|
|
R.string.user_data,
|
|
|
@@ -63,6 +93,43 @@ class InstallableFragment : Fragment() {
|
|
|
install = { mainActivity.importUserData.launch(arrayOf("application/zip")) },
|
|
|
export = { mainActivity.exportUserData.launch("export.zip") }
|
|
|
),
|
|
|
+ Installable(
|
|
|
+ R.string.manage_save_data,
|
|
|
+ R.string.manage_save_data_description,
|
|
|
+ install = {
|
|
|
+ MessageDialogFragment.newInstance(
|
|
|
+ requireActivity(),
|
|
|
+ titleId = R.string.import_save_warning,
|
|
|
+ descriptionId = R.string.import_save_warning_description,
|
|
|
+ positiveAction = { homeViewModel.setOpenImportSaves(true) }
|
|
|
+ ).show(parentFragmentManager, MessageDialogFragment.TAG)
|
|
|
+ },
|
|
|
+ export = {
|
|
|
+ val oldSaveDataFolder = File(
|
|
|
+ "${DirectoryInitialization.userDirectory}/nand" +
|
|
|
+ NativeLibrary.getDefaultProfileSaveDataRoot(false)
|
|
|
+ )
|
|
|
+ val futureSaveDataFolder = File(
|
|
|
+ "${DirectoryInitialization.userDirectory}/nand" +
|
|
|
+ NativeLibrary.getDefaultProfileSaveDataRoot(true)
|
|
|
+ )
|
|
|
+ if (!oldSaveDataFolder.exists() && !futureSaveDataFolder.exists()) {
|
|
|
+ Toast.makeText(
|
|
|
+ YuzuApplication.appContext,
|
|
|
+ R.string.no_save_data_found,
|
|
|
+ Toast.LENGTH_SHORT
|
|
|
+ ).show()
|
|
|
+ return@Installable
|
|
|
+ } else {
|
|
|
+ exportSaves.launch(
|
|
|
+ "${getString(R.string.save_data)} " +
|
|
|
+ LocalDateTime.now().format(
|
|
|
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ),
|
|
|
Installable(
|
|
|
R.string.install_game_content,
|
|
|
R.string.install_game_content_description,
|
|
|
@@ -121,4 +188,156 @@ class InstallableFragment : Fragment() {
|
|
|
|
|
|
windowInsets
|
|
|
}
|
|
|
+
|
|
|
+ private val importSaves =
|
|
|
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
|
|
+ if (result == null) {
|
|
|
+ return@registerForActivityResult
|
|
|
+ }
|
|
|
+
|
|
|
+ val inputZip = requireContext().contentResolver.openInputStream(result)
|
|
|
+ val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
|
|
|
+ cacheSaveDir.mkdir()
|
|
|
+
|
|
|
+ if (inputZip == null) {
|
|
|
+ Toast.makeText(
|
|
|
+ YuzuApplication.appContext,
|
|
|
+ getString(R.string.fatal_error),
|
|
|
+ Toast.LENGTH_LONG
|
|
|
+ ).show()
|
|
|
+ return@registerForActivityResult
|
|
|
+ }
|
|
|
+
|
|
|
+ IndeterminateProgressDialogFragment.newInstance(
|
|
|
+ requireActivity(),
|
|
|
+ R.string.save_files_importing,
|
|
|
+ false
|
|
|
+ ) {
|
|
|
+ try {
|
|
|
+ FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
|
|
|
+ val files = cacheSaveDir.listFiles()
|
|
|
+ var successfulImports = 0
|
|
|
+ var failedImports = 0
|
|
|
+ if (files != null) {
|
|
|
+ for (file in files) {
|
|
|
+ if (file.isDirectory) {
|
|
|
+ val baseSaveDir =
|
|
|
+ NativeLibrary.getSavePath(BigInteger(file.name, 16).toString())
|
|
|
+ if (baseSaveDir.isEmpty()) {
|
|
|
+ failedImports++
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ val internalSaveFolder = File(
|
|
|
+ "${DirectoryInitialization.userDirectory}/nand$baseSaveDir"
|
|
|
+ )
|
|
|
+ internalSaveFolder.deleteRecursively()
|
|
|
+ internalSaveFolder.mkdir()
|
|
|
+ file.copyRecursively(target = internalSaveFolder, overwrite = true)
|
|
|
+ successfulImports++
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ withContext(Dispatchers.Main) {
|
|
|
+ if (successfulImports == 0) {
|
|
|
+ MessageDialogFragment.newInstance(
|
|
|
+ requireActivity(),
|
|
|
+ titleId = R.string.save_file_invalid_zip_structure,
|
|
|
+ descriptionId = R.string.save_file_invalid_zip_structure_description
|
|
|
+ ).show(parentFragmentManager, MessageDialogFragment.TAG)
|
|
|
+ return@withContext
|
|
|
+ }
|
|
|
+ val successString = if (failedImports > 0) {
|
|
|
+ """
|
|
|
+ ${
|
|
|
+ requireContext().resources.getQuantityString(
|
|
|
+ R.plurals.saves_import_success,
|
|
|
+ successfulImports,
|
|
|
+ successfulImports
|
|
|
+ )
|
|
|
+ }
|
|
|
+ ${
|
|
|
+ requireContext().resources.getQuantityString(
|
|
|
+ R.plurals.saves_import_failed,
|
|
|
+ failedImports,
|
|
|
+ failedImports
|
|
|
+ )
|
|
|
+ }
|
|
|
+ """
|
|
|
+ } else {
|
|
|
+ requireContext().resources.getQuantityString(
|
|
|
+ R.plurals.saves_import_success,
|
|
|
+ successfulImports,
|
|
|
+ successfulImports
|
|
|
+ )
|
|
|
+ }
|
|
|
+ MessageDialogFragment.newInstance(
|
|
|
+ requireActivity(),
|
|
|
+ titleId = R.string.import_complete,
|
|
|
+ descriptionString = successString
|
|
|
+ ).show(parentFragmentManager, MessageDialogFragment.TAG)
|
|
|
+ }
|
|
|
+
|
|
|
+ cacheSaveDir.deleteRecursively()
|
|
|
+ } catch (e: Exception) {
|
|
|
+ Toast.makeText(
|
|
|
+ YuzuApplication.appContext,
|
|
|
+ getString(R.string.fatal_error),
|
|
|
+ Toast.LENGTH_LONG
|
|
|
+ ).show()
|
|
|
+ }
|
|
|
+ }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
|
|
+ }
|
|
|
+
|
|
|
+ private val exportSaves = registerForActivityResult(
|
|
|
+ ActivityResultContracts.CreateDocument("application/zip")
|
|
|
+ ) { result ->
|
|
|
+ if (result == null) {
|
|
|
+ return@registerForActivityResult
|
|
|
+ }
|
|
|
+
|
|
|
+ IndeterminateProgressDialogFragment.newInstance(
|
|
|
+ requireActivity(),
|
|
|
+ R.string.save_files_exporting,
|
|
|
+ false
|
|
|
+ ) {
|
|
|
+ val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
|
|
|
+ cacheSaveDir.mkdir()
|
|
|
+
|
|
|
+ val oldSaveDataFolder = File(
|
|
|
+ "${DirectoryInitialization.userDirectory}/nand" +
|
|
|
+ NativeLibrary.getDefaultProfileSaveDataRoot(false)
|
|
|
+ )
|
|
|
+ if (oldSaveDataFolder.exists()) {
|
|
|
+ oldSaveDataFolder.copyRecursively(cacheSaveDir)
|
|
|
+ }
|
|
|
+
|
|
|
+ val futureSaveDataFolder = File(
|
|
|
+ "${DirectoryInitialization.userDirectory}/nand" +
|
|
|
+ NativeLibrary.getDefaultProfileSaveDataRoot(true)
|
|
|
+ )
|
|
|
+ if (futureSaveDataFolder.exists()) {
|
|
|
+ futureSaveDataFolder.copyRecursively(cacheSaveDir)
|
|
|
+ }
|
|
|
+
|
|
|
+ val saveFilesTotal = cacheSaveDir.listFiles()?.size ?: 0
|
|
|
+ if (saveFilesTotal == 0) {
|
|
|
+ cacheSaveDir.deleteRecursively()
|
|
|
+ return@newInstance getString(R.string.no_save_data_found)
|
|
|
+ }
|
|
|
+
|
|
|
+ val zipResult = FileUtil.zipFromInternalStorage(
|
|
|
+ cacheSaveDir,
|
|
|
+ cacheSaveDir.path,
|
|
|
+ BufferedOutputStream(requireContext().contentResolver.openOutputStream(result))
|
|
|
+ )
|
|
|
+ cacheSaveDir.deleteRecursively()
|
|
|
+
|
|
|
+ return@newInstance when (zipResult) {
|
|
|
+ TaskState.Completed -> getString(R.string.export_success)
|
|
|
+ TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
|
|
|
+ }
|
|
|
+ }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
|
|
+ }
|
|
|
}
|