diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt index d5ca09906..8d3a53456 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/viewmodels/TitleUpdateViewModel.kt @@ -1,16 +1,30 @@ package org.ryujinx.android.viewmodels +import androidx.compose.runtime.MutableState import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.toLowerCase +import androidx.documentfile.provider.DocumentFile import com.anggrayudi.storage.SimpleStorageHelper +import com.anggrayudi.storage.callback.FileCallback +import com.anggrayudi.storage.file.DocumentFileCompat +import com.anggrayudi.storage.file.DocumentFileType +import com.anggrayudi.storage.file.copyFileTo import com.anggrayudi.storage.file.getAbsolutePath import com.google.gson.Gson +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.ryujinx.android.MainActivity import java.io.File +import java.util.LinkedList +import java.util.Queue import kotlin.math.max class TitleUpdateViewModel(val titleId: String) { + private var basePath: String + private var updateJsonName = "updates.json" + private var stagingUpdateJsonName = "staging_updates.json" private var storageHelper: SimpleStorageHelper var pathsState: SnapshotStateList? = null @@ -56,19 +70,123 @@ class TitleUpdateViewModel(val titleId: String) { storageHelper.openFilePicker(UpdateRequestCode) } - fun save(index: Int) { + fun save( + index: Int, + isCopying: MutableState, + openDialog: MutableState, + copyProgress: MutableState, + currentProgressName: MutableState + ) { data?.apply { this.selected = "" - if(paths.isNotEmpty() && index > 0) - { + if (paths.isNotEmpty() && index > 0) { val ind = max(index - 1, paths.count() - 1) this.selected = paths[ind] } val gson = Gson() - val json = gson.toJson(this) - jsonPath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current) - File(jsonPath).mkdirs() - File("$jsonPath/updates.json").writeText(json) + var json = gson.toJson(this) + File(basePath).mkdirs() + File("$basePath/$stagingUpdateJsonName").writeText(json) + + // Copy updates to internal data folder + val updatePath = "$basePath/update" + File(updatePath).mkdirs() + + val ioScope = CoroutineScope(Dispatchers.IO) + + var metadata = TitleUpdateMetadata() + var queue: Queue = LinkedList() + + var callback: FileCallback? = null + + fun copy(path: String) { + isCopying.value = true + val documentFile = DocumentFileCompat.fromFullPath( + storageHelper.storage.context, + path, + DocumentFileType.FILE + ) + documentFile?.apply { + val stagedPath = "$basePath/${name}" + if (!File(stagedPath).exists()) { + var file = this + ioScope.launch { + file.copyFileTo( + storageHelper.storage.context, + File(updatePath), + callback = callback!! + ) + + } + + metadata.paths.add(stagedPath) + } + } + } + + fun finish() { + val savedUpdates = mutableListOf() + File(updatePath).listFiles()?.forEach { savedUpdates.add(it.absolutePath) } + var missingFiles = + savedUpdates.filter { i -> paths.find { it.endsWith(File(i).name) } == null } + for (path in missingFiles) { + File(path).delete() + } + + val selectedName = File(selected).name + val newSelectedPath = "$updatePath/$selectedName" + if (File(newSelectedPath).exists()) { + metadata.selected = newSelectedPath + } + + json = gson.toJson(metadata) + File("$basePath/$updateJsonName").writeText(json) + + openDialog.value = false + isCopying.value = false + } + callback = object : FileCallback() { + override fun onFailed(errorCode: FileCallback.ErrorCode) { + super.onFailed(errorCode) + } + + override fun onStart(file: Any, workerThread: Thread): Long { + copyProgress.value = 0f + + (file as DocumentFile)?.apply { + currentProgressName.value = "Copying ${file.name}" + } + return super.onStart(file, workerThread) + } + + override fun onReport(report: Report) { + super.onReport(report) + + copyProgress.value = report.progress / 100f + } + + override fun onCompleted(result: Any) { + super.onCompleted(result) + + if (queue.isNotEmpty()) + copy(queue.remove()) + else { + finish() + } + } + } + for (path in paths) { + queue.add(path) + } + + ioScope.launch { + if (queue.isNotEmpty()) { + copy(queue.remove()) + } else { + finish() + } + + } } } @@ -84,12 +202,14 @@ class TitleUpdateViewModel(val titleId: String) { private var jsonPath: String init { - jsonPath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current) + "/updates.json" + basePath = MainActivity.AppPath + "/games/" + titleId.toLowerCase(Locale.current) + val stagingJson = "${basePath}/${stagingUpdateJsonName}" + jsonPath = "${basePath}/${updateJsonName}" data = TitleUpdateMetadata() - if (File(jsonPath).exists()) { + if (File(stagingJson).exists()) { val gson = Gson() - data = gson.fromJson(File(jsonPath).readText(), TitleUpdateMetadata::class.java) + data = gson.fromJson(File(stagingJson).readText(), TitleUpdateMetadata::class.java) data?.apply { val existingPaths = mutableListOf() @@ -115,4 +235,4 @@ class TitleUpdateViewModel(val titleId: String) { data class TitleUpdateMetadata( var selected: String = "", var paths: MutableList = mutableListOf() -) \ No newline at end of file +) diff --git a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt index ff0900268..558bc559b 100644 --- a/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt +++ b/src/RyujinxAndroid/app/src/main/java/org/ryujinx/android/views/TitleUpdateViews.kt @@ -11,6 +11,7 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton import androidx.compose.material3.Surface @@ -35,11 +36,16 @@ class TitleUpdateViews { val selected = remember { mutableStateOf(0) } viewModel.data?.apply { - selected.value = paths.indexOf(this.selected) + 1 + selected.value = paths.indexOf(this.selected) + 1 } Column(modifier = Modifier.padding(16.dp)) { - + val isCopying = remember { + mutableStateOf(false) + } + val copyProgress = remember { + mutableStateOf(0.0f) + } Column { Text(text = "Updates for ${name}", textAlign = TextAlign.Center) Surface( @@ -50,17 +56,19 @@ class TitleUpdateViews { ) { Column( modifier = Modifier - .height(300.dp) + .height(250.dp) .fillMaxWidth() ) { Row(modifier = Modifier.padding(8.dp)) { RadioButton( selected = (selected.value == 0), - onClick = { selected.value = 0 - }) + onClick = { + selected.value = 0 + }) Text( text = "None", - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() .align(Alignment.CenterVertically) ) } @@ -79,7 +87,8 @@ class TitleUpdateViews { onClick = { selected.value = i }) Text( text = path, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() .align(Alignment.CenterVertically) ) } @@ -113,12 +122,22 @@ class TitleUpdateViews { } } + var currentProgressName = remember { + mutableStateOf("Starting Copy") + } + if (isCopying.value) { + Text(text = "Copying updates to local storage") + Text(text = currentProgressName.value) + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + progress = copyProgress.value + ) + } Spacer(modifier = Modifier.height(18.dp)) TextButton( modifier = Modifier.align(Alignment.End), onClick = { - openDialog.value = false - viewModel.save(selected.value) + viewModel.save(selected.value, isCopying, openDialog, copyProgress, currentProgressName) }, ) { Text("Save") @@ -126,4 +145,4 @@ class TitleUpdateViews { } } } -} \ No newline at end of file +}