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