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