|
|
|
@ -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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|