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