mirror of https://git.suyu.dev/suyu/suyu.git
324 lines
13 KiB
Kotlin
324 lines
13 KiB
Kotlin
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
package org.suyu.suyu_emu.fragments
|
|
|
|
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.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.suyu.suyu_emu.NativeLibrary
|
|
import org.suyu.suyu_emu.R
|
|
import org.suyu.suyu_emu.SuyuApplication
|
|
import org.suyu.suyu_emu.adapters.InstallableAdapter
|
|
import org.suyu.suyu_emu.databinding.FragmentInstallablesBinding
|
|
import org.suyu.suyu_emu.model.HomeViewModel
|
|
import org.suyu.suyu_emu.model.Installable
|
|
import org.suyu.suyu_emu.model.TaskState
|
|
import org.suyu.suyu_emu.ui.main.MainActivity
|
|
import org.suyu.suyu_emu.utils.DirectoryInitialization
|
|
import org.suyu.suyu_emu.utils.FileUtil
|
|
import org.suyu.suyu_emu.utils.ViewUtils.updateMargins
|
|
import org.suyu.suyu_emu.utils.collect
|
|
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
|
|
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()
|
|
}
|
|
|
|
homeViewModel.openImportSaves.collect(viewLifecycleOwner) {
|
|
if (it) {
|
|
importSaves.launch(arrayOf("application/zip"))
|
|
homeViewModel.setOpenImportSaves(false)
|
|
}
|
|
}
|
|
|
|
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.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(
|
|
SuyuApplication.appContext,
|
|
R.string.no_save_data_found,
|
|
Toast.LENGTH_SHORT
|
|
).show()
|
|
return@Installable
|
|
} else {
|
|
exportSaves.launch(
|
|
"${getString(R.string.save_data)} " +
|
|
LocalDateTime.now().format(
|
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
|
|
)
|
|
)
|
|
}
|
|
}
|
|
),
|
|
Installable(
|
|
R.string.install_game_content,
|
|
R.string.install_game_content_description,
|
|
install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) }
|
|
),
|
|
Installable(
|
|
R.string.install_firmware,
|
|
R.string.install_firmware_description,
|
|
install = { mainActivity.getFirmware.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
|
|
|
|
binding.toolbarInstallables.updateMargins(left = leftInsets, right = rightInsets)
|
|
binding.listInstallables.updateMargins(left = leftInsets, right = rightInsets)
|
|
|
|
binding.listInstallables.updatePadding(bottom = barInsets.bottom)
|
|
|
|
windowInsets
|
|
}
|
|
|
|
private val importSaves =
|
|
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
|
if (result == null) {
|
|
return@registerForActivityResult
|
|
}
|
|
|
|
val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
|
|
cacheSaveDir.mkdir()
|
|
|
|
ProgressDialogFragment.newInstance(
|
|
requireActivity(),
|
|
R.string.save_files_importing,
|
|
false
|
|
) { progressCallback, _ ->
|
|
try {
|
|
FileUtil.unzipToInternalStorage(
|
|
result.toString(),
|
|
cacheSaveDir,
|
|
progressCallback
|
|
)
|
|
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(
|
|
SuyuApplication.appContext,
|
|
getString(R.string.fatal_error),
|
|
Toast.LENGTH_LONG
|
|
).show()
|
|
}
|
|
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
|
}
|
|
|
|
private val exportSaves = registerForActivityResult(
|
|
ActivityResultContracts.CreateDocument("application/zip")
|
|
) { result ->
|
|
if (result == null) {
|
|
return@registerForActivityResult
|
|
}
|
|
|
|
ProgressDialogFragment.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, ProgressDialogFragment.TAG)
|
|
}
|
|
}
|