@ -7,20 +7,39 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.LayoutInflater
import android.view.View
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.ViewCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.navigation.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
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.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.adapters.InstallableAdapter
import org.yuzu.yuzu_emu.adapters.InstallableAdapter
import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding
import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.Installable
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.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 ( ) {
class InstallableFragment : Fragment ( ) {
private var _binding : FragmentInstallablesBinding ? = null
private var _binding : FragmentInstallablesBinding ? = null
@ -56,6 +75,17 @@ class InstallableFragment : Fragment() {
binding . root . findNavController ( ) . popBackStack ( )
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 (
val installables = listOf (
Installable (
Installable (
R . string . user _data ,
R . string . user _data ,
@ -63,6 +93,43 @@ class InstallableFragment : Fragment() {
install = { mainActivity . importUserData . launch ( arrayOf ( " application/zip " ) ) } ,
install = { mainActivity . importUserData . launch ( arrayOf ( " application/zip " ) ) } ,
export = { mainActivity . exportUserData . launch ( " export.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 (
Installable (
R . string . install _game _content ,
R . string . install _game _content ,
R . string . install _game _content _description ,
R . string . install _game _content _description ,
@ -121,4 +188,156 @@ class InstallableFragment : Fragment() {
windowInsets
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 )
}
}
}